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