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