001
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
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 }