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.function.Supplier;
034
035import javax.script.Compilable;
036import javax.script.CompiledScript;
037import javax.script.Invocable;
038import javax.script.ScriptContext;
039import javax.script.ScriptEngine;
040import javax.script.ScriptException;
041import org.apache.commons.logging.Log;
042import org.apache.commons.logging.LogFactory;
043import org.nuxeo.automation.scripting.api.AutomationScriptingService;
044import org.nuxeo.ecm.automation.OperationContext;
045import org.nuxeo.ecm.core.api.CoreSession;
046import org.nuxeo.ecm.core.api.NuxeoException;
047import org.nuxeo.runtime.api.Framework;
048import jdk.nashorn.api.scripting.ClassFilter;
049import jdk.nashorn.api.scripting.NashornScriptEngineFactory;
050import jdk.nashorn.api.scripting.ScriptObjectMirror;
051
052public class AutomationScriptingServiceImpl implements AutomationScriptingService {
053
054    private final Supplier<ScriptEngine> supplier = new Factory().supplier;
055
056    protected AutomationScriptingParamsInjector paramsInjector;
057
058    @Override
059    public Session get(CoreSession session) {
060        return get(new OperationContext(session));
061    }
062
063    @Override
064    public Session get(OperationContext context) {
065        return new Bridge(context);
066    }
067
068
069    final ScriptEngine engine = supplier.get();
070
071    class Bridge implements Session {
072
073        final CompiledScript mapperScript = AutomationMapper.compile((Compilable) engine);
074
075        final Compilable compilable = ((Compilable) engine);
076
077        final Invocable invocable = ((Invocable) engine);
078
079        final ScriptContext scriptContext = engine.getContext();
080
081        final AutomationMapper mapper;
082
083        final ScriptObjectMirror global;
084
085        Bridge(OperationContext operationContext) {
086            mapper = new AutomationMapper(operationContext);
087            try {
088                mapperScript.eval(mapper);
089            } catch (ScriptException cause) {
090                throw new NuxeoException("Cannot execute mapper " + mapperScript, cause);
091            }
092            global = (ScriptObjectMirror) mapper.get("nashorn.global");
093            scriptContext.setBindings(mapper, ScriptContext.ENGINE_SCOPE);
094        }
095
096        @Override
097        public <T> T handleof(InputStream input, Class<T> typeof) {
098            run(input);
099            T handle = invocable.getInterface(global, typeof);
100            if (handle == null) {
101                throw new NuxeoException("Script doesn't implements " + typeof.getName());
102            }
103            return typeof.cast(Proxy.newProxyInstance(Thread.currentThread().getContextClassLoader(),
104                    new Class[] { typeof }, new InvocationHandler() {
105
106                        @Override
107                        public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
108                            return mapper.unwrap(method.invoke(handle, mapper.wrap(args[0]), mapper.wrap(args[1])));
109                        }
110                    }));
111        }
112
113        @Override
114        public Object run(InputStream input) {
115            try {
116                return mapper.unwrap(engine.eval(new InputStreamReader(input), mapper));
117            } catch (ScriptException cause) {
118                throw new NuxeoException("Cannot evaluate automation script", cause);
119            }
120        }
121
122        <T> T handleof(Class<T> typeof) {
123            return invocable.getInterface(global, typeof);
124        }
125
126        @Override
127        public <T> T adapt(Class<T> typeof) {
128            if (typeof.isAssignableFrom(engine.getClass())) {
129                return typeof.cast(engine);
130            }
131            if (typeof.isAssignableFrom(AutomationMapper.class)) {
132                return typeof.cast(mapper);
133            }
134            if (typeof.isAssignableFrom(scriptContext.getClass())) {
135                return typeof.cast(scriptContext);
136            }
137            throw new IllegalArgumentException("Cannot adapt scripting context to " + typeof.getName());
138        }
139
140        @Override
141        public void close() throws Exception {
142            mapper.flush();
143        }
144    }
145
146    class Factory {
147
148        final NashornScriptEngineFactory nashorn = new NashornScriptEngineFactory();
149
150        final Supplier<ScriptEngine> supplier = supplier();
151
152        Supplier<ScriptEngine> supplier() {
153
154            Log log = LogFactory.getLog(AutomationScriptingServiceImpl.class);
155            String version = Framework.getProperty("java.version");
156            // Check if jdk8
157            if (!checkJavaVersion(version, NASHORN_JAVA_VERSION)) {
158                throw new UnsupportedOperationException(NASHORN_JAVA_VERSION);
159            }
160            // Check if version < jdk8u25 -> no cache.
161            if (!checkJavaVersion(version, COMPLIANT_JAVA_VERSION_CACHE)) {
162                log.warn(NASHORN_WARN_CACHE);
163                return noCache();
164            }
165            // Check if jdk8u25 <= version < jdk8u40 -> only cache.
166            if (!checkJavaVersion(version, COMPLIANT_JAVA_VERSION_CLASS_FILTER)) {
167                if (Boolean.parseBoolean(
168                        Framework.getProperty(AUTOMATION_SCRIPTING_PRECOMPILE, DEFAULT_PRECOMPILE_STATUS))) {
169                    log.warn(NASHORN_WARN_CLASS_FILTER);
170                    return cache();
171                } else {
172                    log.warn(NASHORN_WARN_CLASS_FILTER);
173                    return noCache();
174                }
175            }
176            // Else if version >= jdk8u40 -> cache + class filter
177            try {
178                if (Boolean.parseBoolean(
179                        Framework.getProperty(AUTOMATION_SCRIPTING_PRECOMPILE, DEFAULT_PRECOMPILE_STATUS))) {
180                    return cacheAndClassFilter();
181                } else {
182                    return noCacheAndClassFilter();
183                }
184            } catch (NoClassDefFoundError cause) {
185                log.warn(NASHORN_WARN_CLASS_FILTER);
186                return cache();
187            }
188        }
189
190        Supplier<ScriptEngine> noCache() {
191            return () -> nashorn.getScriptEngine(new String[] { "-strict" });
192        }
193
194        Supplier<ScriptEngine> cache() {
195            return () -> nashorn.getScriptEngine(
196                    new String[] { "-strict", "--optimistic-types=true", "--persistent-code-cache", "--class-cache-size=50" },
197                    Thread.currentThread().getContextClassLoader());
198        }
199
200        Supplier<ScriptEngine> cacheAndClassFilter() {
201            return () -> nashorn.getScriptEngine(
202                    new String[] { "-strict", "--optimistic-types=true", "--persistent-code-cache", "--class-cache-size=50" },
203                    Thread.currentThread().getContextClassLoader(), new ClassFilter() {
204
205                        @Override
206                        public boolean exposeToScripts(String className) {
207                            return false;
208                        }
209
210                    });
211        }
212
213        Supplier<ScriptEngine> noCacheAndClassFilter() {
214            return () -> nashorn.getScriptEngine(new String[] { "-strict" },
215                    Thread.currentThread().getContextClassLoader(), new ClassFilter() {
216
217                        @Override
218                        public boolean exposeToScripts(String className) {
219                            return false;
220                        }
221
222                    });
223        };
224    }
225
226}