001/*
002 * (C) Copyright 2016 Nuxeo SA (http://nuxeo.com/) and others.
003 *
004 * Licensed under the Apache License, Version 2.0 (the "License");
005 * you may not use this file except in compliance with the License.
006 * You may obtain a copy of the License at
007 *
008 *     http://www.apache.org/licenses/LICENSE-2.0
009 *
010 * Unless required by applicable law or agreed to in writing, software
011 * distributed under the License is distributed on an "AS IS" BASIS,
012 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
013 * See the License for the specific language governing permissions and
014 * limitations under the License.
015 *
016 */
017package org.nuxeo.automation.scripting.internals;
018
019import static org.nuxeo.automation.scripting.api.AutomationScriptingConstants.AUTOMATION_SCRIPTING_PRECOMPILE;
020import static org.nuxeo.automation.scripting.api.AutomationScriptingConstants.COMPLIANT_JAVA_VERSION_CACHE;
021import static org.nuxeo.automation.scripting.api.AutomationScriptingConstants.COMPLIANT_JAVA_VERSION_CLASS_FILTER;
022import static org.nuxeo.automation.scripting.api.AutomationScriptingConstants.DEFAULT_PRECOMPILE_STATUS;
023import static org.nuxeo.automation.scripting.api.AutomationScriptingConstants.NASHORN_JAVA_VERSION;
024import static org.nuxeo.automation.scripting.api.AutomationScriptingConstants.NASHORN_WARN_CACHE;
025import static org.nuxeo.automation.scripting.api.AutomationScriptingConstants.NASHORN_WARN_CLASS_FILTER;
026import static org.nuxeo.launcher.config.ConfigurationGenerator.checkJavaVersion;
027
028import java.io.InputStream;
029import java.io.InputStreamReader;
030import java.lang.reflect.InvocationHandler;
031import java.lang.reflect.Method;
032import java.lang.reflect.Proxy;
033import java.util.HashSet;
034import java.util.Set;
035
036import javax.script.Compilable;
037import javax.script.CompiledScript;
038import javax.script.Invocable;
039import javax.script.ScriptContext;
040import javax.script.ScriptEngine;
041import javax.script.ScriptException;
042import org.apache.commons.logging.Log;
043import org.apache.commons.logging.LogFactory;
044import org.nuxeo.automation.scripting.api.AutomationScriptingService;
045import org.nuxeo.ecm.automation.OperationContext;
046import org.nuxeo.ecm.core.api.CoreSession;
047import org.nuxeo.ecm.core.api.NuxeoException;
048import org.nuxeo.runtime.api.Framework;
049import jdk.nashorn.api.scripting.ClassFilter;
050import jdk.nashorn.api.scripting.NashornScriptEngineFactory;
051import jdk.nashorn.api.scripting.ScriptObjectMirror;
052
053public class AutomationScriptingServiceImpl implements AutomationScriptingService {
054
055    private static final Log log = LogFactory.getLog(AutomationScriptingServiceImpl.class);
056
057    protected final ScriptEngine engine = getScriptEngine();
058
059    protected AutomationScriptingParamsInjector paramsInjector;
060
061    // updated in-place only by extension points, so no concurrency issues
062    protected Set<String> allowedClassNames = new HashSet<>();
063
064    @Override
065    public Session get(CoreSession session) {
066        return get(new OperationContext(session));
067    }
068
069    @Override
070    public Session get(OperationContext context) {
071        return new Bridge(context);
072    }
073
074    class Bridge implements Session {
075
076        final CompiledScript mapperScript = AutomationMapper.compile((Compilable) engine);
077
078        final Compilable compilable = ((Compilable) engine);
079
080        final Invocable invocable = ((Invocable) engine);
081
082        final ScriptContext scriptContext = engine.getContext();
083
084        final AutomationMapper mapper;
085
086        final ScriptObjectMirror global;
087
088        Bridge(OperationContext operationContext) {
089            mapper = new AutomationMapper(operationContext);
090            try {
091                mapperScript.eval(mapper);
092            } catch (ScriptException cause) {
093                throw new NuxeoException("Cannot execute mapper " + mapperScript, cause);
094            }
095            global = (ScriptObjectMirror) mapper.get("nashorn.global");
096            scriptContext.setBindings(mapper, ScriptContext.ENGINE_SCOPE);
097        }
098
099        @Override
100        public <T> T handleof(InputStream input, Class<T> typeof) {
101            run(input);
102            T handle = invocable.getInterface(global, typeof);
103            if (handle == null) {
104                throw new NuxeoException("Script doesn't implements " + typeof.getName());
105            }
106            return typeof.cast(Proxy.newProxyInstance(Thread.currentThread().getContextClassLoader(),
107                    new Class[] { typeof }, new InvocationHandler() {
108
109                        @Override
110                        public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
111                            return mapper.unwrap(method.invoke(handle, mapper.wrap(args[0]), mapper.wrap(args[1])));
112                        }
113                    }));
114        }
115
116        @Override
117        public Object run(InputStream input) {
118            try {
119                return mapper.unwrap(engine.eval(new InputStreamReader(input), mapper));
120            } catch (ScriptException cause) {
121                throw new NuxeoException("Cannot evaluate automation script", cause);
122            }
123        }
124
125        <T> T handleof(Class<T> typeof) {
126            return invocable.getInterface(global, typeof);
127        }
128
129        @Override
130        public <T> T adapt(Class<T> typeof) {
131            if (typeof.isAssignableFrom(engine.getClass())) {
132                return typeof.cast(engine);
133            }
134            if (typeof.isAssignableFrom(AutomationMapper.class)) {
135                return typeof.cast(mapper);
136            }
137            if (typeof.isAssignableFrom(scriptContext.getClass())) {
138                return typeof.cast(scriptContext);
139            }
140            throw new IllegalArgumentException("Cannot adapt scripting context to " + typeof.getName());
141        }
142
143        @Override
144        public void close() throws Exception {
145            mapper.flush();
146        }
147    }
148
149    protected ScriptEngine getScriptEngine() {
150        String version = Framework.getProperty("java.version");
151        // Check if jdk8
152        if (!checkJavaVersion(version, NASHORN_JAVA_VERSION)) {
153            throw new UnsupportedOperationException(NASHORN_JAVA_VERSION);
154        }
155        // Check if version < jdk8u25 -> no cache.
156        if (!checkJavaVersion(version, COMPLIANT_JAVA_VERSION_CACHE)) {
157            log.warn(NASHORN_WARN_CACHE);
158            return getScriptEngine(false, false);
159        }
160        boolean cache = Boolean.parseBoolean(
161                Framework.getProperty(AUTOMATION_SCRIPTING_PRECOMPILE, DEFAULT_PRECOMPILE_STATUS));
162        // Check if jdk8u25 <= version < jdk8u40 -> only cache.
163        if (!checkJavaVersion(version, COMPLIANT_JAVA_VERSION_CLASS_FILTER)) {
164            log.warn(NASHORN_WARN_CLASS_FILTER);
165            return getScriptEngine(cache, false);
166        }
167        // Else if version >= jdk8u40 -> cache + class filter
168        try {
169            return getScriptEngine(cache, true);
170        } catch (NoClassDefFoundError cause) {
171            log.warn(NASHORN_WARN_CLASS_FILTER);
172            return getScriptEngine(cache, false);
173        }
174    }
175
176    protected ScriptEngine getScriptEngine(boolean cache, boolean filter) {
177        NashornScriptEngineFactory nashorn = new NashornScriptEngineFactory();
178        String[] args = cache
179                ? new String[] { "-strict", "--optimistic-types=true", "--persistent-code-cache",
180                        "--class-cache-size=50" }
181                : new String[] { "-strict" };
182        ClassLoader classLoader = Thread.currentThread().getContextClassLoader();
183        ClassFilter classFilter = filter ? getClassFilter() : null;
184        return nashorn.getScriptEngine(args, classLoader, classFilter);
185    }
186
187    protected ClassFilter getClassFilter() {
188        return className -> allowedClassNames.contains(className);
189    }
190
191}