/** * Copyright (c) 2000-present Liferay, Inc. All rights reserved. * * This library is free software; you can redistribute it and/or modify it under * the terms of the GNU Lesser General Public License as published by the Free * Software Foundation; either version 2.1 of the License, or (at your option) * any later version. * * This library is distributed in the hope that it will be useful, but WITHOUT * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS * FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public License for more * details. */ package com.liferay.portal.scripting.ruby.internal; import com.liferay.portal.configuration.metatype.bnd.util.ConfigurableUtil; import com.liferay.portal.kernel.log.Log; import com.liferay.portal.kernel.log.LogFactoryUtil; import com.liferay.portal.kernel.scripting.ExecutionException; import com.liferay.portal.kernel.scripting.ScriptingContainer; import com.liferay.portal.kernel.scripting.ScriptingException; import com.liferay.portal.kernel.scripting.ScriptingExecutor; import com.liferay.portal.kernel.util.NamedThreadFactory; import com.liferay.portal.kernel.util.Props; import com.liferay.portal.kernel.util.PropsKeys; import com.liferay.portal.kernel.util.ReflectionUtil; import com.liferay.portal.kernel.util.StringPool; import com.liferay.portal.scripting.BaseScriptingExecutor; import com.liferay.portal.scripting.ruby.configuration.RubyScriptingConfiguration; import java.io.File; import java.io.FileInputStream; import java.io.FileNotFoundException; import java.lang.reflect.Field; import java.util.ArrayList; import java.util.Arrays; import java.util.HashMap; import java.util.List; import java.util.Map; import java.util.Set; import java.util.concurrent.Callable; import java.util.concurrent.FutureTask; import java.util.concurrent.ThreadFactory; import org.jruby.Ruby; import org.jruby.RubyException; import org.jruby.RubyInstanceConfig; import org.jruby.RubyInstanceConfig.CompileMode; import org.jruby.embed.LocalContextScope; import org.jruby.embed.internal.LocalContextProvider; import org.jruby.exceptions.RaiseException; import org.osgi.service.component.annotations.Activate; import org.osgi.service.component.annotations.Component; import org.osgi.service.component.annotations.Deactivate; import org.osgi.service.component.annotations.Reference; /** * @author Alberto Montero * @author Raymond Augé */ @Component( configurationPid = "com.liferay.portal.scripting.ruby.configuration.RubyScriptingConfiguration", immediate = true, property = {"scripting.language=" + RubyExecutor.LANGUAGE}, service = ScriptingExecutor.class ) public class RubyExecutor extends BaseScriptingExecutor { public static final String LANGUAGE = "ruby"; @Override public Map<String, Object> eval( Set<String> allowedClasses, Map<String, Object> inputObjects, Set<String> outputNames, File scriptFile) throws ScriptingException { return eval( allowedClasses, inputObjects, outputNames, scriptFile, (String)null); } @Override public Map<String, Object> eval( Set<String> allowedClasses, Map<String, Object> inputObjects, Set<String> outputNames, String script) throws ScriptingException { return eval(allowedClasses, inputObjects, outputNames, null, script); } @Override public String getLanguage() { return LANGUAGE; } @Override public ScriptingContainer<?> getScriptingContainer() { return _scriptingContainer; } @Override public ScriptingExecutor newInstance(boolean executeInSeparateThread) { RubyExecutor rubyExecutor = new RubyExecutor(); rubyExecutor.setExecuteInSeparateThread(executeInSeparateThread); return rubyExecutor; } public void setExecuteInSeparateThread(boolean executeInSeparateThread) { _executeInSeparateThread = executeInSeparateThread; } @Activate protected void activate(Map<String, Object> properties) { _rubyScriptingConfiguration = ConfigurableUtil.createConfigurable( RubyScriptingConfiguration.class, properties); initialize(); } @Deactivate protected void deactivate() { _scriptingContainer.destroy(); } protected Map<String, Object> doEval( Set<String> allowedClasses, Map<String, Object> inputObjects, Set<String> outputNames, File scriptFile, String script) throws ScriptingException { if (allowedClasses != null) { throw new ExecutionException( "Constrained execution not supported for Ruby"); } org.jruby.embed.ScriptingContainer scriptingContainer = _scriptingContainer.getWrappedScriptingContainer(); try { LocalContextProvider localContextProvider = scriptingContainer.getProvider(); RubyInstanceConfig rubyInstanceConfig = localContextProvider.getRubyInstanceConfig(); rubyInstanceConfig.setCurrentDirectory(_basePath); rubyInstanceConfig.setLoader(getClassLoader()); rubyInstanceConfig.setLoadPaths(_loadPaths); for (Map.Entry<String, Object> entry : inputObjects.entrySet()) { String inputName = entry.getKey(); Object inputObject = entry.getValue(); if (!inputName.startsWith(StringPool.DOLLAR)) { inputName = StringPool.DOLLAR + inputName; } scriptingContainer.put(inputName, inputObject); } if (scriptFile != null) { scriptingContainer.runScriptlet( new FileInputStream(scriptFile), scriptFile.toString()); } else { _scriptingContainer.runScriptlet(script); } if (outputNames == null) { return null; } Map<String, Object> outputObjects = new HashMap<>(); for (String outputName : outputNames) { outputObjects.put( outputName, scriptingContainer.get(outputName)); } return outputObjects; } catch (RaiseException re) { RubyException rubyException = re.getException(); throw new ScriptingException( rubyException.message.asJavaString() + "\n\n", re); } catch (FileNotFoundException fnfe) { throw new ScriptingException(fnfe); } finally { try { _globalRuntimeField.set(null, null); } catch (Exception e) { _log.error(e, e); } } } protected Map<String, Object> eval( Set<String> allowedClasses, Map<String, Object> inputObjects, Set<String> outputNames, File scriptFile, String script) throws ScriptingException { if (!_executeInSeparateThread) { return doEval( allowedClasses, inputObjects, outputNames, scriptFile, script); } EvalCallable evalCallable = new EvalCallable( allowedClasses, inputObjects, outputNames, scriptFile, script); FutureTask<Map<String, Object>> futureTask = new FutureTask<>( evalCallable); Thread oneTimeExecutorThread = _threadFactory.newThread(futureTask); oneTimeExecutorThread.start(); try { oneTimeExecutorThread.join(); return futureTask.get(); } catch (Exception e) { futureTask.cancel(true); oneTimeExecutorThread.interrupt(); throw new ScriptingException(e); } } protected void initialize() { org.jruby.embed.ScriptingContainer scriptingContainer = new org.jruby.embed.ScriptingContainer( LocalContextScope.THREADSAFE); _scriptingContainer = new RubyScriptingContainer(scriptingContainer); LocalContextProvider localContextProvider = scriptingContainer.getProvider(); RubyInstanceConfig rubyInstanceConfig = localContextProvider.getRubyInstanceConfig(); String compileMode = _rubyScriptingConfiguration.compileMode(); if (compileMode.equals(_COMPILE_MODE_FORCE)) { rubyInstanceConfig.setCompileMode(CompileMode.FORCE); } else if (compileMode.equals(_COMPILE_MODE_JIT)) { rubyInstanceConfig.setCompileMode(CompileMode.JIT); } rubyInstanceConfig.setJitThreshold( _rubyScriptingConfiguration.compileThreshold()); rubyInstanceConfig.setLoader(getClassLoader()); _loadPaths = new ArrayList<>( Arrays.asList(_rubyScriptingConfiguration.loadPaths())); rubyInstanceConfig.setLoadPaths(_loadPaths); scriptingContainer.setCurrentDirectory(_basePath); } @Reference(unbind = "-") protected void setProps(Props props) { _basePath = props.get(PropsKeys.LIFERAY_LIB_PORTAL_DIR); } private static final String _COMPILE_MODE_FORCE = "force"; private static final String _COMPILE_MODE_JIT = "jit"; private static final Log _log = LogFactoryUtil.getLog(RubyExecutor.class); private static final Field _globalRuntimeField; private static final ThreadFactory _threadFactory = new NamedThreadFactory( RubyExecutor.class.getName(), Thread.NORM_PRIORITY, RubyExecutor.class.getClassLoader()); static { try { _globalRuntimeField = ReflectionUtil.getDeclaredField( Ruby.class, "globalRuntime"); } catch (Exception e) { throw new ExceptionInInitializerError(e); } } private String _basePath; private boolean _executeInSeparateThread = true; private List<String> _loadPaths; private volatile RubyScriptingConfiguration _rubyScriptingConfiguration; private ScriptingContainer<org.jruby.embed.ScriptingContainer> _scriptingContainer; private class EvalCallable implements Callable<Map<String, Object>> { public EvalCallable( Set<String> allowedClasses, Map<String, Object> inputObjects, Set<String> outputNames, File scriptFile, String script) { _allowedClasses = allowedClasses; _inputObjects = inputObjects; _outputNames = outputNames; _scriptFile = scriptFile; _script = script; } @Override public Map<String, Object> call() throws Exception { return doEval( _allowedClasses, _inputObjects, _outputNames, _scriptFile, _script); } private final Set<String> _allowedClasses; private final Map<String, Object> _inputObjects; private final Set<String> _outputNames; private final String _script; private final File _scriptFile; } }