/***************************************
* Copyright (c) 2008-2010
* Philipp Berger 2009
* Intalio, Inc 2010
*
* Permission is hereby granted, free of charge, to any person obtaining a
* copy of this software and associated documentation files (the "Software"),
* to deal in the Software without restriction, including without limitation
* the rights to use, copy, modify, merge, publish, distribute, sublicense,
* and/or sell copies of the Software, and to permit persons to whom the
* Software is furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in
* all copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
* FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
* DEALINGS IN THE SOFTWARE.
****************************************/
package org.wapama.web;
import java.io.ByteArrayOutputStream;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileWriter;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.io.StringWriter;
import java.util.ArrayList;
import java.util.Collection;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.StringTokenizer;
import java.util.WeakHashMap;
import javax.servlet.ServletConfig;
import javax.servlet.ServletContext;
import javax.servlet.ServletException;
import javax.servlet.http.Cookie;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import org.jdom.Document;
import org.jdom.Element;
import org.jdom.JDOMException;
import org.jdom.Namespace;
import org.jdom.input.SAXBuilder;
import org.jdom.output.Format;
import org.jdom.output.XMLOutputter;
import org.json.JSONArray;
import org.json.JSONException;
import org.json.JSONObject;
import org.mozilla.javascript.EvaluatorException;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.wapama.web.plugin.IDiagramPlugin;
import org.wapama.web.plugin.IDiagramPluginService;
import org.wapama.web.plugin.impl.PluginServiceImpl;
import org.wapama.web.preference.IDiagramPreference;
import org.wapama.web.preference.IDiagramPreferenceService;
import org.wapama.web.profile.IDiagramProfile;
import org.wapama.web.profile.IDiagramProfileService;
import org.wapama.web.profile.impl.ProfileServiceImpl;
import com.yahoo.platform.yui.compressor.JavaScriptCompressor;
/**
* Servlet to load plugin and Wapama stencilset
*/
public class EditorHandler extends HttpServlet {
private static final long serialVersionUID = -7439613152623067053L;
/**
* da logger
*/
private static final Logger _logger =
LoggerFactory.getLogger(EditorHandler.class);
public static IDiagramPreferenceService PREFERENCE_FACTORY = new IDiagramPreferenceService() {
public IDiagramPreference createPreference(HttpServletRequest req) {
//later we could read the parameters from the URL as well.
return new IDiagramPreference() {
public boolean isAutoSaveEnabled() {
return true;
}
public int getAutosaveInterval() {
return 120000;
}
};
}
};
/**
* The base path under which the application will be made available at runtime.
* This constant should be used throughout the application.
*/
public static final String wapama_path = "/designer/";
/**
* The designer DEV flag.
* When set, the logging will be enabled at the javascript level
*/
public static final String DEV = "designer.dev";
/**
* The profile service, a global registry to get the
* profiles.
*/
private IDiagramProfileService _profileService = null;
/**
* The plugin service, a global registry for all plugins.
*/
private IDiagramPluginService _pluginService = null;
private List<String> _envFiles = new ArrayList<String>();
private Map<String, List<IDiagramPlugin>> _pluginfiles =
new HashMap<String, List<IDiagramPlugin>>();
private Map<String, List<IDiagramPlugin>> _uncompressedPlugins =
new WeakHashMap<String, List<IDiagramPlugin>>();
/**
* editor.html document.
*/
private Document _doc = null;
public void init(ServletConfig config) throws ServletException {
super.init(config);
_profileService = ProfileServiceImpl.INSTANCE;
_profileService.init(config.getServletContext());
_pluginService = PluginServiceImpl.getInstance(
config.getServletContext());
String editor_file = config.
getServletContext().getRealPath("/editor.html");
try {
_doc = readDocument(editor_file);
} catch (Exception e) {
throw new ServletException(
"Error while parsing editor.html", e);
}
if (_doc == null) {
_logger.error("Invalid editor.html, " +
"could not be read as a document.");
throw new ServletException("Invalid editor.html, " +
"could not be read as a document.");
}
Element root = _doc.getRootElement();
Element head = root.getChild("head", root.getNamespace());
if (head == null) {
_logger.error("Invalid editor.html. No html or head tag");
throw new ServletException("Invalid editor.html. " +
"No html or head tag");
}
try {
initEnvFiles(getServletContext());
} catch (IOException e) {
throw new ServletException(e);
}
}
/**
* Initiate the compression of the environment.
* @param context
* @throws IOException
*/
private void initEnvFiles(ServletContext context) throws IOException {
// only do it the first time the servlet starts
try {
JSONObject obj = new JSONObject(readEnvFiles(context));
JSONArray array = obj.getJSONArray("files");
for (int i = 0 ; i < array.length() ; i++) {
_envFiles.add(array.getString(i));
}
} catch (JSONException e) {
_logger.error("invalid js_files.json");
_logger.error(e.getMessage(), e);
throw new RuntimeException("Error initializing the " +
"environment of the editor");
}
// generate script to setup the languages
//_envFiles.add("i18n/translation_ja.js");
if (System.getProperty(DEV) == null) {
StringWriter sw = new StringWriter();
for (String file : _envFiles) {
sw.append("/* ").append(file).append(" */\n");
InputStream input = new FileInputStream(new File(getServletContext().getRealPath(file)));
try {
JavaScriptCompressor compressor = new JavaScriptCompressor(
new InputStreamReader(input), null);
compressor.compress(sw, -1, false, false, false, false);
} catch (EvaluatorException e) {
_logger.error(e.getMessage(), e);
} catch (IOException e) {
_logger.error(e.getMessage(), e);
} finally {
try { input.close(); } catch (IOException e) {}
}
sw.append("\n");
}
try {
FileWriter w = new FileWriter(
context.getRealPath("jsc/env_combined.js"));
w.write(sw.toString());
w.close();
} catch (IOException e) {
_logger.error(e.getMessage(), e);
}
} else {
if (_logger.isInfoEnabled()) {
_logger.info(
"The diagram editor is running in development mode. " +
"Javascript will be served uncompressed");
}
}
}
protected void doGet(HttpServletRequest request,
HttpServletResponse response)
throws ServletException, IOException {
Document doc = (Document) _doc.clone();
String profileName = request.getParameter("profile");
IDiagramProfile profile = _profileService.findProfile(
request, profileName);
if (profile == null) {
_logger.error("No profile with the name " + profileName
+ " was registered");
throw new IllegalArgumentException(
"No profile with the name " + profileName +
" was registered");
}
//output env javascript files
if (System.getProperty(DEV) != null) {
for (String jsFile : _envFiles) {
addScript(doc, wapama_path + jsFile, true);
}
} else {
addScript(doc, wapama_path + "jsc/env_combined.js", true);
}
// get language from cookie
String lang = null;
Cookie[] cookies = request.getCookies();
for (Cookie cookie : cookies) {
if ("locale".equals(cookie.getName())) {
lang = cookie.getValue().trim();
break;
}
}
// i18n message resource file name
String i18nJsFile = null;
if ("de_DE".equals(lang)) {
i18nJsFile = "i18n/translation_de.js";
} else if ("ru".equals(lang)){
i18nJsFile = "i18n/translation_ru.js";
} else if ("es".equals(lang)){
i18nJsFile = "i18n/translation_es.js";
} else if ("ja_JP".equals(lang)){
i18nJsFile = "i18n/translation_ja.js";
} else {
i18nJsFile = "i18n/translation_en_us.js";
}
// generate script to setup the languages
addScript(doc, wapama_path + i18nJsFile, true);
// generate script tags for plugins.
// they are located after the initialization script.
if (_pluginfiles.get(profileName) == null) {
List<IDiagramPlugin> compressed = new ArrayList<IDiagramPlugin>();
List<IDiagramPlugin> uncompressed = new ArrayList<IDiagramPlugin>();
_pluginfiles.put(profileName, compressed);
_uncompressedPlugins.put(profileName, uncompressed);
for (String pluginName : profile.getPlugins()) {
IDiagramPlugin plugin = _pluginService.findPlugin(request,
pluginName);
if (plugin == null) {
_logger.warn("Could not find the plugin " + pluginName +
" requested by the profile " + profile.getName());
continue;
}
if (plugin.isCompressable()) {
compressed.add(plugin);
} else {
uncompressed.add(plugin);
}
}
if (System.getProperty(DEV) == null) {
// let's call the compression routine
String rs = compressJS(_pluginfiles.get(profileName),
getServletContext());
try {
FileWriter w = new FileWriter(getServletContext().
getRealPath("jsc/plugins_" + profileName
+ ".js"));
w.write(rs.toString());
w.close();
} catch (Exception e) {
_logger.error(e.getMessage(), e);
}
}
}
if (System.getProperty(DEV) != null) {
for (IDiagramPlugin jsFile : _pluginfiles.get(profileName)) {
addScript(doc, wapama_path + "plugin/" + jsFile.getName()
+ ".js", true);
}
} else {
addScript(doc,
wapama_path + "jsc/plugins_" + profileName + ".js",
false);
}
for (IDiagramPlugin uncompressed :
_uncompressedPlugins.get(profileName)) {
addScript(doc, wapama_path + "plugin/" + uncompressed.getName()
+ ".js", false);
}
// send the updated editor.html to client
if(!isIE(request)){
response.setContentType("application/xhtml+xml");
}
XMLOutputter outputter = new XMLOutputter();
Format format = Format.getPrettyFormat();
format.setExpandEmptyElements(true);
outputter.setFormat(format);
String html = outputter.outputString(doc);
StringTokenizer tokenizer = new StringTokenizer(
html, "@", true);
StringBuilder resultHtml = new StringBuilder();
boolean tokenFound = false;
boolean replacementMade = false;
IDiagramPreference pref = PREFERENCE_FACTORY.createPreference(request);
int autoSaveInt = pref.getAutosaveInterval();
boolean autoSaveOn = pref.isAutoSaveEnabled();
while(tokenizer.hasMoreTokens()) {
String elt = tokenizer.nextToken();
if ("title".equals(elt)) {
resultHtml.append(profile.getTitle());
replacementMade = true;
} else if ("stencilset".equals(elt)) {
resultHtml.append(profile.getStencilSet());
replacementMade = true;
} else if ("debug".equals(elt)) {
resultHtml.append(System.getProperty(DEV) != null);
replacementMade = true;
} else if ("autosaveinterval".equals(elt)) {
resultHtml.append(autoSaveInt);
replacementMade = true;
} else if ("autosavedefault".equals(elt)) {
resultHtml.append(autoSaveOn);
replacementMade = true;
} else if ("profileplugins".equals(elt)) {
StringBuilder plugins = new StringBuilder();
boolean commaNeeded = false;
for (String ext : profile.getPlugins()) {
if (commaNeeded) {
plugins.append(",");
} else {
commaNeeded = true;
}
plugins.append("\"").append(ext).append("\"");
}
resultHtml.append(plugins.toString());
replacementMade = true;
} else if ("ssextensions".equals(elt)) {
StringBuilder ssexts = new StringBuilder();
boolean commaNeeded = false;
for (String ext : profile.getStencilSetExtensions()) {
if (commaNeeded) {
ssexts.append(",");
} else {
commaNeeded = true;
}
ssexts.append("\"").append(ext).append("\"");
}
resultHtml.append(ssexts.toString());
replacementMade = true;
} else if ("@".equals(elt)) {
if (replacementMade) {
tokenFound = false;
replacementMade = false;
} else {
tokenFound = true;
}
} else {
if (tokenFound) {
tokenFound = false;
resultHtml.append("@");
}
resultHtml.append(elt);
}
}
response.getWriter().write(resultHtml.toString());
}
/**
* Reads the document from the file at the given path
* @param path the path to the file
* @return a document
* @throws JDOMException
* @throws IOException
*/
private static Document readDocument(String path)
throws JDOMException, IOException {
SAXBuilder builder = new SAXBuilder(false);
// no DTD validation
builder.setValidation(false);
builder.setFeature("http://xml.org/sax/features/validation", false);
builder.setFeature("http://apache.org/xml/features/nonvalidating/load-dtd-grammar", false);
builder.setFeature("http://apache.org/xml/features/nonvalidating/load-external-dtd", false);
Document anotherDocument = builder.build(new File(path));
return anotherDocument;
}
/**
* Adds a script to the head.
* @param doc the document to use.
* @param src the location of the script
*/
private void addScript(Document doc, String src, boolean isCore) {
Namespace nm = doc.getRootElement().getNamespace();
Element script = new Element("script", nm);
// set the attributes
script.setAttribute("src", src);
script.setAttribute("type", "text/javascript");
//add an empty text node in them
script.addContent("");
// put it to the right place
Element head = doc.getRootElement().getChild("head", nm);
if (isCore) {
// then place it first.
//insert before the last script tag.
head.addContent(head.getContentSize() -2, script);
} else {
head.addContent(script);
}
return;
}
/**
* @return read the files to be placed as core scripts
* from a configuration file in a json file.
* @throws IOException
*/
private static String readEnvFiles(ServletContext context) throws IOException {
FileInputStream core_scripts = new FileInputStream(
context.getRealPath("/js/js_files.json"));
try {
ByteArrayOutputStream stream =
new ByteArrayOutputStream();
byte[] buffer = new byte[4096];
int read;
while ((read = core_scripts.read(buffer)) != -1) {
stream.write(buffer, 0, read);
}
return stream.toString();
} finally {
try {
core_scripts.close();
} catch (IOException e) {
_logger.error(e.getMessage(), e);
}
}
}
/**
* Compress a list of js files into one combined string
* @param a list of js files
* @return a string that contains all the compressed data
* @throws EvaluatorException
* @throws IOException
*/
private static String compressJS(Collection<IDiagramPlugin> plugins,
ServletContext context) {
StringWriter sw = new StringWriter();
for (IDiagramPlugin plugin : plugins) {
sw.append("/* ").append(plugin.getName()).append(" */\n");
InputStream input = plugin.getContents();
try {
JavaScriptCompressor compressor = new JavaScriptCompressor(
new InputStreamReader(input), null);
compressor.compress(sw, -1, false, false, false, false);
} catch (EvaluatorException e) {
_logger.error(e.getMessage(), e);
} catch (IOException e) {
_logger.error(e.getMessage(), e);
} finally {
try { input.close(); } catch (IOException e) {}
}
sw.append("\n");
}
return sw.toString();
}
/**
* Determine whether the browser is IE
* @param request
* @return true: IE browser false: others browsers
*/
private static boolean isIE(HttpServletRequest request){
return request.getHeader("USER-AGENT").
toLowerCase().indexOf("msie") > 0;
}
}