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.ScriptingException;
022    import com.liferay.portal.kernel.util.AggregateClassLoader;
023    import com.liferay.portal.kernel.util.ArrayUtil;
024    import com.liferay.portal.kernel.util.FileUtil;
025    import com.liferay.portal.kernel.util.NamedThreadFactory;
026    import com.liferay.portal.kernel.util.ReflectionUtil;
027    import com.liferay.portal.kernel.util.StringPool;
028    import com.liferay.portal.kernel.util.SystemProperties;
029    import com.liferay.portal.util.ClassLoaderUtil;
030    import com.liferay.portal.util.PropsValues;
031    
032    import java.io.File;
033    import java.io.FileInputStream;
034    import java.io.FileNotFoundException;
035    
036    import java.lang.reflect.Field;
037    
038    import java.util.ArrayList;
039    import java.util.HashMap;
040    import java.util.List;
041    import java.util.Map;
042    import java.util.Set;
043    import java.util.concurrent.Callable;
044    import java.util.concurrent.FutureTask;
045    import java.util.concurrent.ThreadFactory;
046    
047    import jodd.io.ZipUtil;
048    
049    import org.jruby.Ruby;
050    import org.jruby.RubyInstanceConfig;
051    import org.jruby.RubyInstanceConfig.CompileMode;
052    import org.jruby.embed.LocalContextScope;
053    import org.jruby.embed.ScriptingContainer;
054    import org.jruby.embed.internal.LocalContextProvider;
055    import org.jruby.exceptions.RaiseException;
056    
057    /**
058     * @author Alberto Montero
059     * @author Raymond Aug??
060     */
061    public class RubyExecutor extends BaseScriptingExecutor {
062    
063            public static final String LANGUAGE = "ruby";
064    
065            public RubyExecutor() {
066                    try {
067                            initRubyGems();
068                    }
069                    catch (Exception e) {
070                            _log.error(e, e);
071                    }
072    
073                    _scriptingContainer = new ScriptingContainer(
074                            LocalContextScope.THREADSAFE);
075    
076                    LocalContextProvider localContextProvider =
077                            _scriptingContainer.getProvider();
078    
079                    RubyInstanceConfig rubyInstanceConfig =
080                            localContextProvider.getRubyInstanceConfig();
081    
082                    if (PropsValues.SCRIPTING_JRUBY_COMPILE_MODE.equals(
083                                    _COMPILE_MODE_FORCE)) {
084    
085                            rubyInstanceConfig.setCompileMode(CompileMode.FORCE);
086                    }
087                    else if (PropsValues.SCRIPTING_JRUBY_COMPILE_MODE.equals(
088                                            _COMPILE_MODE_JIT)) {
089    
090                            rubyInstanceConfig.setCompileMode(CompileMode.JIT);
091                    }
092    
093                    rubyInstanceConfig.setJitThreshold(
094                            PropsValues.SCRIPTING_JRUBY_COMPILE_THRESHOLD);
095                    rubyInstanceConfig.setLoader(ClassLoaderUtil.getPortalClassLoader());
096    
097                    _basePath = PropsValues.LIFERAY_LIB_PORTAL_DIR;
098    
099                    _loadPaths = new ArrayList<String>(
100                            PropsValues.SCRIPTING_JRUBY_LOAD_PATHS.length);
101    
102                    for (String gemLibPath : PropsValues.SCRIPTING_JRUBY_LOAD_PATHS) {
103                            _loadPaths.add(gemLibPath);
104                    }
105    
106                    rubyInstanceConfig.setLoadPaths(_loadPaths);
107    
108                    _scriptingContainer.setCurrentDirectory(_basePath);
109            }
110    
111            @Override
112            public Map<String, Object> eval(
113                            Set<String> allowedClasses, Map<String, Object> inputObjects,
114                            Set<String> outputNames, File scriptFile,
115                            ClassLoader... classLoaders)
116                    throws ScriptingException {
117    
118                    return eval(
119                            allowedClasses, inputObjects, outputNames, scriptFile, null,
120                            classLoaders);
121            }
122    
123            @Override
124            public Map<String, Object> eval(
125                            Set<String> allowedClasses, Map<String, Object> inputObjects,
126                            Set<String> outputNames, String script, ClassLoader... classLoaders)
127                    throws ScriptingException {
128    
129                    return eval(
130                            allowedClasses, inputObjects, outputNames, null, script,
131                            classLoaders);
132            }
133    
134            @Override
135            public String getLanguage() {
136                    return LANGUAGE;
137            }
138    
139            public ScriptingContainer getScriptingContainer() {
140                    return _scriptingContainer;
141            }
142    
143            public void setExecuteInSeparateThread(boolean executeInSeparateThread) {
144                    _executeInSeparateThread = executeInSeparateThread;
145            }
146    
147            protected Map<String, Object> doEval(
148                            Set<String> allowedClasses, Map<String, Object> inputObjects,
149                            Set<String> outputNames, File scriptFile, String script,
150                            ClassLoader... classLoaders)
151                    throws ScriptingException {
152    
153                    if (allowedClasses != null) {
154                            throw new ExecutionException(
155                                    "Constrained execution not supported for Ruby");
156                    }
157    
158                    try {
159                            LocalContextProvider localContextProvider =
160                                    _scriptingContainer.getProvider();
161    
162                            RubyInstanceConfig rubyInstanceConfig =
163                                    localContextProvider.getRubyInstanceConfig();
164    
165                            rubyInstanceConfig.setCurrentDirectory(_basePath);
166    
167                            if (ArrayUtil.isNotEmpty(classLoaders)) {
168                                    ClassLoader aggregateClassLoader =
169                                            AggregateClassLoader.getAggregateClassLoader(
170                                                    ClassLoaderUtil.getPortalClassLoader(), classLoaders);
171    
172                                    rubyInstanceConfig.setLoader(aggregateClassLoader);
173                            }
174    
175                            rubyInstanceConfig.setLoadPaths(_loadPaths);
176    
177                            for (Map.Entry<String, Object> entry : inputObjects.entrySet()) {
178                                    String inputName = entry.getKey();
179                                    Object inputObject = entry.getValue();
180    
181                                    if (!inputName.startsWith(StringPool.DOLLAR)) {
182                                            inputName = StringPool.DOLLAR + inputName;
183                                    }
184    
185                                    _scriptingContainer.put(inputName, inputObject);
186                            }
187    
188                            if (scriptFile != null) {
189                                    _scriptingContainer.runScriptlet(
190                                            new FileInputStream(scriptFile), scriptFile.toString());
191                            }
192                            else {
193                                    _scriptingContainer.runScriptlet(script);
194                            }
195    
196                            if (outputNames == null) {
197                                    return null;
198                            }
199    
200                            Map<String, Object> outputObjects = new HashMap<String, Object>();
201    
202                            for (String outputName : outputNames) {
203                                    outputObjects.put(
204                                            outputName, _scriptingContainer.get(outputName));
205                            }
206    
207                            return outputObjects;
208                    }
209                    catch (RaiseException re) {
210                            throw new ScriptingException(
211                                    re.getException().message.asJavaString() + "\n\n", re);
212                    }
213                    catch (FileNotFoundException fnfe) {
214                            throw new ScriptingException(fnfe);
215                    }
216                    finally {
217                            try {
218                                    _globalRuntimeField.set(null, null);
219                            }
220                            catch (Exception e) {
221                                    _log.error(e, e);
222                            }
223                    }
224            }
225    
226            protected Map<String, Object> eval(
227                            Set<String> allowedClasses, Map<String, Object> inputObjects,
228                            Set<String> outputNames, File scriptFile, String script,
229                            ClassLoader... classLoaders)
230                    throws ScriptingException {
231    
232                    if (!_executeInSeparateThread) {
233                            return doEval(
234                                    allowedClasses, inputObjects, outputNames, scriptFile, script,
235                                    classLoaders);
236                    }
237    
238                    EvalCallable evalCallable = new EvalCallable(
239                            allowedClasses, inputObjects, outputNames, scriptFile, script,
240                            classLoaders);
241    
242                    FutureTask<Map<String, Object>> futureTask =
243                            new FutureTask<Map<String, Object>>(evalCallable);
244    
245                    Thread oneTimeExecutorThread = _threadFactory.newThread(futureTask);
246    
247                    oneTimeExecutorThread.start();
248    
249                    try {
250                            oneTimeExecutorThread.join();
251    
252                            return futureTask.get();
253                    }
254                    catch (Exception e) {
255                            futureTask.cancel(true);
256                            oneTimeExecutorThread.interrupt();
257    
258                            throw new ScriptingException(e);
259                    }
260            }
261    
262            protected void initRubyGems() throws Exception {
263                    File rubyGemsJarFile = new File(
264                            PropsValues.LIFERAY_LIB_PORTAL_DIR, "ruby-gems.jar");
265    
266                    if (!rubyGemsJarFile.exists()) {
267                            if (_log.isWarnEnabled()) {
268                                    _log.warn(rubyGemsJarFile + " does not exist");
269                            }
270    
271                            return;
272                    }
273    
274                    String tmpDir = SystemProperties.get(SystemProperties.TMP_DIR);
275    
276                    File rubyDir = new File(tmpDir + "/liferay/ruby");
277    
278                    if (!rubyDir.exists() ||
279                            (rubyDir.lastModified() < rubyGemsJarFile.lastModified())) {
280    
281                            FileUtil.deltree(rubyDir);
282    
283                            rubyDir.mkdirs();
284    
285                            ZipUtil.unzip(rubyGemsJarFile, rubyDir);
286    
287                            rubyDir.setLastModified(rubyGemsJarFile.lastModified());
288                    }
289            }
290    
291            private static final String _COMPILE_MODE_FORCE = "force";
292    
293            private static final String _COMPILE_MODE_JIT = "jit";
294    
295            private static final Log _log = LogFactoryUtil.getLog(RubyExecutor.class);
296    
297            private static final Field _globalRuntimeField;
298            private static final ThreadFactory _threadFactory =
299                    new NamedThreadFactory(
300                            RubyExecutor.class.getName(), Thread.NORM_PRIORITY,
301                            RubyExecutor.class.getClassLoader());
302    
303            static {
304                    try {
305                            _globalRuntimeField = ReflectionUtil.getDeclaredField(
306                                    Ruby.class, "globalRuntime");
307                    }
308                    catch (Exception e) {
309                            throw new ExceptionInInitializerError(e);
310                    }
311            }
312    
313            private final String _basePath;
314            private boolean _executeInSeparateThread = true;
315            private final List<String> _loadPaths;
316            private final ScriptingContainer _scriptingContainer;
317    
318            private class EvalCallable implements Callable<Map<String, Object>> {
319    
320                    public EvalCallable(
321                            Set<String> allowedClasses, Map<String, Object> inputObjects,
322                            Set<String> outputNames, File scriptFile, String script,
323                            ClassLoader[] classLoaders) {
324    
325                            _allowedClasses = allowedClasses;
326                            _inputObjects = inputObjects;
327                            _outputNames = outputNames;
328                            _scriptFile = scriptFile;
329                            _script = script;
330                            _classLoaders = classLoaders;
331                    }
332    
333                    @Override
334                    public Map<String, Object> call() throws Exception {
335                            return doEval(
336                                    _allowedClasses, _inputObjects, _outputNames, _scriptFile,
337                                    _script, _classLoaders);
338                    }
339    
340                    private final Set<String> _allowedClasses;
341                    private final ClassLoader[] _classLoaders;
342                    private final Map<String, Object> _inputObjects;
343                    private final Set<String> _outputNames;
344                    private final String _script;
345                    private final File _scriptFile;
346    
347            }
348    
349    }