org.jahia.services.render.scripting.bundle.BundleScriptResolver.java Source code

Java tutorial

Introduction

Here is the source code for org.jahia.services.render.scripting.bundle.BundleScriptResolver.java

Source

/**
 * ==========================================================================================
 * =                   JAHIA'S DUAL LICENSING - IMPORTANT INFORMATION                       =
 * ==========================================================================================
 *
 *                                 http://www.jahia.com
 *
 *     Copyright (C) 2002-2017 Jahia Solutions Group SA. All rights reserved.
 *
 *     THIS FILE IS AVAILABLE UNDER TWO DIFFERENT LICENSES:
 *     1/GPL OR 2/JSEL
 *
 *     1/ GPL
 *     ==================================================================================
 *
 *     IF YOU DECIDE TO CHOOSE THE GPL LICENSE, YOU MUST COMPLY WITH THE FOLLOWING TERMS:
 *
 *     This program is free software: you can redistribute it and/or modify
 *     it under the terms of the GNU General Public License as published by
 *     the Free Software Foundation, either version 3 of the License, or
 *     (at your option) any later version.
 *
 *     This program 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 General Public License for more details.
 *
 *     You should have received a copy of the GNU General Public License
 *     along with this program. If not, see <http://www.gnu.org/licenses/>.
 *
 *
 *     2/ JSEL - Commercial and Supported Versions of the program
 *     ===================================================================================
 *
 *     IF YOU DECIDE TO CHOOSE THE JSEL LICENSE, YOU MUST COMPLY WITH THE FOLLOWING TERMS:
 *
 *     Alternatively, commercial and supported versions of the program - also known as
 *     Enterprise Distributions - must be used in accordance with the terms and conditions
 *     contained in a separate written agreement between you and Jahia Solutions Group SA.
 *
 *     If you are unsure which license is appropriate for your use,
 *     please contact the sales department at sales@jahia.com.
 */
package org.jahia.services.render.scripting.bundle;

import org.apache.commons.lang.StringUtils;
import org.jahia.data.templates.JahiaTemplatesPackage;
import org.jahia.osgi.ExtensionObserverRegistry;
import org.jahia.registries.ServicesRegistry;
import org.jahia.services.channels.Channel;
import org.jahia.services.channels.ChannelService;
import org.jahia.services.content.JCRContentUtils;
import org.jahia.services.content.decorator.JCRSiteNode;
import org.jahia.services.content.nodetypes.ExtendedNodeType;
import org.jahia.services.content.nodetypes.NodeTypeRegistry;
import org.jahia.services.render.*;
import org.jahia.services.render.scripting.Script;
import org.jahia.services.render.scripting.ScriptFactory;
import org.jahia.services.render.scripting.ScriptResolver;
import org.jahia.services.templates.JahiaTemplateManagerService;
import org.jahia.services.templates.JahiaTemplateManagerService.ModuleDependenciesEvent;
import org.jahia.services.templates.JahiaTemplateManagerService.ModuleDeployedOnSiteEvent;
import org.jahia.services.templates.JahiaTemplateManagerService.TemplatePackageRedeployedEvent;
import org.jahia.utils.ScriptEngineUtils;
import org.osgi.framework.Bundle;
import org.osgi.framework.BundleContext;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.context.ApplicationEvent;
import org.springframework.context.ApplicationListener;

import javax.jcr.RepositoryException;
import javax.jcr.nodetype.NoSuchNodeTypeException;
import javax.script.ScriptEngineFactory;
import java.io.IOException;
import java.net.URL;
import java.util.*;
import java.util.concurrent.ConcurrentHashMap;

public class BundleScriptResolver implements ScriptResolver, ApplicationListener<ApplicationEvent> {

    private static final int PRIORITY_STAGGER_FACTOR = 100;
    private static final String EXTENSION_PATTERN_PREFIX = "*.";
    private static Logger logger = LoggerFactory.getLogger(BundleScriptResolver.class);

