/**
* 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 java.io.IOException;
import java.io.Reader;
import java.io.StringReader;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.Collections;
import java.util.Comparator;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Iterator;
import java.util.LinkedHashMap;
import java.util.LinkedList;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import java.util.Map.Entry;
import java.util.Set;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import javax.servlet.ServletContext;
import org.apache.commons.lang.StringUtils;
import org.exoplatform.commons.utils.CompositeReader;
import org.exoplatform.commons.utils.PropertyManager;
import org.exoplatform.container.ContainerLifecyclePlugin;
import org.exoplatform.container.ExoContainer;
import org.exoplatform.container.ExoContainerContext;
import org.exoplatform.container.RootContainer;
import org.exoplatform.container.xml.InitParams;
import org.exoplatform.portal.resource.AbstractResourceService;
import org.exoplatform.portal.resource.InvalidResourceException;
import org.exoplatform.portal.resource.compressor.ResourceCompressor;
import org.exoplatform.services.log.ExoLogger;
import org.exoplatform.services.log.Log;
import org.exoplatform.web.ControllerContext;
import org.exoplatform.web.controller.QualifiedName;
import org.exoplatform.web.controller.router.URIWriter;
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.portal.controller.resource.script.StaticScriptResource;
import org.gatein.wci.WebApp;
import org.json.JSONArray;
import org.json.JSONObject;
public class JavascriptConfigService extends AbstractResourceService {
/**
* <a href="http://requirejs.org/docs/api.html#config-paths">require.js path mappings</a>
* for module names not found directly under require.js's {@code baseUrl}.
* For a given {@link #pathMappings} entry, the key is the prefix not found in under {@code baseUrl}
* and the value is (possibly a portal-external) path to be used instead of the prefix.
* The value of an entry is actually a {@link List} of substitute paths, to mirror the
* <a href="http://requirejs.org/docs/api.html#pathsfallbacks">fallback paths</a>
* feature of require.js.
* <p>
* Internally, a {@link LinkedHashMap} is used is used to store the prefix to target path mapping,
* because the order of paths matters - they represent a fallback sequence tried in the given
* order by require.js.
* <p>
* This class is deeply immutable, {@link #add(String, Map)} and {@link #remove(String)} methods
* return a new {@link PathMappings} instance or {@code this} if there is nothing to change.
*
* @author <a href="mailto:ppalaga@redhat.com">Peter Palaga</a>
*/
static class PathMappings {
private static final PathMappings EMPTY = new PathMappings();
public static PathMappings empty() {
return EMPTY;
}
/** Path prefixes are mapped to target paths. Always a {@link LinkedHashMap}
* because the order matters - see above. */
private final Map<String, List<String>> entries;
/** A place to store which prefixes were registered from which servlet context. */
private final Map<String, Set<String>> prefixesToContextPaths;
/**
* Both parameters must be immutable.
*
* @param entries
* @param prefixesToContextPaths
*/
private PathMappings(Map<String, List<String>> entries, Map<String, Set<String>> prefixesToContextPaths) {
super();
this.entries = entries;
this.prefixesToContextPaths = prefixesToContextPaths;
}
public PathMappings() {
this.entries = Collections.emptyMap();
this.prefixesToContextPaths = Collections.emptyMap();
}
/**
* Creates a new {@link PathMappings} instance by first copying {@link #entries} from
* {@code this}, then adding all elements from {@code pathEntries} parameter to the
* {@link #entries} of the new instance, returning the new instance.
*
* In this method, we are adding a map of path entries like
* {@code ['/dojo' -> 'http://cdn.com/dojo', '/whatever' -> 'http://cdn.com/whatever']}.
* Keys in that map are called <i>prefixes</i> and values are called <i>target paths</i>.
* We are adding these entries to a map of entries that have been registered before and for
* each of the entries that are being added, we check, if it breaks the internal consistency
* of the newly created PathMappings. There are three cases possible:
* <ol>
* <li>If the prefix is still not available in {@link #entries}, the entry is
* added to {@link #entries}</li>
* <li>If the prefix is already available in {@link #entries} and the available
* target path is equal to the one being added, the entry is also added to {@link #entries}</li>
* <li>If the prefix is already available in {@link #entries} and the available
* target path is not equal to the one being added, a {@link DuplicateResourceKeyException} is thrown.</li>
* </ol>
* @param contextPath the servlet context path
* @param pathEntries the entries to add
* @return see above
* @throws DuplicateResourceKeyException if a key of an added entry is available
* in {@link #pathMappings} and the available
* target path is not equal to the one being added
*/
public PathMappings add(String contextPath, Map<String, List<String>> pathEntries) throws DuplicateResourceKeyException {
if (pathEntries == null || pathEntries.isEmpty()) {
return this;
} else {
final Map<String, List<String>> newPrefixesToTargetPaths = new LinkedHashMap<String, List<String>>(this.entries);
final Map<String, Set<String>> newPrefixesToContextPaths = new HashMap<String, Set<String>>(this.prefixesToContextPaths);
for (Entry<String, List<String>> en : pathEntries.entrySet()) {
String prefix = en.getKey();
List<String> availableValue = newPrefixesToTargetPaths.get(prefix);
if (availableValue != null) {
if (availableValue.equals(en.getValue())) {
/* no need to add to newPrefixesToTargetPaths because it is already there
* just remember the present context path */
Set<String> newContextPaths = new HashSet<String>(newPrefixesToContextPaths.get(prefix));
newContextPaths.add(contextPath);
newPrefixesToContextPaths.put(prefix, Collections.unmodifiableSet(newContextPaths));
} else {
Set<String> contextPaths = newPrefixesToContextPaths.get(prefix);
throw new DuplicateResourceKeyException("Cannot accept path mapping entry " + en
+ " from servlet context '"+ contextPath
+"' because the given prefix '"+ prefix +"' was already registered by servlet contexts "+ contextPaths +". The registered target path is "
+ availableValue);
}
} else {
/* The prefix is not available yet. */
if (log.isDebugEnabled()) {
log.debug("Adding path entry " + en);
}
newPrefixesToTargetPaths.put(prefix, Collections.unmodifiableList(new ArrayList<String>(en.getValue())));
newPrefixesToContextPaths.put(prefix, Collections.singleton(contextPath));
}
}
return new PathMappings(Collections.unmodifiableMap(newPrefixesToTargetPaths), Collections.unmodifiableMap(newPrefixesToContextPaths));
}
}
/**
* Creates a new {@link PathMappings} instance by copying {@link #entries} from {@code this},
* removes all entries that were registered for the given {@code contextPath} from the
* new instance and returns the new instance.
*
* @param contextPath the servlet context path
* @return see above
*/
public PathMappings remove(String contextPath) {
Map<String, List<String>> newPrefixesToTargetPaths = this.entries;
Map<String, Set<String>> newPrefixesToContextPaths = this.prefixesToContextPaths;
for (Entry<String, Set<String>> en : this.prefixesToContextPaths.entrySet()) {
String prefix = en.getKey();
Set<String> contextPaths = en.getValue();
if (contextPaths.contains(contextPath)) {
if (newPrefixesToTargetPaths == this.entries) {
/* we hit the first change, so prepare mutable objects */
newPrefixesToTargetPaths = new LinkedHashMap<String, List<String>>(this.entries);
newPrefixesToContextPaths = new HashMap<String, Set<String>>(this.prefixesToContextPaths);
}
switch (contextPaths.size()) {
case 0:
/* should never happen */
throw new IllegalStateException("contextPaths set should never have size 0");
case 1:
/* we are removing the last context that relied on this prefix
* hence we can remove the entry from newPrefixesToTargetPaths */
newPrefixesToTargetPaths.remove(prefix);
newPrefixesToContextPaths.remove(prefix);
break;
default:
/* copy the set and remove the present context path from it */
Set<String> newContextPaths = new HashSet<String>(newPrefixesToContextPaths.get(prefix));
newContextPaths.remove(contextPath);
newPrefixesToContextPaths.put(prefix, Collections.unmodifiableSet(newContextPaths));
break;
}
}
}
if (newPrefixesToTargetPaths == this.entries) {
return this;
} else {
return new PathMappings(Collections.unmodifiableMap(newPrefixesToTargetPaths), Collections.unmodifiableMap(newPrefixesToContextPaths));
}
}
/**
* @return the {@link #entries}
*/
public Map<String, List<String>> getEntries() {
return entries;
}
/**
* Returns a {@link Set} of servlet context paths which registered the given {@code prefix}.
* For testing purposes only, therefore the package visibility.
*
* @return {@link #prefixesToContextPaths}
*/
Map<String, Set<String>> getPrefixesToContextPaths() {
return prefixesToContextPaths;
}
}
/**
* A immutable collection of {@link StaticScriptResource}s.
* <p>
* Immutable because there may happen concurrent invocations of say
* {@link JavascriptConfigService#remove(String)} and {@link JavascriptConfigService#getJSConfig(ControllerContext, Locale)}
*
* @see {@link ScriptResources#staticScriptResources}
* @see {@link JavascriptConfigService#staticScriptResources}
*
* @author <a href="mailto:ppalaga@redhat.com">Peter Palaga</a>
*
*/
static class StaticScriptResources {
private static final StaticScriptResources EMPTY = new StaticScriptResources();
/**
* @return an empty immutable {@link Map}.
*/
public static StaticScriptResources empty() {
return EMPTY;
}
/**
* A collection of {@link StaticScriptResource}s keyed by the given
* {@link StaticScriptResource#getResourcePath()}
*/
private final Map<String, StaticScriptResource> entries;
/**
* Creates a new builder based on the given {@code entries}. The values from {@code paths}
* are copied into a new {@link HashMap}.
*
* @param entries
*/
private StaticScriptResources(Map<String, StaticScriptResource> entries) {
this.entries = entries;
}
private StaticScriptResources() {
this.entries = Collections.emptyMap();
}
/**
* Adds the all elements from {@code toAdd} to {@link #entries}. If a {@code resourcePath}
* of an added entry is available in {@link #entries} as a key,
* a {@link DuplicateResourceKeyException} is thrown.
*
* @param toAdd entries to add
* @return
* @throws DuplicateResourceKeyException if a {@code resourcePath} of an added entry is
* available in {@link #entries} as a key, a {@link DuplicateResourceKeyException} is thrown.
*/
public StaticScriptResources add(Collection<StaticScriptResource> toAdd) throws DuplicateResourceKeyException {
if (toAdd == null || toAdd.isEmpty()) {
return this;
} else {
Map<String, StaticScriptResource> newStaticScriptResources = new HashMap<String, StaticScriptResource>(this.entries);
for (StaticScriptResource staticScriptResource : toAdd) {
String resourcePath = staticScriptResource.getResourcePath();
StaticScriptResource availableValue = entries.get(resourcePath);
if (availableValue != null) {
throw new DuplicateResourceKeyException("Ignoring " + StaticScriptResource.class.getSimpleName() + " " + staticScriptResource
+ " because the given resource path was already provided by " + availableValue);
} else {
/* add only if not there already */
if (log.isDebugEnabled()) {
log.debug("Adding " + staticScriptResource);
}
newStaticScriptResources.put(resourcePath, staticScriptResource);
}
}
return new StaticScriptResources(Collections.unmodifiableMap(newStaticScriptResources));
}
}
/**
* Copies this into a new {@link StaticScriptResources} instance, removes all entries
* with the given {@code contextPath} from {@link #entries} of the new instance and returns
* the new instance. Returns {@code this} if there is nothing to change.
*
* @param contextPath a servlet context path
* @return
*/
public StaticScriptResources remove(String contextPath) {
Map<String, StaticScriptResource> newStaticScriptResources = this.entries;
for (StaticScriptResource staticScriptResource : this.entries.values()) {
if (staticScriptResource.getContextPath().equals(contextPath)) {
if (newStaticScriptResources == this.entries) {
/* we hit the first change, so prepare a mutable object */
newStaticScriptResources = new HashMap<String, StaticScriptResource>(this.entries);
}
newStaticScriptResources.remove(staticScriptResource.getResourcePath());
}
}
if (newStaticScriptResources == this.entries) {
return this;
} else {
return new StaticScriptResources(Collections.unmodifiableMap(newStaticScriptResources));
}
}
/**
* @return the entries
*/
public Map<String, StaticScriptResource> getEntries() {
return entries;
}
}
private class ShutDownListener implements ContainerLifecyclePlugin {
@Override
public void stopContainer(ExoContainer container) throws Exception {
log.debug("Will ignore cleanup on application undeploy from now on because the container is shutting down.");
rootContainerShuttingDown = true;
}
@Override
public void startContainer(ExoContainer container) throws Exception {
}
@Override
public void setName(String s) {
}
@Override
public void setInitParams(InitParams params) {
}
@Override
public void setDescription(String s) {
}
@Override
public void initContainer(ExoContainer container) throws Exception {
}
@Override
public String getName() {
return getClass().getName();
}
@Override
public InitParams getInitParams() {
return null;
}
@Override
public String getDescription() {
return null;
}
@Override
public void destroyContainer(ExoContainer container) throws Exception {
}
}
/** Our logger. */
private static final Log log = ExoLogger.getLogger(JavascriptConfigService.class);
/** The scripts. */
private ScriptGraph scripts;
/**
* require.js path mappings.
*
* @see PathMappings
*/
private PathMappings pathMappings;
/** A collection of {@link StaticScriptResource}s. */
private StaticScriptResources staticScriptResources;
/**
* @see #getSharedBaseUrl(ControllerContext)
*/
private volatile String sharedBaseUrl;
private boolean rootContainerShuttingDown = false;
/** . */
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 Pattern JS_ID_PATTERN = Pattern.compile("^[a-zA-Z_$][0-9a-zA-Z_$]*$");
/** . */
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 = ScriptGraph.empty();
this.pathMappings = PathMappings.empty();
this.staticScriptResources = StaticScriptResources.empty();
RootContainer.getInstance().addContainerLifecylePlugin(new ShutDownListener());
}
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 (resource.isNativeAmd()) {
/* nothing to do for an AMD module */
// buffer.append("/* native AMD module */\n");
} else if (isModule) {
Set<ResourceId> depResourceIds = resource.getDependencies();
int argCount = depResourceIds.size();
JSONArray deps = new JSONArray();
LinkedList<String> params = new LinkedList<String>();
List<String> argNames = new ArrayList<String>(argCount);
List<String> argValues = new ArrayList<String>(argCount);
for (ResourceId id : depResourceIds) {
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 ");
}
final WebApp app = contexts.get(resource.getContextPath());
for (Module js : modules) {
Reader jScript = getJavascript(app, js, locale);
if (jScript != null) {
readers.add(new StringReader(buffer.toString()));
buffer.setLength(0);
readers.add(new NormalizeJSReader(jScript));
}
}
if (resource.isNativeAmd()) {
/* nothing to do for an AMD module */
//buffer.append("\n");
} else 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();
for (Entry<String, List<String>> en : this.pathMappings.getEntries().entrySet()) {
String prefix = en.getKey();
List<String> pathValues = en.getValue();
switch (pathValues.size()) {
case 0:
/* This should never happen as it is forbidden in gatein_resources XSD */
throw new IllegalStateException("Unexpected empty target path list for prefix '"+ prefix +"'.");
case 1:
paths.put(prefix, pathValues.get(0));
break;
default:
paths.put(prefix, pathValues);
break;
}
}
Map<ResourceId, String> groupURLs = new HashMap<ResourceId, String>();
/* use local ScriptGraph variable to stay consistent throughout the loop */
ScriptGraph graph = this.scripts;
for (ResourceScope scope : ResourceScope.values()) {
for (ScriptResource resource : graph.getResources(scope)) {
if (!resource.isNativeAmd() /* exclude native AMD modules to reduce the size
* of the HTTP response as there may be thosands of them
* They are always SHARED so there is no need to put their URLs here
* explicitly. */
&& (!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(id);
}
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();
String sharedBaseUrl = getSharedBaseUrl(controllerContext);
if (sharedBaseUrl != null) {
config.put("baseUrl", sharedBaseUrl);
}
config.put("paths", paths);
config.put("shim", shim);
return config;
}
public ScriptResource getResource(ResourceId resource) {
return scripts.getResource(resource);
}
private Reader getJavascript(WebApp webApp, Module module, Locale locale) {
if (module instanceof Module.Local) {
Module.Local localModule = (Module.Local) module;
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 String encode(LinkedList<String> params, String alias) {
alias = alias.replace("/", "_");
Matcher validMatcher = JS_ID_PATTERN.matcher(alias);
if (!validMatcher.matches()) {
log.error("alias {} is not valid, changing to default 'alias' name", alias);
alias = "alias";
}
//
int idx = -1;
Iterator<String> iterator = params.descendingIterator();
while (iterator.hasNext()) {
String param = iterator.next();
Matcher matcher = INDEX_PATTERN.matcher(param);
if ( matcher.matches()) {
if (param.replace(matcher.group(1), "").equals(alias)) {
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);
String a = tmp.toString();
log.warn("alias {} is duplicated, adding index: {}", alias, a);
return a;
} 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 (!Character.isWhitespace(c) && !Character.isSpaceChar(c) && !Character.isISOControl(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();
}
}
/**
* Returns a value equivalent to
* <pre>"/"+ defaultPortalContext
* + "/"+ ResourceRequestHandler.SCRIPT_HANDLER_NAME
* + "/"+ ResourceRequestHandler.VERSION
* + "/"+ ResourceScope.SHARED.name()</pre>
*
* This value is used as {@code baseUrl} in the configuration of requireJS javascript
* loader on the client side.
*
* Rather than concatenating the above values this method uses
* {@link BaseScriptResource#createBaseParameters(ResourceScope, String)} and delegates to
* {@link ControllerContext#renderURL(Map, URIWriter)} which seems to be safer for
* any future changes.
*
* The value computed once is stored in {@link JavascriptConfigService#sharedBaseUrl} and
* re-used upon subsequent calls.
*
* @param controllerContext
* @return
* @throws Exception
*/
private String getSharedBaseUrl(ControllerContext controllerContext) throws Exception {
if (this.sharedBaseUrl == null) {
/* Let's accept some harmless race conditions here rather than syncing explicitly.
* It does not matter if this.sharedBaseUrl gets initialized several times
* concurrently as the result will be the same every time.*/
Map<QualifiedName, String> baseParams = BaseScriptResource.createBaseParameters(ResourceScope.SHARED, "fake");
/* 52 is the length of /portal/scripts/3.8.0.Beta01-SNAPSHOT/SHARED/fake.js
* it should be a little bit more than necessary in most cases */
StringBuilder buffer = new StringBuilder(52);
URIWriter writer = new URIWriter(buffer);
controllerContext.renderURL(baseParams, writer);
if (buffer.length() < 2) {
throw new IllegalStateException("sharedBaseUrl too short: '"+ buffer.toString() +"'");
}
/* There is no StringBuilder.lastIndexOf(char) let's loop manually */
int lastSlash = -1;
for (int i = buffer.length() -1; i >= 0; i--) {
if (buffer.charAt(i) == '/') {
lastSlash = i;
break;
}
}
if (lastSlash < 0) {
throw new IllegalStateException("No slash in '"+ buffer.toString() +"'");
}
this.sharedBaseUrl = buffer.substring(0, lastSlash);
}
return this.sharedBaseUrl;
}
/**
* Equivalent to {@code staticScriptResources.getEntries().get(resourcePath)}.
* See {@link StaticScriptResources#entries} and {@link StaticScriptResource#getResourcePath()}
*
* @param resourcePath see {@link StaticScriptResource#getResourcePath()}
* @return
*/
public StaticScriptResource getStaticScriptResource(String resourcePath) {
return staticScriptResources.getEntries().get(resourcePath);
}
/**
* Adds the entities provided in the given {@link ScriptResources} to this
* {@link JavascriptConfigService}.
*
* {@link InvalidResourceException} is thrown if the addition of the given {@code scriptResources}
* would break the internal consistency of this {@link JavascriptConfigService}. See the
* documentation of the following methods to see which specific conditions are illegal:
* {@link StaticScriptResources#add(Collection)}, {@link PathMappings#add(String, Map)}
* and {@link ScriptGraph#add(String, List)}.
*
* In case this method throws an {@link InvalidResourceException}, the the internal state of
* this {@link JavascriptConfigService} stays as it was before the invocation of this method.
*
* @param scriptResources entities to add to this {@link JavascriptConfigService}
* @throws InvalidResourceException see above.
*/
public void add(ScriptResources scriptResources) throws InvalidResourceException {
/* Combine the present resources with the ones being added */
StaticScriptResources newStaticScriptResources = this.staticScriptResources.add(scriptResources.getStaticScriptResources());
/* Combine the present paths with the ones being added */
PathMappings newPaths = this.pathMappings.add(scriptResources.getContextPath(), scriptResources.getPaths());
/* We might get an exception here, if there are resources in getScriptResourceDescriptors
* which were registered already in the past by other apps. If that is the case, the
* deployment will be interrupted, and the state of this is the same as before the call */
this.scripts = this.scripts.add(scriptResources.getContextPath(), scriptResources.getScriptResourceDescriptors());
/* No exception was thrown, now we can at once assign these two local variables to the fields of this service */
this.staticScriptResources = newStaticScriptResources;
this.pathMappings = newPaths;
}
/**
* Removes the entities provided in the given {@link ScriptResources} from this
* {@link JavascriptConfigService}.
*
* @param contextPath for which which context path the script resources should be removed
*/
public void remove(String contextPath) {
final boolean debug = log.isDebugEnabled();
if (this.rootContainerShuttingDown) {
if (debug) {
log.debug("Going without script cleanup for context '"+ contextPath +"' because the container is shutting down.");
}
} else {
if (debug) {
log.debug("Removing scripts coming from context '"+ contextPath +"'.");
}
this.scripts = this.scripts.remove(contextPath);
this.staticScriptResources = this.staticScriptResources.remove(contextPath);
this.pathMappings = this.pathMappings.remove(contextPath);
}
}
}