/* * Copyright (C) 2005-2010 Alfresco Software Limited. * * This file is part of Alfresco * * Alfresco 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 3 of the License, or * (at your option) any later version. * * Alfresco 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 Alfresco. If not, see . */ package org.alfresco.repo.web.scripts; import java.io.IOException; import java.io.Writer; import java.util.ArrayList; import java.util.HashMap; import java.util.List; import java.util.Map; import java.util.ResourceBundle; import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; import org.dom4j.Element; import org.springframework.extensions.config.ConfigImpl; import org.springframework.extensions.config.ConfigSection; import org.springframework.extensions.config.ConfigService; import org.springframework.extensions.config.evaluator.Evaluator; import org.springframework.extensions.config.xml.XMLConfigService; import org.springframework.extensions.config.xml.elementreader.ConfigElementReader; import org.springframework.extensions.surf.extensibility.BasicExtensionModule; import org.springframework.extensions.surf.extensibility.ExtensibilityModel; import org.springframework.extensions.surf.extensibility.HandlesExtensibility; import org.springframework.extensions.surf.extensibility.WebScriptExtensibilityModuleHandler; import org.springframework.extensions.surf.extensibility.impl.ExtensibilityModelImpl; import org.springframework.extensions.surf.extensibility.impl.MarkupDirective; import org.springframework.extensions.webscripts.Authenticator; import org.springframework.extensions.webscripts.ExtendedScriptConfigModel; import org.springframework.extensions.webscripts.ExtendedTemplateConfigModel; import org.springframework.extensions.webscripts.ScriptConfigModel; import org.springframework.extensions.webscripts.TemplateConfigModel; import org.springframework.extensions.webscripts.WebScriptPropertyResourceBundle; import org.springframework.extensions.webscripts.WebScriptRequest; import org.springframework.extensions.webscripts.WebScriptResponse; /** *

A simple extensibility {@link Container} for processing WebScripts. This extends the {@link RepositoryContainer} and * implements the {@link HandlesExtensibility} interface to provide extensibility capabilities.

* * @author David Draper */ public class ExtensibilityContainer extends RepositoryContainer implements HandlesExtensibility { private static final Log logger = LogFactory.getLog(ExtensibilityContainer.class); public boolean isExtensibilitySuppressed() { return false; } /** *

Opens a new {@link ExtensibilityModel}, defers execution to the extended {@link RepositoryContainer} and * then closes the {@link ExtensibilityModel}.

*/ @Override public void executeScript(WebScriptRequest scriptReq, WebScriptResponse scriptRes, Authenticator auth) throws IOException { ExtensibilityModel extModel = this.openExtensibilityModel(); try { super.executeScript(scriptReq, scriptRes, auth); } finally { // It's only necessary to close the model if it's actually been used. Not all WebScripts will make use of the // model. An example of this would be the StreamContent WebScript. It is important not to attempt to close // an unused model since the WebScript executed may have already flushed the response if it has overridden // the default .execute() method. if (this.modelUsed.get()) { try { this.closeExtensibilityModel(extModel, scriptRes.getWriter()); } catch (IOException e) { logger.error("An error occurred getting the Writer when closing an ExtensibilityModel", e); } } } } /** *

This keeps track of whether or not the {@link ExtensibilityModel} for the current thread has been used. The * thread local value will only be set to true if the getCurrentExtensibilityModel method * is called.

*/ private ThreadLocal modelUsed = new ThreadLocal(); /** *

A {@link WebScriptExtensibilityModuleHandler} is required for retrieving information on what * {@link BasicExtensionModule} instances have been configured and the extension files that need * to be processed. This variable should be set thorugh the Spring application context configuration.

*/ private WebScriptExtensibilityModuleHandler extensibilityModuleHandler = null; /** *

Sets the {@link WebScriptExtensibilityModuleHandler} for this {@link Container}.

* @param extensibilityModuleHandler */ public void setExtensibilityModuleHandler(WebScriptExtensibilityModuleHandler extensibilityModuleHandler) { this.extensibilityModuleHandler = extensibilityModuleHandler; } /** *

Maintains a list of all the {@link ExtensibilityModel} instances being used across all the * available threads.

*/ private ThreadLocal extensibilityModel = new ThreadLocal(); /** *

Creates a new {@link ExtensibilityModel} and sets it on the current thread */ public ExtensibilityModel openExtensibilityModel() { if (logger.isDebugEnabled()) { logger.debug("Opening for thread: " + Thread.currentThread().getName()); } this.extendedBundleCache.set(new HashMap()); this.evaluatedModules.set(null); this.fileBeingProcessed.set(null); this.globalConfig.set(null); this.sections.set(null); this.sectionsByArea.set(null); ExtensibilityModel model = new ExtensibilityModelImpl(null, this); this.extensibilityModel.set(model); this.modelUsed.set(Boolean.FALSE); return model; } /** *

Flushes the {@link ExtensibilityModel} provided and sets its parent as the current {@link ExtensibilityModel} * for the current thread.

*/ public void closeExtensibilityModel(ExtensibilityModel model, Writer out) { if (logger.isDebugEnabled()) { logger.debug("Closing for thread: " + Thread.currentThread().getName()); } model.flushModel(out); this.modelUsed.set(Boolean.FALSE); this.extensibilityModel.set(null); } /** *

Returns the {@link ExtensibilityModel} for the current thread.

*/ public ExtensibilityModel getCurrentExtensibilityModel() { if (logger.isDebugEnabled()) { logger.debug("Getting current for thread: " + Thread.currentThread().getName()); } this.modelUsed.set(Boolean.TRUE); return this.extensibilityModel.get(); } /** *

This method is implemented to perform no action as it is not necessary for a standalone WebScript * container to add dependencies for processing.

*/ public void updateExtendingModuleDependencies(String pathBeingProcessed, Map model) { // NOT REQUIRED FOR STANDALONE WEBSCRIPT CONTAINER } /** *

A thread-safe cache of extended {@link ResourceBundle} instances for the current request.

*/ private ThreadLocal> extendedBundleCache = new ThreadLocal>(); /** *

Checks the cache to see if it has cached an extended bundle (that is a basic {@link ResourceBundle} that * has had extension modules applied to it. Extended bundles can only be safely cached once per request as the modules * applied can vary for each request.

* * @param webScriptId The id of the WebScript to retrieve the extended bundle for. * @return A cached bundle or null if the bundle has not previously been cached. */ public ResourceBundle getCachedExtendedBundle(String webScriptId) { ResourceBundle cachedExtendedBundle = null; Map threadLocal = this.extendedBundleCache.get(); if (threadLocal != null) { cachedExtendedBundle = this.extendedBundleCache.get().get(webScriptId); } return cachedExtendedBundle; } /** *

Adds a new extended bundle to the cache. An extended bundle is a WebScript {@link ResourceBundle} that has had * {@link ResourceBundle} instances merged into it from extension modules that have been applied. These can only be cached * for the lifetime of the request as different modules may be applied to the same WebScript for different requests.

* * @param webScriptId The id of the WebScript to cache the extended bundle against. * @param extensionBUndle The extended bundle to cache. */ public void addExtensionBundleToCache(String webScriptId, WebScriptPropertyResourceBundle extensionBundle) { Map threadLocal = this.extendedBundleCache.get(); if (threadLocal == null) { // This should never be the case because when a new model is opened this value should be reset // but we will double-check to avoid the potential of NPEs... threadLocal = new HashMap(); this.extendedBundleCache.set(threadLocal); } threadLocal.put(webScriptId, extensionBundle); } /** *

A {@link ThreadLocal} reference to the file currently being processed in the model. */ private ThreadLocal fileBeingProcessed = new ThreadLocal(); /** *

Returns the path of the file currently being processed in the model by the current thread. * This information is primarily provided for the purposes of generating debug information.

* * @return The path of the file currently being processed. */ public String getFileBeingProcessed() { return this.fileBeingProcessed.get(); } /** *

Sets the path of the file currently being processed in the model by the current thread. * This information should be collected to assist with providing debug information.

* @param file The path of the file currently being processed. */ public void setFileBeingProcessed(String file) { this.fileBeingProcessed.set(file); } /** *

Retrieves an files for the evaluated modules that are extending the WebScript files being processed.

*/ public List getExtendingModuleFiles(String pathBeingProcessed) { List extendingModuleFiles = new ArrayList(); for (BasicExtensionModule module: this.getEvaluatedModules()) { extendingModuleFiles.addAll(this.extensibilityModuleHandler.getExtendingModuleFiles(module, pathBeingProcessed)); } return extendingModuleFiles; } /** *

The list of {@link ExtensionModule} instances that have been evaluated as applicable to * this RequestContext. This is set to null when during instantiation and is only * properly set the first time the getEvaluatedModules method is invoked. This ensures * that module evaluation only occurs once per request.

*/ private ThreadLocal> evaluatedModules = new ThreadLocal>(); /** *

Retrieve the list of {@link ExtensionModule} instances that have been evaluated as applicable * for the current request. If this list has not yet been populated then use the {@link ExtensibilityModuleHandler} * configured in the Spring application context to evaluate them.

* * @return A list of {@link ExtensionModule} instances that are applicable to the current request. */ public List getEvaluatedModules() { List evaluatedModules = this.evaluatedModules.get(); if (evaluatedModules == null) { if (this.extensibilityModuleHandler == null) { if (logger.isErrorEnabled()) { logger.error("No 'extensibilityModuleHandler' has been configured for this request context. Extensions cannot be processed"); } evaluatedModules = new ArrayList(); this.evaluatedModules.set(evaluatedModules); } else { evaluatedModules = this.extensibilityModuleHandler.getExtensionModules(); this.evaluatedModules.set(evaluatedModules); } } return evaluatedModules; } /** *

This is a local {@link ConfigImpl} instance that will only be used when extension modules are employed. It will * initially be populated with the default "static" global configuration taken from the {@link ConfigService} associated * with this {@link RequestContext} but then updated to include global configuration provided by extension modules that * have been evaluated to be applied to the current request.

*/ private ThreadLocal globalConfig = new ThreadLocal(); /** *

This map represents {@link ConfigSection} instances mapped by area. It will only be used when extension modules are * employed. It will initially be populated with the default "static" configuration taken from the {@link ConfigService} associated * with this {@link RequestContext} but then updated to include configuration provided by extension modules that have been evaluated * to be applied to the current request.

*/ private ThreadLocal>> sectionsByArea = new ThreadLocal>>(); /** *

A list of {@link ConfigSection} instances that are only applicable to the current request. It will only be used when extension modules are * employed. It will initially be populated with the default "static" configuration taken from the {@link ConfigService} associated * with this {@link RequestContext} but then updated to include configuration provided by extension modules that have been evaluated * to be applied to the current request.

*/ private ThreadLocal> sections = new ThreadLocal>(); /** *

Creates a new {@link ExtendedScriptConfigModel} instance using the local configuration generated for this request. * If configuration for the request will be generated if it does not yet exist. It is likely that this method will be * called multiple times within the context of a single request and although the configuration containers will always * be the same a new {@link ExtendedScriptConfigModel} instance will always be created as the the supplied xmlConfig * string could be different for each call (because each WebScript invoked in the request will supply different * configuration.

*/ public ScriptConfigModel getExtendedScriptConfigModel(String xmlConfig) { if (this.globalConfig.get() == null && this.sectionsByArea.get() == null && this.sections.get() == null) { this.getConfigExtensions(); } return new ExtendedScriptConfigModel(getConfigService(), xmlConfig, this.globalConfig.get(), this.sectionsByArea.get(), this.sections.get()); } /** *

Creates a new {@link TemplateConfigModel} instance using the local configuration generated for this request. * If configuration for the request will be generated if it does not yet exist. It is likely that this method will be * called multiple times within the context of a single request and although the configuration containers will always * be the same a new {@link TemplateConfigModel} instance will always be created as the the supplied xmlConfig * string could be different for each call (because each WebScript invoked in the request will supply different * configuration.

*/ public TemplateConfigModel getExtendedTemplateConfigModel(String xmlConfig) { if (this.globalConfig.get() == null && this.sectionsByArea.get() == null && this.sections.get() == null) { this.getConfigExtensions(); } return new ExtendedTemplateConfigModel(getConfigService(), xmlConfig, this.globalConfig.get(), this.sectionsByArea.get(), this.sections.get()); } /** *

Creates and populates the request specific configuration container objects (globalConfig, sectionsByArea & * sections with a combination of the default static configuration (taken from files accessed by the {@link ConfigService}) and * dynamic configuration taken from extension modules evaluated for the current request.

*/ private void getConfigExtensions() { // Extended configuration is only possible if config service is an XMLConfigService... // // ...also, it's only necessary to populate the configuration containers if they have not already been populated. This test should also // be carried out by the two methods ("getExtendedTemplateConfigModel" & "getExtendedTemplateConfigModel") to prevent duplication // of effort... but in case other methods attempt to access it we will make these additional tests. if (getConfigService() instanceof XMLConfigService && this.globalConfig == null && this.sectionsByArea == null && this.sections == null) { // Cast the config service for ease of access XMLConfigService xmlConfigService = (XMLConfigService) getConfigService(); // Get the current configuration from the ConfigService - we don't want to permanently pollute // the standard configuration with additions from the modules... this.globalConfig.set(new ConfigImpl((ConfigImpl)xmlConfigService.getGlobalConfig())); // Make a copy of the current global config // Initialise these with the config service values... this.sectionsByArea.set(new HashMap>(xmlConfigService.getSectionsByArea())); this.sections.set(new ArrayList(xmlConfigService.getSections())); // Check to see if there are any modules that we need to apply... List evaluatedModules = this.getEvaluatedModules(); if (evaluatedModules != null && !evaluatedModules.isEmpty()) { for (BasicExtensionModule currModule: evaluatedModules) { for (Element currentConfigElement: currModule.getConfigurations()) { // Set up containers for our request specific configuration - this will contain data taken from the evaluated modules... Map parsedElementReaders = new HashMap(); Map parsedEvaluators = new HashMap(); List parsedConfigSections = new ArrayList(); // Parse and process the parses configuration... String currentArea = xmlConfigService.parseFragment(currentConfigElement, parsedElementReaders, parsedEvaluators, parsedConfigSections); for (Map.Entry entry : parsedEvaluators.entrySet()) { // add the evaluators to the config service parsedEvaluators.put(entry.getKey(), entry.getValue()); } for (Map.Entry entry : parsedElementReaders.entrySet()) { // add the element readers to the config service parsedElementReaders.put(entry.getKey(), entry.getValue()); } for (ConfigSection section : parsedConfigSections) { // Update local configuration with our updated data... xmlConfigService.addConfigSection(section, currentArea, this.globalConfig.get(), this.sectionsByArea.get(), this.sections.get()); } } } } } } /** *

Adds the <{@code}@markup> directive to the container which allows FreeMarker templates to be extended.

*/ public void addExtensibilityDirectives(Map freeMarkerModel, ExtensibilityModel extModel) { MarkupDirective mud = new MarkupDirective("markup", extModel); freeMarkerModel.put("markup", mud); } }