001    /**
002     * Copyright (c) 2000-present Liferay, Inc. All rights reserved.
003     *
004     * This library is free software; you can redistribute it and/or modify it under
005     * the terms of the GNU Lesser General Public License as published by the Free
006     * Software Foundation; either version 2.1 of the License, or (at your option)
007     * any later version.
008     *
009     * This library is distributed in the hope that it will be useful, but WITHOUT
010     * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
011     * FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public License for more
012     * details.
013     */
014    
015    package com.liferay.portal.scripting.ruby;
016    
017    import com.liferay.portal.kernel.log.Log;
018    import com.liferay.portal.kernel.log.LogFactoryUtil;
019    import com.liferay.portal.kernel.scripting.BaseScriptingExecutor;
020    import com.liferay.portal.kernel.scripting.ExecutionException;
021    import com.liferay.portal.kernel.scripting.ScriptingContainer;
022    import com.liferay.portal.kernel.scripting.ScriptingException;
023    import com.liferay.portal.kernel.scripting.ScriptingExecutor;
024    import com.liferay.portal.kernel.util.ArrayUtil;
025    import com.liferay.portal.kernel.util.ClassLoaderUtil;
026    import com.liferay.portal.kernel.util.FileUtil;
027    import com.liferay.portal.kernel.util.NamedThreadFactory;
028    import com.liferay.portal.kernel.util.ReflectionUtil;
029    import com.liferay.portal.kernel.util.StringPool;
030    import com.liferay.portal.kernel.util.SystemProperties;
031    import com.liferay.portal.util.PropsValues;
032    
033    import java.io.File;
034    import java.io.FileInputStream;
035    import java.io.FileNotFoundException;
036    import java.io.IOException;
037    
038    import java.lang.reflect.Field;
039    
040    import java.util.ArrayList;
041    import java.util.HashMap;
042    import java.util.List;
043    import java.util.Map;
044    import java.util.Set;
045    import java.util.concurrent.Callable;
046    import java.util.concurrent.FutureTask;
047    import java.util.concurrent.ThreadFactory;
048    
049    import javax.servlet.ServletContext;
050    
051    import jodd.io.ZipUtil;
052    
053    import org.jruby.Ruby;
054    import org.jruby.RubyInstanceConfig;
055    import org.jruby.RubyInstanceConfig.CompileMode;
056    import org.jruby.embed.LocalContextScope;
057    import org.jruby.embed.internal.LocalContextProvider;
058    import org.jruby.exceptions.RaiseException;
059    
060    /**
061     * @author Alberto Montero
062     * @author Raymond Aug??
063     */
064    public class RubyExecutor extends BaseScriptingExecutor {
065    
066            public static final String LANGUAGE = "ruby";
067    
068            public static void initRubyGems(ServletContext servletContext) {
069                    File rubyGemsJarFile = new File(
070                            servletContext.getRealPath("/WEB-INF/lib/ruby-gems.jar"));
071    
072                    if (!rubyGemsJarFile.exists()) {
073                            if (_log.isWarnEnabled()) {
074                                    _log.warn(rubyGemsJarFile + " does not exist");
075                            }
076    
077                            return;
078                    }
079    
080                    String tmpDir = SystemProperties.get(SystemProperties.TMP_DIR);
081    
082                    File rubyDir = new File(tmpDir + "/liferay/ruby");
083    
084                    if (!rubyDir.exists() ||
085                            (rubyDir.lastModified() < rubyGemsJarFile.lastModified())) {
086    
087                            FileUtil.deltree(rubyDir);
088    
089                            try {
090                                    FileUtil.mkdirs(rubyDir);
091    
092                                    ZipUtil.unzip(rubyGemsJarFile, rubyDir);
093    
094                                    rubyDir.setLastModified(rubyGemsJarFile.lastModified());
095                            }
096                            catch (IOException ioe) {
097                                    _log.error(
098                                            "Unable to unzip " + rubyGemsJarFile + " to " + rubyDir,
099                                            ioe);
100                            }
101                    }
102            }
103    
104            public RubyExecutor() {
105                    initScriptingExecutorClassLoader();
106    
107                    org.jruby.embed.ScriptingContainer scriptingContainer =
108                            new org.jruby.embed.ScriptingContainer(
109                                    LocalContextScope.THREADSAFE);
110    
111                    _scriptingContainer = new RubyScriptingContainer(scriptingContainer);
112    
113                    LocalContextProvider localContextProvider =
114                            scriptingContainer.getProvider();
115    
116                    RubyInstanceConfig rubyInstanceConfig =
117                            localContextProvider.getRubyInstanceConfig();
118    
119                    if (PropsValues.SCRIPTING_JRUBY_COMPILE_MODE.equals(
120                                    _COMPILE_MODE_FORCE)) {
121    
122                            rubyInstanceConfig.setCompileMode(CompileMode.FORCE);
123                    }
124                    else if (PropsValues.SCRIPTING_JRUBY_COMPILE_MODE.equals(
125                                            _COMPILE_MODE_JIT)) {
126    
127                            rubyInstanceConfig.setCompileMode(CompileMode.JIT);
128                    }
129    
130                    rubyInstanceConfig.setJitThreshold(
131                            PropsValues.SCRIPTING_JRUBY_COMPILE_THRESHOLD);
132                    rubyInstanceConfig.setLoader(ClassLoaderUtil.getPortalClassLoader());
133    
134                    _basePath = PropsValues.LIFERAY_LIB_PORTAL_DIR;
135    
136                    _loadPaths = new ArrayList<>(
137                            PropsValues.SCRIPTING_JRUBY_LOAD_PATHS.length);
138    
139                    for (String gemLibPath : PropsValues.SCRIPTING_JRUBY_LOAD_PATHS) {
140                            _loadPaths.add(gemLibPath);
141                    }
142    
143                    rubyInstanceConfig.setLoadPaths(_loadPaths);
144    
145                    scriptingContainer.setCurrentDirectory(_basePath);
146            }
147    
148            public void destroy() {
149                    _scriptingContainer.destroy();
150            }
151    
152            @Override
153            public Map<String, Object> eval(
154                            Set<String> allowedClasses, Map<String, Object> inputObjects,
155                            Set<String> outputNames, File scriptFile,
156                            ClassLoader... classLoaders)
157                    throws ScriptingException {
158    
159                    return eval(
160                            allowedClasses, inputObjects, outputNames, scriptFile, null,
161                            classLoaders);
162            }
163    
164            @Override
165            public Map<String, Object> eval(
166                            Set<String> allowedClasses, Map<String, Object> inputObjects,
167                            Set<String> outputNames, String script, ClassLoader... classLoaders)
168                    throws ScriptingException {
169    
170                    return eval(
171                            allowedClasses, inputObjects, outputNames, null, script,
172                            classLoaders);
173            }
174    
175            @Override
176            public String getLanguage() {
177                    return LANGUAGE;
178            }
179    
180            @Override
181            public ScriptingContainer<?> getScriptingContainer() {
182                    return _scriptingContainer;
183            }
184    
185            @Override
186            public ScriptingExecutor newInstance(boolean executeInSeparateThread) {
187                    RubyExecutor rubyExecutor = new RubyExecutor();
188    
189                    rubyExecutor.setExecuteInSeparateThread(executeInSeparateThread);
190    
191                    return rubyExecutor;
192            }
193    
194            public void setExecuteInSeparateThread(boolean executeInSeparateThread) {
195                    _executeInSeparateThread = executeInSeparateThread;
196            }
197    
198            protected Map<String, Object> doEval(
199                            Set<String> allowedClasses, Map<String, Object> inputObjects,
200                            Set<String> outputNames, File scriptFile, String script,
201                            ClassLoader... classLoaders)
202                    throws ScriptingException {
203    
204                    if (allowedClasses != null) {
205                            throw new ExecutionException(
206                                    "Constrained execution not supported for Ruby");
207                    }
208    
209                    org.jruby.embed.ScriptingContainer scriptingContainer =
210                            _scriptingContainer.getWrappedScriptingContainer();
211    
212                    try {
213                            LocalContextProvider localContextProvider =
214                                    scriptingContainer.getProvider();
215    
216                            RubyInstanceConfig rubyInstanceConfig =
217                                    localContextProvider.getRubyInstanceConfig();
218    
219                            rubyInstanceConfig.setCurrentDirectory(_basePath);
220    
221                            if (ArrayUtil.isNotEmpty(classLoaders)) {
222                                    rubyInstanceConfig.setLoader(
223                                            getAggregateClassLoader(classLoaders));
224                            }
225    
226                            rubyInstanceConfig.setLoadPaths(_loadPaths);
227    
228                            for (Map.Entry<String, Object> entry : inputObjects.entrySet()) {
229                                    String inputName = entry.getKey();
230                                    Object inputObject = entry.getValue();
231    
232                                    if (!inputName.startsWith(StringPool.DOLLAR)) {
233                                            inputName = StringPool.DOLLAR + inputName;
234                                    }
235    
236                                    scriptingContainer.put(inputName, inputObject);
237                            }
238    
239                            if (scriptFile != null) {
240                                    scriptingContainer.runScriptlet(
241                                            new FileInputStream(scriptFile), scriptFile.toString());
242                            }
243                            else {
244                                    _scriptingContainer.runScriptlet(script);
245                            }
246    
247                            if (outputNames == null) {
248                                    return null;
249                            }
250    
251                            Map<String, Object> outputObjects = new HashMap<>();
252    
253                            for (String outputName : outputNames) {
254                                    outputObjects.put(
255                                            outputName, scriptingContainer.get(outputName));
256                            }
257    
258                            return outputObjects;
259                    }
260                    catch (RaiseException re) {
261                            throw new ScriptingException(
262                                    re.getException().message.asJavaString() + "\n\n", re);
263                    }
264                    catch (FileNotFoundException fnfe) {
265                            throw new ScriptingException(fnfe);
266                    }
267                    finally {
268                            try {
269                                    _globalRuntimeField.set(null, null);
270                            }
271                            catch (Exception e) {
272                                    _log.error(e, e);
273                            }
274                    }
275            }
276    
277            protected Map<String, Object> eval(
278                            Set<String> allowedClasses, Map<String, Object> inputObjects,
279                            Set<String> outputNames, File scriptFile, String script,
280                            ClassLoader... classLoaders)
281                    throws ScriptingException {
282    
283                    if (!_executeInSeparateThread) {
284                            return doEval(
285                                    allowedClasses, inputObjects, outputNames, scriptFile, script,
286                                    classLoaders);
287                    }
288    
289                    EvalCallable evalCallable = new EvalCallable(
290                            allowedClasses, inputObjects, outputNames, scriptFile, script,
291                            classLoaders);
292    
293                    FutureTask<Map<String, Object>> futureTask = new FutureTask<>(
294                            evalCallable);
295    
296                    Thread oneTimeExecutorThread = _threadFactory.newThread(futureTask);
297    
298                    oneTimeExecutorThread.start();
299    
300                    try {
301                            oneTimeExecutorThread.join();
302    
303                            return futureTask.get();
304                    }
305                    catch (Exception e) {
306                            futureTask.cancel(true);
307                            oneTimeExecutorThread.interrupt();
308    
309                            throw new ScriptingException(e);
310                    }
311            }
312    
313            private static final String _COMPILE_MODE_FORCE = "force";
314    
315            private static final String _COMPILE_MODE_JIT = "jit";
316    
317            private static final Log _log = LogFactoryUtil.getLog(RubyExecutor.class);
318    
319            private static final Field _globalRuntimeField;
320            private static final ThreadFactory _threadFactory = new NamedThreadFactory(
321                    RubyExecutor.class.getName(), Thread.NORM_PRIORITY,
322                    RubyExecutor.class.getClassLoader());
323    
324            static {
325                    try {
326                            _globalRuntimeField = ReflectionUtil.getDeclaredField(
327                                    Ruby.class, "globalRuntime");
328                    }
329                    catch (Exception e) {
330                            throw new ExceptionInInitializerError(e);
331                    }
332            }
333    
334            private final String _basePath;
335            private boolean _executeInSeparateThread = true;
336            private final List<String> _loadPaths;
337            private final ScriptingContainer<org.jruby.embed.ScriptingContainer>
338                    _scriptingContainer;
339    
340            private class EvalCallable implements Callable<Map<String, Object>> {
341    
342                    public EvalCallable(
343                            Set<String> allowedClasses, Map<String, Object> inputObjects,
344                            Set<String> outputNames, File scriptFile, String script,
345                            ClassLoader[] classLoaders) {
346    
347                            _allowedClasses = allowedClasses;
348                            _inputObjects = inputObjects;
349                            _outputNames = outputNames;
350                            _scriptFile = scriptFile;
351                            _script = script;
352                            _classLoaders = classLoaders;
353                    }
354    
355                    @Override
356                    public Map<String, Object> call() throws Exception {
357                            return doEval(
358                                    _allowedClasses, _inputObjects, _outputNames, _scriptFile,
359                                    _script, _classLoaders);
360                    }
361    
362                    private final Set<String> _allowedClasses;
363                    private final ClassLoader[] _classLoaders;
364                    private final Map<String, Object> _inputObjects;
365                    private final Set<String> _outputNames;
366                    private final String _script;
367                    private final File _scriptFile;
368    
369            }
370    
371    }