    private static Map<String, SortedSet<View>> viewSetCache = new ConcurrentHashMap<>(512);

    private Map<String, SortedMap<String, ViewResourceInfo>> availableScripts = new HashMap<>(64);

    private LinkedHashMap<String, ScriptFactory> scriptFactoryMap;
    private Set<String> preRegisteredExtensions;

    private HashMap<String, Integer> extensionPriorities;
    private JahiaTemplateManagerService templateManagerService;
    private final Comparator<ViewResourceInfo> scriptExtensionComparator = new Comparator<ViewResourceInfo>() {
        public int compare(ViewResourceInfo o1, ViewResourceInfo o2) {
            if (Objects.equals(o1, o2)) {
                return 1;
            }

            final Integer o1Priority = extensionPriorities.get(o1.extension);
            final Integer o2Priority = extensionPriorities.get(o2.extension);
            if (o1Priority == null) {
                throw new IllegalArgumentException("Unknown extension " + o1.extension);
            }
            if (o2Priority == null) {
                throw new IllegalArgumentException("Unknown extension " + o2.extension);
            }
            final int i = o1Priority - o2Priority;
            return i != 0 ? i : 1; // not sure why returning 1 is needed when i == 0 here but not doing so seems to break :(
        }
    };
    private BundleJSR223ScriptFactory bundleScriptFactory;
    private ExtensionObserverRegistry observerRegistry;
    private final ScriptBundleObserver scriptBundleObserver = new ScriptBundleObserver(this);
    /**
     * Prefixes for bundles that are excluded from bundle scanning for views since they contain lots of files with
     * registered extensions that would be considered as views.
     */
    private Set<String> ignoredBundlePrefixes = new HashSet<>(7);

    public void setIgnoredBundlePrefixes(Set<String> ignoredBundlePrefixes) {
        this.ignoredBundlePrefixes = ignoredBundlePrefixes;
    }

    public void setExtensionObserverRegistry(ExtensionObserverRegistry extensionObserverRegistry) {
        this.observerRegistry = extensionObserverRegistry;
    }

    public ExtensionObserverRegistry getObserverRegistry() {
        return observerRegistry;
    }

    public void registerObservers() {
        // add scanners for all types of scripts of the views to register them in the BundleScriptResolver
        for (String scriptExtension : scriptFactoryMap.keySet()) {
            registerObserver(scriptExtension);
        }
    }

    private void registerObserver(String extension) {
        observerRegistry.put(new ScriptBundleURLScanner("/", extension, true), scriptBundleObserver);
    }

    private void removeObserver(String extension) {
        observerRegistry.remove(new ScriptBundleURLScanner("/", extension, true));
    }

    public ScriptBundleObserver getBundleObserver() {
        return scriptBundleObserver;
    }

    // Initialization on demand holder idiom: thread-safe singleton initialization
    private static class Holder {
        static final BundleScriptResolver INSTANCE = new BundleScriptResolver();

        private Holder() {
        }
    }

    public static BundleScriptResolver getInstance() {
        return Holder.INSTANCE;
    }

    public void setScriptFactoryMap(Map<String, ScriptFactory> scriptFactoryMap) {
        if (!(scriptFactoryMap instanceof LinkedHashMap)) {
            throw new IllegalArgumentException(
                    "Error instantiating BundleScriptResolver: Spring is supposed to create a SortedMap when using a <map> "
                            + "property but didn't. Was: " + scriptFactoryMap.getClass().getName());
        }
        this.scriptFactoryMap = (LinkedHashMap<String, ScriptFactory>) scriptFactoryMap;

        // record pre-registered extensions
        preRegisteredExtensions = new HashSet<>(scriptFactoryMap.size());
        preRegisteredExtensions.addAll(scriptFactoryMap.keySet());

        // record priorities of extensions, each pre-registered extension is assigned a priority of its registration index times 100
        extensionPriorities = new HashMap<>(scriptFactoryMap.size());
        int i = 0;
        for (String extension : scriptFactoryMap.keySet()) {
            extensionPriorities.put(extension, PRIORITY_STAGGER_FACTOR * i);
            i++;
        }
    }

