/* * Copyright (C) 2011 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.gatein.portal.controller.resource.script; import java.util.ArrayList; import java.util.Collections; import java.util.HashSet; import java.util.Iterator; import java.util.LinkedHashMap; import java.util.LinkedHashSet; 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 org.exoplatform.portal.resource.InvalidResourceException; import org.exoplatform.services.log.ExoLogger; import org.exoplatform.services.log.Log; import org.exoplatform.web.application.javascript.JavascriptConfigService; import org.exoplatform.web.controller.QualifiedName; import org.gatein.portal.controller.resource.ResourceId; import org.gatein.portal.controller.resource.script.Module.Local.Content; import org.gatein.portal.controller.resource.script.ScriptGraph.ScriptGraphBuilder; import org.gatein.portal.controller.resource.script.ScriptGroup.ScriptGroupBuilder; /** * This class implements the {@link Comparable} interface, however the natural ordering provided here is not consistent with * equals, therefore this class should not be used as a key in a {@link java.util.TreeMap} for instance. * * @author <a href="mailto:julien.viet@exoplatform.com">Julien Viet</a> * @author <a href="mailto:ppalaga@redhat.com">Peter Palaga</a> */ public class ScriptResource extends BaseScriptResource<ScriptResource> implements Comparable<ScriptResource> { static class ScriptResourceBuilder extends BaseScriptResourceBuilder { private List<Module> modules; private Map<ResourceId, Set<DepInfo>> dependencies; private Set<ResourceId> closure; private FetchMode fetchMode; private String alias; private final ScriptGroupBuilder group; private boolean nativeAmd; ScriptResourceBuilder(ScriptGraphBuilder scriptGraphBuilder, ResourceId id, String contextPath, FetchMode fetchMode, String alias, ScriptGroupBuilder group, boolean nativeAmd) { super(scriptGraphBuilder, id, contextPath); this.fetchMode = fetchMode; this.alias = alias; this.group = group; this.nativeAmd = nativeAmd; this.modules = new ArrayList<Module>(); this.closure = new HashSet<ResourceId>(); this.dependencies = new LinkedHashMap<ResourceId, Set<DepInfo>>(); } ScriptResourceBuilder(ScriptGraphBuilder scriptGraphBuilder, ResourceId id, String contextPath, FetchMode fetchMode, String alias, ScriptGroupBuilder group, boolean nativeAmd, List<Module> modules, Set<ResourceId> closure, Map<ResourceId, Set<DepInfo>> dependencies) { super(scriptGraphBuilder, id, contextPath); this.fetchMode = fetchMode; this.alias = alias; this.group = group; this.nativeAmd = nativeAmd; this.modules = new ArrayList<Module>(modules); this.closure = new HashSet<ResourceId>(closure); this.dependencies = new LinkedHashMap<ResourceId, Set<DepInfo>>(dependencies); } void dependencyAdded(ResourceId id, Set<ResourceId> closure) { if (this.closure == null) { this.closure = buildClosure(); } if (this.closure.contains(id)) { this.closure.addAll(closure); } } void addDependency(String contextPath, ResourceId dependencyId, String alias, String pluginRS) throws InvalidResourceException { if (dependencyId.equals(this.id)) { log.warn("Ignoring self-dependency declared for resource '"+ this.id +"'. To avoid this warning, remove the self-dependency declaration for '"+ this.id +"' declaration from gatein-resources.xml in context '"+ contextPath +"'"); return; } ScriptResourceBuilder dependency = scriptGraphBuilder.getResource(dependencyId); if (this.closure == null) { this.closure = buildClosure(); } if (dependency != null) { dependency.checkDependentFetchMode(id, fetchMode); if (dependency.closure.contains(id)) { /* cycle detected */ throw new InvalidResourceException("Adding script dependency "+ dependency.id +" to "+ id +" would introduce a dependency circle"); } } // That is important to make closure independent from building order of graph nodes. if (dependency != null) { closure.addAll(dependency.closure); } // Update the source's closure closure.add(dependencyId); // Update any entry that points to the source scriptGraphBuilder.dependencyAdded(id, closure); // Set<DepInfo> infos = dependencies.get(dependencyId); if (infos == null) { dependencies.put(dependencyId, infos = new LinkedHashSet<DepInfo>()); } infos.add(new DepInfo(alias, pluginRS)); } /** * @return */ private Set<ResourceId> buildClosure() { return buildClosure(new HashSet<ResourceId>(dependencies.size() * 2)); } private Set<ResourceId> buildClosure(Set<ResourceId> result) { for (ResourceId dependencyId : dependencies.keySet()) { ScriptResourceBuilder dependencyBuilder = scriptGraphBuilder.getResource(dependencyId); if (dependencyBuilder != null) { result.add(dependencyId); dependencyBuilder.buildClosure(result); } } return result; } void addLocalModule(String contextPath, Content[] contents, String resourceBundle, int priority) { Module.Local module = new Module.Local(this.id, contextPath, contents, resourceBundle, priority); modules.add(module); } void addRemoteModule(String path, int priority) { Module.Remote module = new Module.Remote(path, priority); modules.add(module); } @Override void addSupportedLocale(Locale locale) { super.addSupportedLocale(locale); if (group != null) { group.addSupportedLocale(locale); } } ScriptResource build() { if (alias == null) { String resName = id.getName(); alias = resName.substring(resName.lastIndexOf("/") + 1); } if (FetchMode.ON_LOAD.equals(fetchMode)) { Matcher validMatcher = JavascriptConfigService.JS_ID_PATTERN.matcher(alias); if (!validMatcher.matches()) { log.warn("alias {} is not valid JS identifier", alias); } } if (this.closure == null) { this.closure = buildClosure(); } /* sync the isFullId field in dependencies and closure */ for (ResourceId closureId : this.closure) { ScriptResourceBuilder resource = scriptGraphBuilder.getResource(closureId); if (resource != null) { closureId.setFullId(resource.getId().isFullId()); } } for (ResourceId dependencyId : this.dependencies.keySet()) { ScriptResourceBuilder resource = scriptGraphBuilder.getResource(dependencyId); if (resource != null) { dependencyId.setFullId(resource.getId().isFullId()); } } ScriptGroup scriptGroup = group != null ? group.build() : null; return new ScriptResource(id, parameters, parametersMap, minParameters, minParametersMap, contextPath, fetchMode, alias, scriptGroup, nativeAmd, modules, dependencies, closure); } void checkDependencyFetchMode(ResourceId id, FetchMode expectedFetchMode) throws InvalidResourceException { if (this.dependencies.containsKey(id) && !this.fetchMode.equals(expectedFetchMode)) { throw new InvalidResourceException("ScriptResource " + this.id + " with fetchMode '"+ this.fetchMode +"' cannot depend on '" + id + "' with fetchMode '"+ fetchMode +"'. The fetchModes must be equal."); } } void checkDependentFetchMode(ResourceId id, FetchMode expectedFetchMode) throws InvalidResourceException { if (!expectedFetchMode.equals(this.fetchMode)) { throw new InvalidResourceException("ScriptResource " + id + " with fetchMode '"+ expectedFetchMode +"' cannot depend on '" + this.id + "' with fetchMode '"+ this.fetchMode +"'. The fetchModes must be equal."); } } boolean isEmpty() { return modules.isEmpty(); } void resourcesRemoved(String removedContextPath, Set<ResourceId> removedIds) { if (!this.contextPath.equals(removedContextPath)) { /* no need to warn or do anything with closure if we remove within the servlet * context that is being removed */ if (log.isWarnEnabled()) { for (Iterator<ResourceId> it = dependencies.keySet().iterator(); it.hasNext();) { ResourceId dependencyId = it.next(); if (removedIds.contains(dependencyId)) { /* warn about a stale dependency */ log.warn("Undeploying context '"+ removedContextPath +"' although resource '"+ this.id +"' from context '"+ this.contextPath +"' still depends on resource '"+ dependencyId +"' from '"+ removedContextPath +"'. From now on, any application relying on '"+ this.id +"' will not work properly. You'll have to either deploy '"+ removedContextPath +"' back or remove '"+ dependencyId +"' from the dependencies of '"+ this.id +"'."); /* ppalaga considered to put it.remove() here for the sake of the script graph's * internal consistency, but it would lead to a state that is both broken (resource * missing a dependency) and, in some cases, impossible to fix without a re-start * of the portal. * * When we do not remove the the stale ResourceId from this.dependencies here, * there is a good chance that that the state is easily fixable by re-deploying * of the application that is being undeployd now. * * Note that although we do not clean the undeployed resources from dependencies, * we always clean them from closure.*/ } } } if (closure != null) { for (ResourceId resourceId : removedIds) { if (this.closure.contains(resourceId)) { /* assign null to closure to mark that it is messy */ this.closure = null; break; } } } } } /** * @return * @throws InvalidResourceException */ void validateDependencies() throws InvalidResourceException { for (ResourceId depId : dependencies.keySet()) { if (scriptGraphBuilder.getResource(depId) == null && !JavascriptConfigService.RESERVED_MODULE.contains(depId.getName())) { throw new InvalidResourceException("Inconsistent ScriptGraph: ScriptResource '"+ this.id +"' depends on a non-existent resource '"+ depId +"'."); } } } } private static final Log log = ExoLogger.getLogger(ScriptResource.class); /** . */ private final List<Module> modules; /** . */ private final Map<ResourceId, Set<DepInfo>> dependencies; /** . */ private final Set<ResourceId> closure; /** . */ private final FetchMode fetchMode; /** . */ private final String alias; /** . */ private final ScriptGroup group; private final boolean nativeAmd; private ScriptResource(ResourceId id, Map<QualifiedName, String> parameters, Map<Locale, Map<QualifiedName, String>> parametersMap, Map<QualifiedName, String> minParameters, Map<Locale, Map<QualifiedName, String>> minParametersMap, String contextPath, FetchMode fetchMode, String alias, ScriptGroup group, boolean nativeAmd, List<Module> modules, Map<ResourceId, Set<DepInfo>> dependencies, Set<ResourceId> closure) { super(id, contextPath, parameters, parametersMap, minParameters, minParametersMap); this.modules = Collections.unmodifiableList(new ArrayList<Module>(modules)); LinkedHashMap<ResourceId, Set<DepInfo>> depsCopy = new LinkedHashMap<ResourceId, Set<DepInfo>>(dependencies); for (Entry<ResourceId, Set<DepInfo>> en : depsCopy.entrySet()) { en.setValue(Collections.unmodifiableSet(new LinkedHashSet<ScriptResource.DepInfo>(en.getValue()))); } this.dependencies = Collections.unmodifiableMap(depsCopy); this.closure = Collections.unmodifiableSet(new HashSet<ResourceId>(closure)); this.fetchMode = fetchMode; this.alias = alias; this.group = group; this.nativeAmd = nativeAmd; } public boolean isEmpty() { return modules.isEmpty(); } public String getContextPath() { return contextPath; } public FetchMode getFetchMode() { return fetchMode; } public Set<ResourceId> getClosure() { return closure; } public List<Module> getModules() { return modules; } public int compareTo(ScriptResource o) { if (closure.contains(o.id)) { return 1; } else if (o.closure.contains(id)) { return -1; } else { return 0; } } @Override public Set<ResourceId> getDependencies() { return dependencies.keySet(); } public Set<DepInfo> getDepInfo(ResourceId id) { return dependencies.get(id); } /** * If no alias was set, return the last part of the resource name If resourceID is null, return null */ public String getAlias() { return alias; } @Override public String toString() { return "ScriptResource[id=" + id + "]"; } public ScriptGroup getGroup() { return group; } /** * Returns {@code true} if this is an AMD resource. See the invocations of {@link #isNativeAmd()} in * {@link JavascriptConfigService#getScript(ResourceId, Locale)} to learn more about the purpose * of this method. */ public boolean isNativeAmd() { return nativeAmd; } public static class DepInfo { private final String alias; private final String pluginRS; DepInfo(String alias, String pluginRS) { this.alias = alias; this.pluginRS = pluginRS; } public String getAlias() { return alias; } public String getPluginRS() { return pluginRS; } @Override public int hashCode() { final int prime = 31; int result = 1; result = prime * result + ((alias == null) ? 0 : alias.hashCode()); result = prime * result + ((pluginRS == null) ? 0 : pluginRS.hashCode()); return result; } @Override public boolean equals(Object obj) { if (this == obj) return true; if (obj == null || !(obj instanceof DepInfo)) return false; DepInfo other = (DepInfo) obj; return ((alias == other.alias || alias != null && alias.equals(other.alias)) && (pluginRS == other.pluginRS || pluginRS != null && pluginRS.equals(other.pluginRS))); } } /** * @param scriptGraphBuilder * @return */ ScriptResourceBuilder newBuilder(ScriptGraphBuilder scriptGraphBuilder, ScriptGroupBuilder groupBuilder) { return new ScriptResourceBuilder(scriptGraphBuilder, id, contextPath, fetchMode, alias, groupBuilder, nativeAmd, modules, closure, dependencies); } }