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