    /**
     * Registers the specified {@link ScriptEngineFactory} defined in the context of the specified starting
     * {@link Bundle}. This results in registering the associated view extensions along with activating any existing
     * views in active bundles. This registration operation also takes care of script extension re-ordering if
     * necessary since bundles defining ScriptEngineFactories can do so.
     * <p>
     * <p>Additionally, {@link ScriptBundleURLScanner} are also activated to look for any files with the newly
     * registered extensions during future bundle deployments.</p>
     *
     * @param scriptEngineFactory the ScriptEngineFactory instance we want to register
     * @param bundle              the starting bundle defining the given ScriptEngineFactory
     */
    public void register(ScriptEngineFactory scriptEngineFactory, Bundle bundle) {
        final List<String> extensions = scriptEngineFactory.getExtensions();

        if (extensions.isEmpty()) {
            return;
        }

        // determine if the bundle provided any scripting-related information
        final BundleScriptingContext context;
        if (scriptEngineFactory instanceof BundleScriptEngineFactory) {
            BundleScriptEngineFactory engineFactory = (BundleScriptEngineFactory) scriptEngineFactory;
            context = engineFactory.getContext();
        } else {
            context = null;
        }

        for (String extension : extensions) {
            // first check that we don't already have a script factory assigned to that extension
            final ScriptFactory scriptFactory = scriptFactoryMap.get(extension);
            if (scriptFactory != null) {
                ScriptEngineFactory alreadyRegistered = BundleScriptEngineManager.getInstance()
                        .getFactoryForExtension(extension);
                throw new IllegalArgumentException("Extension " + extension
                        + " is already associated with ScriptEngineFactory " + alreadyRegistered);
            }

            scriptFactoryMap.put(extension, bundleScriptFactory);

            // compute or retrieve the extensions priority and record it
            final int priority = getPriority(extension, context);
            extensionPriorities.put(extension, priority);

            logger.info("ScriptEngineFactory {} registered extension {} with priority {}",
                    new Object[] { scriptEngineFactory, extension, priority });

            // now we need to activate the bundle script scanner inside of newly deployed or existing bundles
            // register view script observers
            addBundleScripts(bundle, extension);

            // check existing bundles to see if they provide views for the newly deployed scripting language
            final BundleContext bundleContext = bundle.getBundleContext();
            if (bundleContext != null) {
                for (Bundle otherBundle : bundleContext.getBundles()) {
                    if (otherBundle.getState() == Bundle.ACTIVE) {
                        addBundleScripts(otherBundle, extension);
                    }
                }
            }

            // register extension observer
            registerObserver(extension);
        }

        // deal with extension priorities if needed
        if (context != null && context.specifiesExtensionPriorities()) {
            final Map<String, Integer> specifiedPriorities = context.getExtensionPriorities();
            final SortedMap<Integer, String> orderedPriorities = new TreeMap<>();

            for (Map.Entry<String, Integer> entry : extensionPriorities.entrySet()) {
                final String extension = entry.getKey();
                Integer priority = entry.getValue();

                final Integer newPriority = specifiedPriorities.get(extension);
                if (newPriority != null) {
                    extensionPriorities.put(extension, newPriority);
                    priority = newPriority;
                }
                orderedPriorities.put(priority, extension);
            }

            //check if we specified unknown extensions
            final Set<String> specifiedExtensions = specifiedPriorities.keySet();
            specifiedExtensions.removeAll(extensionPriorities.keySet());
            if (!specifiedExtensions.isEmpty()) {
                logger.warn("Module {} specified priorities for unknown extensions {}", bundle.getSymbolicName(),
                        specifiedExtensions);
            }

            logger.info("Extension priorities got re-ordered by module {} to {}", bundle.getSymbolicName(),
                    orderedPriorities);
        }
    }

