de.axelfaust.alfresco.nashorn.repo.processor.NashornScriptProcessor.java Source code

Java tutorial

Introduction

Here is the source code for de.axelfaust.alfresco.nashorn.repo.processor.NashornScriptProcessor.java

Source

/*
 * Copyright 2015, 2016 Axel Faust
 *
 * Licensed under the Eclipse Public License (EPL), Version 1.0 (the "License"); you may not use
 * this file except in compliance with the License. You may obtain a copy of the License at
 *
 * https://www.eclipse.org/legal/epl-v10.html
 *
 * 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.axelfaust.alfresco.nashorn.repo.processor;

import java.io.IOException;
import java.io.Reader;
import java.net.URL;
import java.nio.charset.StandardCharsets;
import java.text.MessageFormat;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Properties;
import java.util.concurrent.locks.ReentrantReadWriteLock;

import javax.script.Bindings;
import javax.script.Invocable;
import javax.script.ScriptContext;
import javax.script.ScriptEngine;
import javax.script.ScriptException;
import javax.script.SimpleBindings;

import jdk.nashorn.api.scripting.URLReader;

import org.alfresco.repo.jscript.ClasspathScriptLocation;
import org.alfresco.repo.processor.BaseProcessor;
import org.alfresco.service.cmr.module.ModuleDependency;
import org.alfresco.service.cmr.module.ModuleDetails;
import org.alfresco.service.cmr.module.ModuleService;
import org.alfresco.service.cmr.repository.NodeRef;
import org.alfresco.service.cmr.repository.ScriptLocation;
import org.alfresco.service.cmr.repository.ScriptProcessor;
import org.alfresco.service.cmr.repository.StoreRef;
import org.alfresco.service.namespace.NamespaceService;
import org.alfresco.service.namespace.QName;
import org.alfresco.util.MD5;
import org.alfresco.util.Pair;
import org.alfresco.util.ParameterCheck;
import org.alfresco.util.PropertyCheck;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.BeansException;
import org.springframework.beans.factory.InitializingBean;
import org.springframework.context.ApplicationContext;
import org.springframework.context.ApplicationContextAware;
import org.springframework.core.io.Resource;

import de.axelfaust.alfresco.nashorn.repo.loaders.CallerProvidedURLStreamHandler;

/**
 * @author Axel Faust
 */
