All Downloads are FREE. Search and download functionalities are using the official Maven repository.

de.acosix.alfresco.utility.repo.web.scripts.TenantExtensibilityContainer Maven / Gradle / Ivy

There is a newer version: 1.4.4
Show newest version
/*
 * Copyright 2016 - 2024 Acosix GmbH
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 * http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
package de.acosix.alfresco.utility.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.alfresco.repo.web.scripts.ExtensibilityContainer;
import org.alfresco.repo.web.scripts.RepositoryContainer;
import org.alfresco.repo.web.scripts.TenantRepositoryContainer;
import org.dom4j.Element;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
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.Status;
import org.springframework.extensions.webscripts.TemplateConfigModel;
import org.springframework.extensions.webscripts.WebScriptException;
import org.springframework.extensions.webscripts.WebScriptPropertyResourceBundle;
import org.springframework.extensions.webscripts.WebScriptRequest;
import org.springframework.extensions.webscripts.WebScriptResponse;

/**
 * This class is basically a copy of the {@link ExtensibilityContainer} capable of handling
 * Repository-tier web
 * script extensibility to use {@link TenantRepositoryContainer} as the base which has become the default container with Alfresco .
 *
 * An improvement request has been filed with Alfresco as ALF-21794
 *
 * @author Axel Faust
 */
public class TenantExtensibilityContainer extends TenantRepositoryContainer implements HandlesExtensibility
{

    private static final Logger LOGGER = LoggerFactory.getLogger(TenantExtensibilityContainer.class);

    /**
     *
     * Maintains a list of all the {@link ExtensibilityModel} instances being used across all the
     * available threads.
     *
     */
    protected final ThreadLocal extensibilityModel = new ThreadLocal<>();

    /**
     *
     * A thread-safe cache of extended {@link ResourceBundle} instances for the current request.
     *
     */
    protected final ThreadLocal> extendedBundleCache = new ThreadLocal<>();

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

    /**
     *
     * The list of {@link BasicExtensionModule extension module} 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.
     *
     */
    protected final ThreadLocal> evaluatedModules = new ThreadLocal<>();

    /**
     *
     * 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 the current request context but then updated to include global configuration provided by
     * extension modules that have been evaluated to be applied to the current request.
     *
     */
    protected 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 the current request context but then updated to include configuration provided by extension modules that have been evaluated to
     * be applied to the current request.
     *
     */
    protected 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 the current request context but then updated to include configuration provided by extension modules that have been evaluated to
     * be applied to the current request.
     *
     */
    protected ThreadLocal> sections = new ThreadLocal<>();

    /**
     *
     * A {@link WebScriptExtensibilityModuleHandler extensibility handler} 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.
     *
     */
    protected WebScriptExtensibilityModuleHandler extensibilityModuleHandler = null;

    /**
     *
     * Sets the {@link WebScriptExtensibilityModuleHandler extensibility handler} for this {@link RepositoryContainer container}.
     *
     *
     * @param extensibilityModuleHandler
     *            WebScriptExtensibilityModuleHandler
     */
    public void setExtensibilityModuleHandler(final WebScriptExtensibilityModuleHandler extensibilityModuleHandler)
    {
        this.extensibilityModuleHandler = extensibilityModuleHandler;
    }

    /**
     *
     * 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.
     */
    @Override
    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.
     */
    @Override
    public void setFileBeingProcessed(final String file)
    {
        this.fileBeingProcessed.set(file);
    }