    private int getPriority(String extension, BundleScriptingContext context) {
        final int defaultPriority = extensionPriorities.size() * PRIORITY_STAGGER_FACTOR;
        if (context == null) {
            return defaultPriority;
        } else {
            return context.getPriority(extension, defaultPriority);
        }
    }

    /**
     * Removes the specified {@link ScriptEngineFactory} from the known ones, deactivating any associated views from
     * currently active bundles. This is called when a bundle declaring ScriptEngineFactories is stopping. Previously
     * registered extension observers associated with the ScriptEngineFactory's supported extensions are also removed.
     *
     * @param factory the factory to be removed
     * @param bundle  the bundle which declared the factory and which is being stopped
     */
    public void remove(ScriptEngineFactory factory, Bundle bundle) {
        List<String> extensions = factory.getExtensions();
        for (String extension : extensions) {
            // we need to remove the views associated with our bundle
            availableScripts.remove(bundle.getSymbolicName());

            // remove all the bundle scripts for all the deployed bundles.
            for (Bundle otherBundle : bundle.getBundleContext().getBundles()) {
                if (otherBundle.getState() == Bundle.ACTIVE) {
                    removeBundleScripts(otherBundle, extension);
                }
            }

            scriptFactoryMap.remove(extension);
            extensionPriorities.remove(extension);

            // remove associated observer
            removeObserver(extension);
        }
    }

    /**
     * Whether or not to scan the specified bundle for views with the specified extension.
     *
     * @param bundle        the bundle to possibly scan
     * @param viewExtension the extension for views we're looking for in bundles
     * @return {@code true} if the specified bundle should be scanned for views with the specified extension, {@code
     * false} otherwise
     */
    static boolean shouldBeScannedForViews(Bundle bundle, String viewExtension) {
        if (isIgnoredBundle(bundle)) {
            return false;
        } else if (isPreRegisteredExtension(viewExtension)) {
            // if the extension is one of the pre-registered ones (via Spring configuration), we should scan the bundle
            return true;
        } else {
            final ScriptEngineFactory scriptFactory = BundleScriptEngineManager.getInstance()
                    .getFactoryForExtension(viewExtension);
            if (scriptFactory == null) {
                // we don't have a ScriptEngineFactory associated with this extension so no need to scan
                return false;
            } else {
                // check headers for view markers
                final Dictionary<String, String> headers = bundle.getHeaders();
                return ScriptEngineUtils.canFactoryProcessViews(scriptFactory, headers);
            }
        }
    }

    private static boolean isIgnoredBundle(Bundle bundle) {
        final String symbolicName = bundle.getSymbolicName();
        for (String ignoredBundlePrefix : getInstance().ignoredBundlePrefixes) {
            if (symbolicName.startsWith(ignoredBundlePrefix)) {
                return true;
            }
        }

        return false;
    }

    private static boolean isPreRegisteredExtension(String viewExtension) {
        return getInstance().preRegisteredExtensions.contains(viewExtension);
    }

    private void addBundleScripts(Bundle bundle, String extension) {
        // only add views if we need to
        if (shouldBeScannedForViews(bundle, extension)) {
            final String extensionPattern = getExtensionPattern(extension);
            final Enumeration<URL> entries = bundle.findEntries("/", extensionPattern, true);
            if (entries != null) {
                final List<URL> scripts = new LinkedList<>();
                while (entries.hasMoreElements()) {
                    scripts.add(entries.nextElement());
                }
                addBundleScripts(bundle, scripts);
            }
        }
    }

    static String getExtensionPattern(String extension) {
        return EXTENSION_PATTERN_PREFIX + extension;
    }

    private void removeBundleScripts(Bundle bundle, String extension) {
        if (shouldBeScannedForViews(bundle, extension)) {
            final String extensionPattern = getExtensionPattern(extension);
            final Enumeration<URL> entries = bundle.findEntries("/", extensionPattern, true);
            if (entries != null) {
                final List<URL> scripts = new LinkedList<>();
                while (entries.hasMoreElements()) {
                    scripts.add(entries.nextElement());
                }
                removeBundleScripts(bundle, scripts);
            }
        }
    }

