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