@SuppressWarnings("restriction")
public class NashornScriptProcessor extends BaseProcessor implements ScriptProcessor, InitializingBean,
        ApplicationContextAware, ResettableScriptProcessorElement.Registry {

    public static final List<String> NASHORN_GLOBAL_PROPERTIES_TO_ALWAYS_REMOVE = Collections
            .unmodifiableList(Arrays.asList("load", "loadWithNewGlobal", "exit", "quit"));

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

    private static final String SCRIPT_SIMPLE_LOGGER = "simple-logger.js";

    private static final String SCRIPT_AMD = "amd.js";

    private static final String SCRIPT_NO_SUCH_PROPERTY = "noSuchProperty.js";

    private static final String SCRIPE_AMD_SCRIPT_RUNNER = "amd-script-runner.js";

    private static final ThreadLocal<Boolean> inEngineContextInitialization = new ThreadLocal<Boolean>();

    /**
     * Checks if the current thread is currently involved in script engine context initialization.
     *
     * @return {@code true} if the current thread is initializing a new script engine context
     */
    public static boolean isInEngineContextInitialization() {
        final boolean result = Boolean.TRUE.equals(inEngineContextInitialization.get());
        return result;
    }

    protected ScriptEngine engine;

    protected NamespaceService namespaceService;

    protected ModuleService moduleService;

    protected Properties globalProperties;

    protected Resource amdConfig;

    protected String nashornGlobalPropertiesToRemove;

    protected ApplicationContext applicationContext;

    protected boolean executeArbitraryScriptStringsAsSecure = false;

    protected AMDModulePreloader amdPreloader;

    protected AMDScriptRunner amdRunner;

    protected final ReentrantReadWriteLock initialisationStateLock = new ReentrantReadWriteLock(true);

    protected final List<ResettableScriptProcessorElement> resettableProcessorElements = new ArrayList<ResettableScriptProcessorElement>();

    /**
     * {@inheritDoc}
     */
    @Override
    public void setApplicationContext(final ApplicationContext applicationContext) throws BeansException {
        this.applicationContext = applicationContext;
    }

    /**
     * @param engine
     *            the engine to set
     */
    public void setEngine(final ScriptEngine engine) {
        this.engine = engine;
    }

    /**
     * @param namespaceService
     *            the namespaceService to set
     */
    public void setNamespaceService(final NamespaceService namespaceService) {
        this.namespaceService = namespaceService;
    }

    /**
     * @param moduleService
     *            the moduleService to set
     */
    public void setModuleService(final ModuleService moduleService) {
        this.moduleService = moduleService;
    }

    /**
     * @param globalProperties
     *            the globalProperties to set
     */
    public void setGlobalProperties(final Properties globalProperties) {
        this.globalProperties = globalProperties;
    }

    /**
     * @param amdConfig
     *            the amdConfig to set
     */
    public void setAmdConfig(final Resource amdConfig) {
        this.amdConfig = amdConfig;
    }

    /**
     * @param nashornGlobalPropertiesToRemove
     *            the nashornGlobalPropertiesToRemove to set
     */
    public void setNashornGlobalPropertiesToRemove(final String nashornGlobalPropertiesToRemove) {
        this.nashornGlobalPropertiesToRemove = nashornGlobalPropertiesToRemove;
    }

    /**
     * @param executeArbitraryScriptStringsAsSecure
     *            the executeArbitraryScriptStringsAsSecure to set
     */
    public void setExecuteArbitraryScriptStringsAsSecure(final boolean executeArbitraryScriptStringsAsSecure) {
        this.executeArbitraryScriptStringsAsSecure = executeArbitraryScriptStringsAsSecure;
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public void afterPropertiesSet() {
        PropertyCheck.mandatory(this, "name", this.name);
        PropertyCheck.mandatory(this, "extension", this.extension);
        PropertyCheck.mandatory(this, "scriptService", this.scriptService);
        PropertyCheck.mandatory(this, "moduleService", this.moduleService);
        PropertyCheck.mandatory(this, "engine", this.engine);

        PropertyCheck.mandatory(this, "globalProperties", this.globalProperties);
        PropertyCheck.mandatory(this, "amdConfig", this.amdConfig);

        this.initialisationStateLock.writeLock().lock();
        try {
            this.initScriptContext();
        } catch (final ScriptException sex) {
            throw new org.alfresco.scripts.ScriptException("Error initialising script processor", sex);
        } finally {
            this.initialisationStateLock.writeLock().unlock();
        }

        super.register();
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public void register(final ResettableScriptProcessorElement element) {
        if (element != null) {
            this.resettableProcessorElements.add(element);
        }
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public Object execute(final ScriptLocation location, final Map<String, Object> model) {
        ParameterCheck.mandatory("location", location);

        final Object result;
        if (location instanceof AMDLoadableScript) {
            result = this.executeAMDLoadableScript((AMDLoadableScript) location, model);
        } else if (location instanceof ClasspathScriptLocation) {
            final AMDLoadableScript script = new SimpleAMDLoadableScript("classpath", location.getPath());
            result = this.executeAMDLoadableScript(script, model);
        } else {
            CallerProvidedURLStreamHandler.registerCallerProvidedScript(location);
            try {
                final AMDLoadableScript script = new SimpleAMDLoadableScript("callerProvided", location.getPath());
                result = this.executeAMDLoadableScript(script, model);
            } finally {
                CallerProvidedURLStreamHandler.clearCallerProvidedScript();
            }
        }

        return result;
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public Object execute(final NodeRef nodeRef, final QName contentProp, final Map<String, Object> model) {
        ParameterCheck.mandatory("nodeRef", nodeRef);
        ParameterCheck.mandatory("contentProp", contentProp);

        final StringBuilder moduleIdBuilder = new StringBuilder();
        final StoreRef storeRef = nodeRef.getStoreRef();
        moduleIdBuilder.append(storeRef.getProtocol());
        moduleIdBuilder.append('/');
        moduleIdBuilder.append(storeRef.getIdentifier());
        moduleIdBuilder.append('/');
        moduleIdBuilder.append(nodeRef.getId());
        moduleIdBuilder.append('/');
        moduleIdBuilder.append(contentProp.toPrefixString(this.namespaceService).replaceFirst(":", "_"));

        final AMDLoadableScript script = new SimpleAMDLoadableScript("node", moduleIdBuilder.toString());
        final Object result = this.executeAMDLoadableScript(script, model);

        return result;
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public Object execute(final String location, final Map<String, Object> model) {
        ParameterCheck.mandatoryString("location", location);

        final AMDLoadableScript script = new SimpleAMDLoadableScript("classpath", location);
        final Object result = this.executeAMDLoadableScript(script, model);

        return result;
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public Object executeString(final String scriptStr, final Map<String, Object> model) {
        ParameterCheck.mandatoryString("script", scriptStr);

        CallerProvidedURLStreamHandler.registerCallerProvidedScript(scriptStr,
                this.executeArbitraryScriptStringsAsSecure);
        try {
            final String location = MD5.Digest(scriptStr.getBytes(StandardCharsets.UTF_8));
            final AMDLoadableScript script = new SimpleAMDLoadableScript("callerProvided", location);
            final Object result = this.executeAMDLoadableScript(script, model);
            return result;
        } finally {
            CallerProvidedURLStreamHandler.clearCallerProvidedScript();
        }
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public void reset() {
        this.initialisationStateLock.writeLock().lock();
        try {
            this.initScriptContext();
        } catch (final ScriptException sex) {
            throw new org.alfresco.scripts.ScriptException("Error resetting script processor", sex);
        } finally {
            this.initialisationStateLock.writeLock().unlock();
        }

        this.resettableProcessorElements.forEach(x -> x.reset());
    }

    protected Object executeAMDLoadableScript(final AMDLoadableScript script, final Map<String, Object> model) {
        this.initialisationStateLock.readLock().lock();
        try {
            try (final NashornScriptModel scriptModel = NashornScriptModel.openModel()) {
                LOGGER.debug("Executing AMD-loadable script {}", script);
                final String moduleId;
                if (script.getLoaderName() != null && !script.getLoaderName().isEmpty()) {
                    moduleId = MessageFormat.format("{0}!{1}", script.getLoaderName(), script.getScriptModuleId());
                } else {
                    moduleId = script.getScriptModuleId();
                }

                try {
                    final Object scriptResult = this.amdRunner.run(moduleId, model);
                    LOGGER.debug("Return value of AMD-loadable script {}: {}", script, scriptResult);

                    // TODO Convert / unwrap scriptResult for Java (could also be done in runner)
                    return scriptResult;
                } catch (final RuntimeException ex) {
                    LOGGER.debug("Error executing AMD-loadable script", ex);
                    throw ex;
                }
            }
        } finally {
            this.initialisationStateLock.readLock().unlock();
        }
    }

    protected void initScriptContext() throws ScriptException {
        final Bindings oldEngineBindings = this.engine.getBindings(ScriptContext.ENGINE_SCOPE);
        final AMDModulePreloader oldPreloader = this.amdPreloader;
        final AMDScriptRunner oldAMDRunner = this.amdRunner;

        LOGGER.debug("Initializing new script context");

        inEngineContextInitialization.set(Boolean.TRUE);
        try (NashornScriptModel model = NashornScriptModel.openModel()) {
            // create new (unpolluted) engine bindings
            final Bindings engineBindings = this.engine.createBindings();
            this.engine.setBindings(engineBindings, ScriptContext.ENGINE_SCOPE);

            final Bindings globalBindings = new SimpleBindings();
            this.engine.setBindings(globalBindings, ScriptContext.GLOBAL_SCOPE);

            // only available during initialisation
            globalBindings.put("applicationContext", this.applicationContext);

            LOGGER.debug("Executing bootstrap scripts");
            URL resource;

            // 1) simple logger facade for SLF4J
            LOGGER.debug("Bootstrapping simple logger");
            resource = NashornScriptProcessor.class.getResource(SCRIPT_SIMPLE_LOGGER);
            this.executeScriptFromResource(resource);

            // 2) AMD loader and noSuchProperty to be used for all scripts apart from bootstrap
            LOGGER.debug("Setting up AMD");
            resource = NashornScriptProcessor.class.getResource(SCRIPT_AMD);
            this.executeScriptFromResource(resource);

            resource = NashornScriptProcessor.class.getResource(SCRIPT_NO_SUCH_PROPERTY);
            this.executeScriptFromResource(resource);

            final Object define = this.engine.get("define");
            this.amdPreloader = ((Invocable) this.engine).getInterface(define, AMDModulePreloader.class);

            // 3) the nashorn loader plugin so we can control access to globals
            LOGGER.debug("Setting up nashorn AMD loader");
            this.preloadAMDModule("loaderMetaLoader", "nashorn", "nashorn");

            // 4) remove Nashorn globals
            LOGGER.debug("Removing disallowed Nashorn globals \"{}\" and \"{}\"",
                    NASHORN_GLOBAL_PROPERTIES_TO_ALWAYS_REMOVE, this.nashornGlobalPropertiesToRemove);
            for (final String property : NASHORN_GLOBAL_PROPERTIES_TO_ALWAYS_REMOVE) {
                engineBindings.remove(property);
            }

            if (this.nashornGlobalPropertiesToRemove != null) {
                for (final String property : this.nashornGlobalPropertiesToRemove.split(",")) {
                    engineBindings.remove(property.trim());
                }
            }

            // 5) Java extensions (must be picked up by modules before #9)
            globalBindings.put("processorExtensions", this.processorExtensions);

            // 6) AMD config
            LOGGER.debug("Applying AMD config \"{}\"", this.amdConfig);
            resource = this.amdConfig.getURL();
            this.executeScriptFromResource(resource);

            // 7) configured JavaScript base modules
            LOGGER.debug("Bootstrapping base modules");
            this.initScriptContextBaseModules(this.amdPreloader);

            // 8) configured JavaScript extension modules
            LOGGER.debug("Bootstrapping extension modules");
            this.initScriptContextExtensions(this.amdPreloader);

            // 9) remove any init data that shouldn't be publicly available
            globalBindings.clear();

            // 10) obtain the runner interface
            LOGGER.debug("Preparing AMD script runner");
            resource = NashornScriptProcessor.class.getResource(SCRIPE_AMD_SCRIPT_RUNNER);
            final Object scriptRunnerObj = this.executeScriptFromResource(resource);
            this.amdRunner = ((Invocable) this.engine).getInterface(scriptRunnerObj, AMDScriptRunner.class);

            LOGGER.info("New script context initialized");
        } catch (final Throwable t) {
            LOGGER.warn("Initialization of script context failed", t);

            // reset
            this.engine.setBindings(oldEngineBindings, ScriptContext.ENGINE_SCOPE);
            this.engine.getBindings(ScriptContext.GLOBAL_SCOPE).clear();
            this.amdPreloader = oldPreloader;
            this.amdRunner = oldAMDRunner;

            if (t instanceof RuntimeException) {
                throw (RuntimeException) t;
            } else if (t instanceof ScriptException) {
                throw (ScriptException) t;
            } else {
                throw new org.alfresco.scripts.ScriptException(
                        "Unknown error initializing script context - reset to previous state", t);
            }
        } finally {
            inEngineContextInitialization.remove();
        }
    }

    protected void initScriptContextBaseModules(final AMDModulePreloader amdPreloader) throws ScriptException {
        final Object extensionScriptsValue = this.globalProperties.get("nashornJavaScriptProcessor.baseScripts");
        if (extensionScriptsValue instanceof String && !((String) extensionScriptsValue).trim().isEmpty()) {
            final String[] scriptNames = ((String) extensionScriptsValue).trim().split("\\s*,\\s*");

            LOGGER.debug("Processing {} base scripts", Integer.valueOf(scriptNames.length));

            for (final String scriptName : scriptNames) {
                final String modulePrefix = "nashornJavaScriptProcessor.baseScript." + scriptName;
                final String moduleIdKey = modulePrefix + ".moduleId";
                final String loaderNameKey = modulePrefix + ".loaderName";
                final String aliasModuleIdKey = modulePrefix + ".aliasModuleId";

                final Object moduleIdValue = this.globalProperties.get(moduleIdKey);
                final Object loaderNameValue = this.globalProperties.get(loaderNameKey);
                final Object aliasModuleIdValue = this.globalProperties.get(aliasModuleIdKey);

                if (moduleIdValue instanceof String && loaderNameValue instanceof String) {
                    this.preloadAMDModule(loaderNameValue, moduleIdValue, aliasModuleIdValue);
                }
            }
        }
    }

    protected void initScriptContextExtensions(final AMDModulePreloader amdPreloader) throws ScriptException {
        final Map<String, Pair<ModuleDetails, Boolean>> modules = new HashMap<String, Pair<ModuleDetails, Boolean>>();

        for (final ModuleDetails module : this.moduleService.getAllModules()) {
            final String moduleId = module.getId();
            modules.put(moduleId, new Pair<>(module, Boolean.FALSE));

            for (final String alias : module.getAliases()) {
                modules.put(alias, modules.get(moduleId));
            }
        }

        for (final ModuleDetails module : this.moduleService.getAllModules()) {
            this.initScriptContextExtensions(modules, module);
        }

        // TODO Load custom extensions outside of module context (i.e. definitions by customer in alfresco-global.properties)
    }

    protected void initScriptContextExtensions(final Map<String, Pair<ModuleDetails, Boolean>> modules,
            final ModuleDetails module) throws ScriptException {
        final String moduleId = module.getId();
        final Pair<ModuleDetails, Boolean> moduleExecution = modules.get(moduleId);
        if (!Boolean.TRUE.equals(moduleExecution.getSecond())) {
            // ensure module dependencies had their chance to init script context extensions first
            for (final ModuleDependency dependency : module.getDependencies()) {
                final Pair<ModuleDetails, Boolean> dependencyState = modules.get(dependency.getDependencyId());
                if (dependencyState != null) {
                    // just recurse - will check execution state therein
                    this.initScriptContextExtensions(modules, dependencyState.getFirst());
                }
                // else missing dependency - ModuleService should have failed startup
            }

            LOGGER.trace("Checking for extension scripts in module {}", moduleId);

            // check for module script context extensions
            final String extensionScriptsKey = MessageFormat
                    .format("nashornJavaScriptProcessor.{0}.extensionScripts", moduleId);
            final Object extensionScriptsValue = this.globalProperties.get(extensionScriptsKey);
            if (extensionScriptsValue instanceof String && !((String) extensionScriptsValue).trim().isEmpty()) {
                final String[] scriptNames = ((String) extensionScriptsValue).trim().split("\\s*,\\s*");

                LOGGER.debug("Processing {} extension scripts in module {}", Integer.valueOf(scriptNames.length),
                        moduleId);

                for (final String scriptName : scriptNames) {
                    final String modulePrefix = MessageFormat
                            .format("nashornJavaScriptProcessor.{0}.extensionScript.{1}", moduleId, scriptName);
                    final String moduleIdKey = modulePrefix + ".moduleId";
                    final String loaderNameKey = modulePrefix + ".loaderName";
                    final String aliasModuleIdKey = modulePrefix + ".aliasModuleId";

                    final Object moduleIdValue = this.globalProperties.get(moduleIdKey);
                    final Object loaderNameValue = this.globalProperties.get(loaderNameKey);
                    final Object aliasModuleIdValue = this.globalProperties.get(aliasModuleIdKey);

                    if (moduleIdValue instanceof String && loaderNameValue instanceof String) {
                        this.preloadAMDModule(loaderNameValue, moduleIdValue, aliasModuleIdValue);
                    }
                }
            }

            // mark as executed
            moduleExecution.setSecond(Boolean.TRUE);
        }
    }

    protected void preloadAMDModule(final Object loaderNameValue, final Object moduleIdValue,
            final Object aliasModuleIdValue) throws ScriptException {
        this.initialisationStateLock.readLock().lock();
        try {
            LOGGER.debug("Pre-loading module {}!{}", loaderNameValue, moduleIdValue);

            String call;
            if (aliasModuleIdValue != null) {
                call = MessageFormat.format("define.preload(''{0}!{1}'', ''{2}'');", loaderNameValue, moduleIdValue,
                        aliasModuleIdValue);
            } else {
                call = MessageFormat.format("define.preload(''{0}!{1}'');", loaderNameValue, moduleIdValue);
            }

            try {
                // TODO Determine if it's possible to still use ScriptContext attributes in scripts called via extracted interfaces
                // TODO Determine why we can't get AMDModulePreloader interface for define.preload()
                // final ScriptObjectMirror define = (ScriptObjectMirror) this.engine.getContext().getAttribute("define");
                // define.callMember("preload", fullModuleId);
                // this.amdPreloader.preload(fullModuleId);
                this.engine.eval(call);
            } catch (final RuntimeException ex) {
                LOGGER.error("Error preloading module {}!{}", loaderNameValue, moduleIdValue);
                throw ex;
            }
        } finally {
            this.initialisationStateLock.readLock().unlock();
        }
    }

    protected Object executeScriptFromResource(final URL resource) throws ScriptException {
        this.initialisationStateLock.readLock().lock();
        try {
            LOGGER.debug("Executing script from resource {}", resource);
            try (Reader reader = new URLReader(resource)) {
                this.engine.getContext().setAttribute(ScriptEngine.FILENAME, resource.toString(),
                        ScriptContext.ENGINE_SCOPE);
                return this.engine.eval(reader);
            } catch (final IOException e) {
                throw new ScriptException(e);
            } finally {
                LOGGER.trace("Execution of script from resource {} completed", resource);
            }
        } finally {
            this.initialisationStateLock.readLock().unlock();
        }
    }
}