    /**
     * Callback for registering new resource views for a bundle.
     *
     * @param bundle  the bundle to register views for
     * @param scripts the URLs of the views to register
     */
    public void addBundleScripts(Bundle bundle, List<URL> scripts) {
        // TODO consider versions of modules/bundles
        if (!scripts.isEmpty()) {
            for (URL script : scripts) {
                addBundleScript(bundle, script.getPath());
            }
            logger.info("Bundle {} registered {} views", bundle,
                    logger.isDebugEnabled() ? scripts : scripts.size());
        }
    }

    /**
     * Method for registering a new resource view for a bundle.
     *
     * @param bundle the bundle to register views for
     * @param path   the path of the view to register
     */
    public void addBundleScript(Bundle bundle, String path) {
        if (path.split("/").length != 4) {
            return;
        }
        ViewResourceInfo scriptResource = new ViewResourceInfo(path);
        final String symbolicName = bundle.getSymbolicName();
        SortedMap<String, ViewResourceInfo> existingBundleScripts = availableScripts.get(symbolicName);
        if (existingBundleScripts == null) {
            existingBundleScripts = new TreeMap<>();
            availableScripts.put(symbolicName, existingBundleScripts);
            existingBundleScripts.put(scriptResource.path, scriptResource);
        } else if (!existingBundleScripts.containsKey(scriptResource.path)) {
            existingBundleScripts.put(scriptResource.path, scriptResource);
        } else {
            // if we already have a script resource available, retrieve it to make sure we update it with new properties
            // this is required because it is possible that the properties file is not found when the view is first processed due to
            // file ordering processing in ModulesDataSource.start.process method.
            scriptResource = existingBundleScripts.get(scriptResource.path);
        }

        String properties = StringUtils.substringBeforeLast(path, ".") + ".properties";
        final URL propertiesResource = bundle.getResource(properties);
        if (propertiesResource != null) {
            Properties p = new Properties();
            try {
                p.load(propertiesResource.openStream());
            } catch (IOException e) {
                logger.error("Cannot read properties", e);
            }
            scriptResource.setProperties(p);
        } else {
            scriptResource.setProperties(new Properties());
        }
        clearCaches();
    }

    /**
     * Callback for unregistering resource views for a bundle.
     *
     * @param bundle  the bundle to unregister views for
     * @param scripts the URLs of the views to unregister
     */
    public void removeBundleScripts(Bundle bundle, List<URL> scripts) {
        final String bundleName = bundle.getSymbolicName();
        final SortedMap<String, ViewResourceInfo> existingBundleScripts = availableScripts.get(bundleName);
        if (existingBundleScripts == null) {
            return;
        }
        if (!scripts.isEmpty()) {
            boolean didRemove = false;
            for (URL script : scripts) {
                didRemove = existingBundleScripts.remove(script.getPath()) != null;
            }

            if (didRemove) {
                // remove entry if we don't have any scripts anymore for this bundle
                if (existingBundleScripts.isEmpty()) {
                    availableScripts.remove(bundleName);
                }

                logger.info("Bundle {} unregistered {} views", bundle, scripts);
                clearCaches();
            }
        }
    }

    /**
     * Method for unregistering a resource view for a bundle.
     *
     * @param bundle the bundle to unregister views for
     * @param path   the path of the view to unregister
     */
    public void removeBundleScript(Bundle bundle, String path) {
        final SortedMap<String, ViewResourceInfo> existingBundleScripts = availableScripts
                .get(bundle.getSymbolicName());
        if (existingBundleScripts == null) {
            return;
        }
        existingBundleScripts.remove(path);
        clearCaches();
    }

