/**
* Copyright (C) 2009 eXo Platform SAS.
*
* This is free software; you can redistribute it and/or modify it
* under the terms of the GNU Lesser General Public License as
* published by the Free Software Foundation; either version 2.1 of
* the License, or (at your option) any later version.
*
* This software is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
* Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public
* License along with this software; if not, write to the Free
* Software Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA
* 02110-1301 USA, or see the FSF site: http://www.fsf.org.
*/
package org.exoplatform.web.application.javascript;
import javax.servlet.ServletContext;
import java.io.IOException;
import java.io.Reader;
import java.io.StringReader;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.Comparator;
import java.util.HashMap;
import java.util.Iterator;
import java.util.LinkedList;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import java.util.Set;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import org.apache.commons.lang.StringUtils;
import org.exoplatform.commons.utils.CompositeReader;
import org.exoplatform.commons.utils.PropertyManager;
import org.exoplatform.container.ExoContainerContext;
import org.exoplatform.portal.resource.AbstractResourceService;
import org.exoplatform.portal.resource.compressor.ResourceCompressor;
import org.exoplatform.web.ControllerContext;
import org.exoplatform.web.controller.router.URIWriter;
import org.gatein.common.logging.Logger;
import org.gatein.common.logging.LoggerFactory;
import org.gatein.portal.controller.resource.ResourceId;
import org.gatein.portal.controller.resource.ResourceScope;
import org.gatein.portal.controller.resource.script.BaseScriptResource;
import org.gatein.portal.controller.resource.script.FetchMode;
import org.gatein.portal.controller.resource.script.Module;
import org.gatein.portal.controller.resource.script.ScriptGraph;
import org.gatein.portal.controller.resource.script.ScriptGroup;
import org.gatein.portal.controller.resource.script.ScriptResource;
import org.gatein.portal.controller.resource.script.ScriptResource.DepInfo;
import org.gatein.wci.ServletContainerFactory;
import org.gatein.wci.WebApp;
import org.gatein.wci.WebAppListener;
import org.json.JSONArray;
import org.json.JSONObject;
import org.picocontainer.Startable;
public class JavascriptConfigService extends AbstractResourceService implements Startable
{
/** Our logger. */
private final Logger log = LoggerFactory.getLogger(JavascriptConfigService.class);
/** The scripts. */
final ScriptGraph scripts;
/** . */
private final WebAppListener deployer;
/** . */
public static final List<String> RESERVED_MODULE = Arrays.asList("require", "exports", "module");
/** . */
private static final Pattern INDEX_PATTERN = Pattern.compile("^.+?(_([1-9]+))$");
/** . */
public static final Comparator<Module> MODULE_COMPARATOR = new Comparator<Module>()
{
public int compare(Module o1, Module o2)
{
return o1.getPriority() - o2.getPriority();
}
};
public JavascriptConfigService(ExoContainerContext context, ResourceCompressor compressor)
{
super(compressor);
//
this.scripts = new ScriptGraph();
this.deployer = new JavascriptConfigDeployer(context.getPortalContainerName(), this);
}
public Reader getScript(ResourceId resourceId, Locale locale) throws Exception
{
if (ResourceScope.GROUP.equals(resourceId.getScope()))
{
ScriptGroup loadGroup = scripts.getLoadGroup(resourceId.getName());
if (loadGroup != null)
{
List<Reader> readers = new ArrayList<Reader>(loadGroup.getDependencies().size());
for (ResourceId id : loadGroup.getDependencies())
{
Reader rd = getScript(id, locale);
if (rd != null)
{
readers.add(new StringReader("\n//Begin " + id));
readers.add(rd);
readers.add(new StringReader("\n//End " + id));
}
}
return new CompositeReader(readers);
}
else
{
return null;
}
}
else
{
ScriptResource resource = getResource(resourceId);
if (resource != null)
{
List<Module> modules = new ArrayList<Module>(resource.getModules());
Collections.sort(modules, MODULE_COMPARATOR);
ArrayList<Reader> readers = new ArrayList<Reader>(modules.size() * 2);
StringBuilder buffer = new StringBuilder();
//
boolean isModule = FetchMode.ON_LOAD.equals(resource.getFetchMode());
if (isModule)
{
JSONArray deps = new JSONArray();
LinkedList<String> params = new LinkedList<String>();
List<String> argNames = new LinkedList<String>();
List<String> argValues = new LinkedList<String>(params);
for (ResourceId id : resource.getDependencies())
{
ScriptResource dep = getResource(id);
if (dep != null)
{
Set<DepInfo> depInfos = resource.getDepInfo(id);
for (DepInfo info : depInfos)
{
String pluginRS = info.getPluginRS();
String alias = info.getAlias();
if (alias == null)
{
alias = dep.getAlias();
}
deps.put(parsePluginRS(dep.getId().toString(), pluginRS));
params.add(encode(params, alias));
argNames.add(parsePluginRS(alias, pluginRS));
}
}
else if (RESERVED_MODULE.contains(id.getName()))
{
String reserved = id.getName();
deps.put(reserved);
params.add(reserved);
argNames.add(reserved);
}
}
argValues.addAll(params);
int reserveIdx = argValues.indexOf("require");
if (reserveIdx != -1)
{
argValues.set(reserveIdx, "eXo.require");
}
//
buffer.append("\ndefine('").append(resourceId).append("', ");
buffer.append(deps);
buffer.append(", function(");
buffer.append(StringUtils.join(params, ","));
buffer.append(") {\nvar require = eXo.require, requirejs = eXo.require,define = eXo.define;");
buffer.append("\neXo.define.names=").append(new JSONArray(argNames)).append(";");
buffer.append("\neXo.define.deps=[").append(StringUtils.join(argValues, ",")).append("]").append(";");
buffer.append("\nreturn ");
}
//
for (Module js : modules)
{
Reader jScript = getJavascript(js, locale);
if (jScript != null)
{
readers.add(new StringReader(buffer.toString()));
buffer.setLength(0);
readers.add(new NormalizeJSReader(jScript));
}
}
if (isModule)
{
buffer.append("\n});");
}
else
{
buffer.append("\nif (typeof define === 'function' && define.amd && !require.specified('").append(resource.getId()).append("')) {");
buffer.append("define('").append(resource.getId()).append("');}");
}
readers.add(new StringReader(buffer.toString()));
return new CompositeReader(readers);
}
else
{
return null;
}
}
}
@SuppressWarnings("unchecked")
public String generateURL(
ControllerContext controllerContext,
ResourceId id,
boolean merge,
boolean minified,
Locale locale) throws IOException
{
@SuppressWarnings("rawtypes")
BaseScriptResource resource = null;
if (ResourceScope.GROUP.equals(id.getScope()))
{
resource = scripts.getLoadGroup(id.getName());
}
else
{
resource = getResource(id);
}
//
if (resource != null)
{
if (resource instanceof ScriptResource)
{
ScriptResource rs = (ScriptResource)resource;
List<Module> modules = rs.getModules();
if (modules.size() > 0 && modules.get(0) instanceof Module.Remote)
{
return ((Module.Remote)modules.get(0)).getURI();
}
}
StringBuilder buffer = new StringBuilder();
URIWriter writer = new URIWriter(buffer);
controllerContext.renderURL(resource.getParameters(minified, locale), writer);
return buffer.toString();
}
else
{
return null;
}
}
public Map<ScriptResource, FetchMode> resolveIds(Map<ResourceId, FetchMode> ids)
{
return scripts.resolve(ids);
}
public JSONObject getJSConfig(ControllerContext controllerContext, Locale locale) throws Exception
{
JSONObject paths = new JSONObject();
JSONObject shim = new JSONObject();
Map<ResourceId, String> groupURLs = new HashMap<ResourceId, String>();
for (ScriptResource resource : getAllResources())
{
if (!resource.isEmpty() || ResourceScope.SHARED.equals(resource.getId().getScope()))
{
String name = resource.getId().toString();
List<Module> modules = resource.getModules();
if (FetchMode.IMMEDIATE.equals(resource.getFetchMode()) || (modules.size() > 0 && modules.get(0) instanceof Module.Remote))
{
JSONArray deps = new JSONArray();
for (ResourceId id : resource.getDependencies())
{
deps.put(getResource(id).getId());
}
if (deps.length() > 0)
{
shim.put(name, new JSONObject().put("deps", deps));
}
}
String url;
ScriptGroup group = resource.getGroup();
if (group != null)
{
ResourceId grpId = group.getId();
url = groupURLs.get(grpId);
if (url == null)
{
url = buildURL(grpId, controllerContext, locale);
groupURLs.put(grpId, url);
}
}
else
{
url = buildURL(resource.getId(), controllerContext, locale);
}
paths.put(name, url);
}
}
JSONObject config = new JSONObject();
config.put("paths", paths);
config.put("shim", shim);
return config;
}
public ScriptResource getResource(ResourceId resource)
{
return scripts.getResource(resource);
}
/**
* Start service.
* Registry org.exoplatform.web.application.javascript.JavascriptDeployer,
* org.exoplatform.web.application.javascript.JavascriptRemoval into ServletContainer
* @see org.picocontainer.Startable#start()
*/
public void start()
{
log.debug("Registering JavascriptConfigService for servlet container events");
ServletContainerFactory.getServletContainer().addWebAppListener(deployer);
}
/**
* Stop service.
* Remove org.exoplatform.web.application.javascript.JavascriptDeployer,
* org.exoplatform.web.application.javascript.JavascriptRemoval from ServletContainer
* @see org.picocontainer.Startable#stop()
*/
public void stop()
{
log.debug("Unregistering JavascriptConfigService for servlet container events");
ServletContainerFactory.getServletContainer().removeWebAppListener(deployer);
}
private Reader getJavascript(Module module, Locale locale)
{
if (module instanceof Module.Local)
{
Module.Local localModule = (Module.Local)module;
final WebApp webApp = contexts.get(localModule.getContextPath());
if (webApp != null)
{
ServletContext sc = webApp.getServletContext();
return localModule.read(locale, sc, webApp.getClassLoader());
}
}
return null;
}
private String buildURL(ResourceId id, ControllerContext context, Locale locale) throws Exception
{
String url = generateURL(context, id, !PropertyManager.isDevelopping(),
!PropertyManager.isDevelopping(), locale);
if (url != null && url.endsWith(".js"))
{
return url.substring(0, url.length() - ".js".length());
}
else
{
return null;
}
}
private List<ScriptResource> getAllResources()
{
List<ScriptResource> resources = new LinkedList<ScriptResource>();
for (ResourceScope scope : ResourceScope.values())
{
resources.addAll(scripts.getResources(scope));
}
return resources;
}
private String encode(LinkedList<String> params, String alias)
{
alias = alias.replace("/", "_");
int idx = -1;
Iterator<String> iterator = params.descendingIterator();
while (iterator.hasNext())
{
String param = iterator.next();
Matcher matcher = INDEX_PATTERN.matcher(param);
if (matcher.matches())
{
idx = Integer.parseInt(matcher.group(2));
break;
}
else if (alias.equals(param))
{
idx = 0;
break;
}
}
if (idx != -1)
{
StringBuilder tmp = new StringBuilder(alias);
tmp.append("_").append(idx + 1);
return tmp.toString();
}
else
{
return alias;
}
}
private String parsePluginRS(String name, String pluginRS)
{
StringBuilder depBuild = new StringBuilder(name);
if (pluginRS != null)
{
depBuild.append("!").append(pluginRS);
}
return depBuild.toString();
}
private class NormalizeJSReader extends Reader
{
private boolean finished = false;
private boolean multiComments = false;
private boolean singleComment = false;
private Reader sub;
public NormalizeJSReader(Reader sub)
{
this.sub = sub;
}
@Override
public int read(char[] cbuf, int off, int len) throws IOException
{
if (finished)
{
return sub.read(cbuf, off, len);
}
else
{
char[] buffer = new char[len];
int relLen = sub.read(buffer, 0, len);
if (relLen == -1)
{
finished = true;
return -1;
}
else
{
int r = off;
for (int i = 0; i < relLen; i++)
{
char c = buffer[i];
char next = 0;
boolean skip = false, overflow = (i + 1 == relLen);
if (!finished)
{
skip = true;
if (!singleComment && c == '/' && (next = readNext(buffer, i, overflow)) == '*')
{
multiComments = true;
i++;
}
else if (!singleComment && c == '*' && (next = readNext(buffer, i, overflow)) == '/')
{
multiComments = false;
i++;
}
else if (!multiComments && c == '/' && next == '/')
{
singleComment = true;
i++;
}
else if (c == '\n')
{
singleComment = false;
}
else if (c != ' ')
{
skip = false;
}
if (!skip && !multiComments && !singleComment)
{
if (next != 0 && overflow)
{
sub = new CompositeReader(new StringReader(String.valueOf(c)), sub);
}
cbuf[r++] = c;
finished = true;
}
}
else
{
cbuf[r++] = c;
}
}
return r - off;
}
}
}
private char readNext(char[] buffer, int i, boolean overflow) throws IOException
{
char c = 0;
if (overflow)
{
int tmp = sub.read();
if (tmp != -1)
{
c = (char)tmp;
}
}
else
{
c = buffer[i + 1];
}
return c;
}
@Override
public void close() throws IOException
{
sub.close();
}
}
}