    /**
     *
     * {@inheritDoc}
     */
    @Override
    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(final WebScriptRequest scriptReq, final WebScriptResponse scriptRes, final Authenticator auth)
            throws IOException
    {
        final ExtensibilityModel extModel = this.openExtensibilityModel();
        boolean exceptionOccurred = false;
        try
        {
            super.executeScript(scriptReq, scriptRes, auth);
        }
        catch (final Exception e)
        {
            LOGGER.debug(
                    "{} occurred during script execution - not closing extensibility model and thus not flushing response (relegated to container status handling)",
                    e.getClass());
            exceptionOccurred = true;
            if (e instanceof RuntimeException || e instanceof IOException)
            {
                throw e;
            }
            throw new WebScriptException(Status.STATUS_INTERNAL_SERVER_ERROR, "Unexpected error", e);
        }
        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 (!exceptionOccurred && extModel.isModelStarted())
            {
                try
                {
                    this.closeExtensibilityModel(extModel, scriptRes.getWriter());
                }
                catch (final IOException e)
                {
                    LOGGER.error("An error occurred getting the Writer when closing an ExtensibilityModel", e);
                }
            }
        }
    }

    /**
     *
     * Creates a new {@link ExtensibilityModel} and sets it on the current thread
     */
    @Override
    public ExtensibilityModel openExtensibilityModel()
    {
        LOGGER.debug("Opening for thread: {}", Thread.currentThread().getName());
        this.extendedBundleCache.set(new HashMap());

        final ExtensibilityModel model = new ExtensibilityModelImpl(null, this);
        this.extensibilityModel.set(model);

        return model;
    }

    /**
     * Flushes the {@link ExtensibilityModel} provided and sets its parent as the current {@link ExtensibilityModel}
     * for the current thread.
     */
    @Override
    public void closeExtensibilityModel(final ExtensibilityModel model, final Writer out)
    {
        LOGGER.debug("Closing for thread: {}", Thread.currentThread().getName());
        model.flushModel(out);

        this.extendedBundleCache.remove();
        this.evaluatedModules.remove();
        this.fileBeingProcessed.remove();
        this.globalConfig.remove();
        this.sections.remove();
        this.sectionsByArea.remove();

        this.extensibilityModel.remove();
    }

    /**
     * Returns the {@link ExtensibilityModel} for the current thread.
     */
    @Override
    public ExtensibilityModel getCurrentExtensibilityModel()
    {
        LOGGER.debug("Getting current for thread: {}", Thread.currentThread().getName());
        ExtensibilityModel extensibilityModel = this.extensibilityModel.get();
        if (extensibilityModel == null)
        {
            extensibilityModel = this.openExtensibilityModel();
        }
        return extensibilityModel;
    }

    /**
     * This method is implemented to perform no action as it is not necessary for a standalone WebScript
     * container to add dependencies for processing.
     */
    @Override
    public void updateExtendingModuleDependencies(final String pathBeingProcessed, final Map model)
    {
        // NOT REQUIRED FOR STANDALONE WEBSCRIPT CONTAINER
    }

    /**
     * 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.
     */
    @Override
    public ResourceBundle getCachedExtendedBundle(final String webScriptId)
    {
        ResourceBundle cachedExtendedBundle = null;
        final 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.
     */
    @Override
    public void addExtensionBundleToCache(final String webScriptId, final 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);
    }

    /**
     *
     * Retrieves an files for the evaluated modules that are extending the WebScript files being processed.
     *
     */
    @Override
    public List getExtendingModuleFiles(final String pathBeingProcessed)
    {
        final List extendingModuleFiles = new ArrayList<>();
        for (final BasicExtensionModule module : this.getEvaluatedModules())
        {
            extendingModuleFiles.addAll(this.extensibilityModuleHandler.getExtendingModuleFiles(module, pathBeingProcessed));
        }
        return extendingModuleFiles;
    }

    /**
     * Retrieve the list of {@link BasicExtensionModule extension module} instances that have been evaluated as applicable for the current
     * request. If this list has not yet been populated then use the {@link WebScriptExtensibilityModuleHandler extensibility handler}
     * configured in the Spring application context to
     * evaluate them.
     *
     * @return A list of {@link BasicExtensionModule} 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<>();
            }
            else
            {
                evaluatedModules = this.extensibilityModuleHandler.getExtensionModules();
            }
            this.evaluatedModules.set(evaluatedModules);
        }
        return evaluatedModules;
    }

    /**
     * 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.
     */
    @Override
    public ScriptConfigModel getExtendedScriptConfigModel(final String xmlConfig)
    {
        if (this.globalConfig.get() == null && this.sectionsByArea.get() == null && this.sections.get() == null)
        {
            this.getConfigExtensions();
        }
        return new ExtendedScriptConfigModel(this.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.
     */
    @Override
    public TemplateConfigModel getExtendedTemplateConfigModel(final String xmlConfig)
    {
        if (this.globalConfig.get() == null && this.sectionsByArea.get() == null && this.sections.get() == null)
        {
            this.getConfigExtensions();
        }
        return new ExtendedTemplateConfigModel(this.getConfigService(), xmlConfig, this.globalConfig.get(), this.sectionsByArea.get(),
                this.sections.get());
    }

    /**
     * Adds the <@markup> directive to the container which allows FreeMarker templates to be extended.
     */
    @Override
    public void addExtensibilityDirectives(final Map freeMarkerModel, final ExtensibilityModel extModel)
    {
        final MarkupDirective mud = new MarkupDirective("markup", extModel);
        freeMarkerModel.put("markup", mud);
    }

    /**
     * Creates and populates the request specific configuration container objects (globalConfig, sectionsByArea
     * and
     * 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.
     */
    protected 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 (this.getConfigService() instanceof XMLConfigService && this.globalConfig.get() == null && this.sectionsByArea.get() == null
                && this.sections.get() == null)
        {
            // Cast the config service for ease of access
            final XMLConfigService xmlConfigService = (XMLConfigService) this.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...
            final List evaluatedModules = this.getEvaluatedModules();
            if (evaluatedModules != null && !evaluatedModules.isEmpty())
            {
                for (final BasicExtensionModule currModule : evaluatedModules)
                {
                    for (final Element currentConfigElement : currModule.getConfigurations())
                    {
                        // Set up containers for our request specific configuration - this will contain data taken from the evaluated
                        // modules...
                        final Map parsedElementReaders = new HashMap<>();
                        final Map parsedEvaluators = new HashMap<>();
                        final List parsedConfigSections = new ArrayList<>();

                        // Parse and process the parses configuration...
                        final String currentArea = xmlConfigService.parseFragment(currentConfigElement, parsedElementReaders,
                                parsedEvaluators, parsedConfigSections);
                        for (final Map.Entry entry : parsedEvaluators.entrySet())
                        {
                            // add the evaluators to the config service
                            parsedEvaluators.put(entry.getKey(), entry.getValue());
                        }

                        for (final Map.Entry entry : parsedElementReaders.entrySet())
                        {
                            // add the element readers to the config service
                            parsedElementReaders.put(entry.getKey(), entry.getValue());
                        }

                        for (final ConfigSection section : parsedConfigSections)
                        {
                            // Update local configuration with our updated data...
                            xmlConfigService.addConfigSection(section, currentArea, this.globalConfig.get(), this.sectionsByArea.get(),
                                    this.sections.get());
                        }
                    }
                }
            }
        }
    }
}




© 2015 - 2024 Weber Informatics LLC | Privacy Policy