    @Override
    public Script resolveScript(Resource resource, RenderContext renderContext) throws TemplateNotFoundException {
        try {
            View resolvedView = resolveView(resource, renderContext);
            if (resolvedView == null) {
                throw new TemplateNotFoundException("Unable to find the view for resource " + resource);
            }

            if (scriptFactoryMap.containsKey(resolvedView.getFileExtension())) {
                return scriptFactoryMap.get(resolvedView.getFileExtension()).createScript(resolvedView);
            }
            throw new TemplateNotFoundException(
                    "Unable to script factory map extension handler for the resolved view "
                            + resolvedView.getInfo());
        } catch (RepositoryException e) {
            throw new TemplateNotFoundException(e);
        }
    }

    private View resolveView(Resource resource, RenderContext renderContext) throws RepositoryException {
        ExtendedNodeType nt = resource.getNode().getPrimaryNodeType();
        List<ExtendedNodeType> nodeTypeList = getNodeTypeList(nt);
        for (ExtendedNodeType type : resource.getNode().getMixinNodeTypes()) {
            nodeTypeList.addAll(0, type.getSupertypeSet());
            nodeTypeList.add(0, type);
        }

        if (resource.getResourceNodeType() != null) {
            nodeTypeList.addAll(0, getNodeTypeList(resource.getResourceNodeType()));
        }

        return resolveView(resource, nodeTypeList, renderContext);
    }

    private View resolveView(Resource resource, List<ExtendedNodeType> nodeTypeList, RenderContext renderContext) {
        String template = resource.getResolvedTemplate();
        JCRSiteNode site = renderContext.getSite();

        List<String> templateTypeMappings = null;
        Channel channel = renderContext.getChannel();
        if (channel != null && !channel.getFallBack().equals("root")) {
            templateTypeMappings = new LinkedList<String>();
            while (!channel.getFallBack().equals("root")) {
                if (channel.getCapability("template-type-mapping") != null) {
                    templateTypeMappings
                            .add(resource.getTemplateType() + "-" + channel.getCapability("template-type-mapping"));
                }
                channel = ChannelService.getInstance().getChannel(channel.getFallBack());
            }
            templateTypeMappings.add(resource.getTemplateType());
        }
        Set<View> s = getViewsSet(nodeTypeList, site, templateTypeMappings != null ? templateTypeMappings
                : Collections.singletonList(resource.getTemplateType()));
        View selected;
        selected = getView(template, s);
        if (selected == null && !"default".equals(template)) {
            selected = getView("default", s);
        }
        return selected;
    }

    private View getView(String template, Set<View> s) {
        for (View view : s) {
            if (view.getKey().equals(template)) {
                return view;
            }
        }
        return null;
    }

    @Override
    public boolean hasView(ExtendedNodeType nt, String key, JCRSiteNode site, String templateType) {
        for (View view : getViewsSet(nt, site, templateType)) {
            if (view.getKey().equals(key)) {
                return true;
            }
        }
        return false;
    }

    @Override
    public SortedSet<View> getViewsSet(ExtendedNodeType nt, JCRSiteNode site, String templateType) {
        try {
            return getViewsSet(getNodeTypeList(nt), site, Collections.singletonList(templateType));
        } catch (NoSuchNodeTypeException e) {
            logger.error(e.getMessage(), e);
        }

        return null;
    }

    /**
     * @param nt
     * @return
     * @throws NoSuchNodeTypeException
     */
    private List<ExtendedNodeType> getNodeTypeList(ExtendedNodeType nt) throws NoSuchNodeTypeException {
        List<ExtendedNodeType> nodeTypeList = new LinkedList<ExtendedNodeType>();
        nodeTypeList.add(nt);
        nodeTypeList.addAll(nt.getSupertypeSet());
        ExtendedNodeType base = NodeTypeRegistry.getInstance().getNodeType("nt:base");
        if (nodeTypeList.remove(base)) {
            nodeTypeList.add(base);
        }
        return nodeTypeList;
    }

    private SortedSet<View> getViewsSet(List<ExtendedNodeType> nodeTypeList, JCRSiteNode site,
            List<String> templateTypes) {

        StringBuilder cacheKey = new StringBuilder();
        for (ExtendedNodeType type : nodeTypeList) {
            cacheKey.append(type.getName()).append("_");
        }
        cacheKey.append("_").append((site != null ? site.getPath() : "")).append("__");
        for (String type : templateTypes) {
            cacheKey.append(type).append("_");
        }
        final String s = cacheKey.toString();

        if (viewSetCache.containsKey(s)) {
            return viewSetCache.get(s);
        } else {
            Map<String, View> views = new HashMap<String, View>();

            Set<String> installedModules = getInstalledModules(site);

            for (ExtendedNodeType type : nodeTypeList) {
                boolean defaultModuleProcessed = false;
                Set<JahiaTemplatesPackage> packages = templateManagerService
                        .getModulesWithViewsForComponent(JCRContentUtils.replaceColon(type.getName()));
                for (JahiaTemplatesPackage aPackage : packages) {
                    String packageName = aPackage.getId();
                    if (installedModules == null || installedModules.contains(packageName)) {
                        if (aPackage.isDefault()) {
                            defaultModuleProcessed = true;
                        }
                        for (String templateType : templateTypes) {
                            getViewsSet(type, views, templateType, aPackage);
                        }
                    }
                }
                if (type.getTemplatePackage() != null && installedModules != null
                        && !installedModules.contains(type.getSystemId())) {
                    for (String templateType : templateTypes) {
                        getViewsSet(type, views, templateType, type.getTemplatePackage());
                    }
                }
                if (!defaultModuleProcessed) {
                    JahiaTemplatesPackage defaultModule = templateManagerService
                            .getTemplatePackageById(JahiaTemplatesPackage.ID_DEFAULT);
                    if (defaultModule != null) {
                        for (String templateType : templateTypes) {
                            getViewsSet(type, views, templateType, defaultModule);
                        }
                    }
                }
            }
            SortedSet<View> t = new TreeSet<View>(views.values());
            viewSetCache.put(s, t);
            return t;
        }
    }

    private Set<String> getInstalledModules(JCRSiteNode site) {
        if (site == null) {
            return null;
        }
        Set<String> installedModules = null;
        String sitePath = site.getPath();
        if (sitePath.startsWith("/sites/")) {
            installedModules = site.getInstalledModulesWithAllDependencies();
        } else if (sitePath.startsWith("/modules/")) {
            JahiaTemplatesPackage aPackage = templateManagerService.getTemplatePackageById(site.getName());
            if (aPackage != null) {
                installedModules = new LinkedHashSet<String>();
                installedModules.add(aPackage.getId());
                for (JahiaTemplatesPackage depend : aPackage.getDependencies()) {
                    if (!installedModules.contains(depend.getId())) {
                        installedModules.add(depend.getId());
                    }
                }
            }
            if (installedModules != null) {
                installedModules.add("templates-system");
                for (JahiaTemplatesPackage depend : templateManagerService
                        .getTemplatePackageById("templates-system").getDependencies()) {
                    if (!installedModules.contains(depend.getId())) {
                        installedModules.add(depend.getId());
                    }
                }
            }
        }

        return installedModules;
    }

    private void getViewsSet(ExtendedNodeType nt, Map<String, View> views, String templateType,
            JahiaTemplatesPackage tplPackage) {
        StringBuilder pathBuilder = new StringBuilder(64);
        pathBuilder.append("/").append(JCRContentUtils.replaceColon(nt.getAlias())).append("/").append(templateType)
                .append("/");

        // append node type name (without namespace prefix) + "."
        pathBuilder
                .append(nt.getName().contains(":") ? StringUtils.substringAfter(nt.getName(), ":") : nt.getName())
                .append(".");

        // find scripts in the module bundle, matching that path prefix
        Set<ViewResourceInfo> sortedScripts = findBundleScripts(tplPackage.getId(), pathBuilder.toString());
        Properties defaultProperties = null;
        if (!sortedScripts.isEmpty()) {
            defaultProperties = new Properties();
            JahiaTemplatesPackage aPackage = nt.getTemplatePackage();
            if (aPackage == null) {
                aPackage = ServicesRegistry.getInstance().getJahiaTemplateManagerService()
                        .getTemplatePackageById(JahiaTemplatesPackage.ID_DEFAULT);
            }
            if (!aPackage.getId().equals(tplPackage.getId())) {
                Set<ViewResourceInfo> defaultScripts = findBundleScripts(aPackage.getId(), pathBuilder.toString());
                for (ViewResourceInfo defaultScript : defaultScripts) {
                    if (defaultScript.viewKey.equals(View.DEFAULT_VIEW_KEY)) {
                        defaultProperties.putAll(defaultScript.getProperties());
                        break;
                    }
                }
            }
            for (ViewResourceInfo defaultScript : sortedScripts) {
                if (defaultScript.viewKey.equals(View.DEFAULT_VIEW_KEY)) {
                    defaultProperties.putAll(defaultScript.getProperties());
                    break;
                }
            }
        }
        for (ViewResourceInfo res : sortedScripts) {
            if (!views.containsKey(res.viewKey)) {
                if (!scriptFactoryMap.containsKey(res.extension)) {
                    logger.error("Script extension " + res.extension + " can not be handled by this system.");
                    break;
                }
                BundleView view = new BundleView(res.path, res.viewKey, tplPackage, res.filename);
                view.setProperties(res.getProperties());
                view.setDefaultProperties(defaultProperties);
                views.put(res.viewKey, view);
                scriptFactoryMap.get(res.extension).initView(view);
            }
        }
    }

    /**
     * Returns view scripts for the specified module bundle which match the specified path.
     *
     * @param module     the module bundle to perform lookup in
     * @param pathPrefix the resource path prefix to match
     * @return a set of matching view scripts ordered by the extension (script type)
     */
    private Set<ViewResourceInfo> findBundleScripts(String module, String pathPrefix) {
        final SortedMap<String, ViewResourceInfo> allBundleScripts = availableScripts.get(module);
        if (allBundleScripts == null || allBundleScripts.isEmpty()) {
            return Collections.emptySet();
        }

        // get all the ViewResourceInfos which path is greater than or equal to the given prefix
        final SortedMap<String, ViewResourceInfo> viewInfosWithPathGTEThanPrefix = allBundleScripts
                .tailMap(pathPrefix);

        // if the tail map is empty, we won't find the path prefix in the available scripts so return an empty set
        if (viewInfosWithPathGTEThanPrefix.isEmpty()) {
            return Collections.emptySet();
        }

        // check if the first key contains the prefix. If not, the prefix will not match any entries so return an empty set
        if (!viewInfosWithPathGTEThanPrefix.firstKey().startsWith(pathPrefix)) {
            return Collections.emptySet();
        } else {
            SortedSet<ViewResourceInfo> sortedScripts = new TreeSet<ViewResourceInfo>(scriptExtensionComparator);
            for (String path : viewInfosWithPathGTEThanPrefix.keySet()) {
                // we should have only few values to look at
                if (path.startsWith(pathPrefix)) {
                    sortedScripts.add(viewInfosWithPathGTEThanPrefix.get(path));
                } else {
                    // as soon as the path doesn't start with the given prefix anymore, we won't have a match in the remaining so return
                    return sortedScripts;
                }
            }
            return sortedScripts;
        }
    }

    public void setTemplateManagerService(JahiaTemplateManagerService templateManagerService) {
        this.templateManagerService = templateManagerService;
    }

    /**
     * {@inheritDoc}
     */
    public void onApplicationEvent(ApplicationEvent event) {
        if (event instanceof TemplatePackageRedeployedEvent || event instanceof ModuleDeployedOnSiteEvent
                || event instanceof ModuleDependenciesEvent) {
            clearCaches();
        }
    }

    /**
     * Utility method to clear all view caches
     */
    public static void clearCaches() {
        viewSetCache.clear();
    }

    public void setBundleScriptFactory(BundleJSR223ScriptFactory bundleScriptFactory) {
        this.bundleScriptFactory = bundleScriptFactory;
    }
}