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 void setExecuteInSeparateThread(boolean executeInSeparateThread) {
140 _executeInSeparateThread = executeInSeparateThread;
141 }
142
143 protected Map<String, Object> doEval(
144 Set<String> allowedClasses, Map<String, Object> inputObjects,
145 Set<String> outputNames, File scriptFile, String script,
146 ClassLoader... classLoaders)
147 throws ScriptingException {
148
149 if (allowedClasses != null) {
150 throw new ExecutionException(
151 "Constrained execution not supported for Ruby");
152 }
153
154 try {
155 LocalContextProvider localContextProvider =
156 _scriptingContainer.getProvider();
157
158 RubyInstanceConfig rubyInstanceConfig =
159 localContextProvider.getRubyInstanceConfig();
160
161 rubyInstanceConfig.setCurrentDirectory(_basePath);
162
163 if (ArrayUtil.isNotEmpty(classLoaders)) {
164 ClassLoader aggregateClassLoader =
165 AggregateClassLoader.getAggregateClassLoader(
166 ClassLoaderUtil.getPortalClassLoader(), classLoaders);
167
168 rubyInstanceConfig.setLoader(aggregateClassLoader);
169 }
170
171 rubyInstanceConfig.setLoadPaths(_loadPaths);
172
173 for (Map.Entry<String, Object> entry : inputObjects.entrySet()) {
174 String inputName = entry.getKey();
175 Object inputObject = entry.getValue();
176
177 if (!inputName.startsWith(StringPool.DOLLAR)) {
178 inputName = StringPool.DOLLAR + inputName;
179 }
180
181 _scriptingContainer.put(inputName, inputObject);
182 }
183
184 if (scriptFile != null) {
185 _scriptingContainer.runScriptlet(
186 new FileInputStream(scriptFile), scriptFile.toString());
187 }
188 else {
189 _scriptingContainer.runScriptlet(script);
190 }
191
192 if (outputNames == null) {
193 return null;
194 }
195
196 Map<String, Object> outputObjects = new HashMap<String, Object>();
197
198 for (String outputName : outputNames) {
199 outputObjects.put(
200 outputName, _scriptingContainer.get(outputName));
201 }
202
203 return outputObjects;
204 }
205 catch (RaiseException re) {
206 throw new ScriptingException(
207 re.getException().message.asJavaString() + "\n\n", re);
208 }
209 catch (FileNotFoundException fnfe) {
210 throw new ScriptingException(fnfe);
211 }
212 finally {
213 try {
214 _globalRuntimeField.set(null, null);
215 }
216 catch (Exception e) {
217 _log.error(e, e);
218 }
219 }
220 }
221
222 protected Map<String, Object> eval(
223 Set<String> allowedClasses, Map<String, Object> inputObjects,
224 Set<String> outputNames, File scriptFile, String script,
225 ClassLoader... classLoaders)
226 throws ScriptingException {
227
228 if (!_executeInSeparateThread) {
229 return doEval(
230 allowedClasses, inputObjects, outputNames, scriptFile, script,
231 classLoaders);
232 }
233
234 EvalCallable evalCallable = new EvalCallable(
235 allowedClasses, inputObjects, outputNames, scriptFile, script,
236 classLoaders);
237
238 FutureTask<Map<String, Object>> futureTask =
239 new FutureTask<Map<String, Object>>(evalCallable);
240
241 Thread oneTimeExecutorThread = _threadFactory.newThread(futureTask);
242
243 oneTimeExecutorThread.start();
244
245 try {
246 oneTimeExecutorThread.join();
247
248 return futureTask.get();
249 }
250 catch (Exception e) {
251 futureTask.cancel(true);
252 oneTimeExecutorThread.interrupt();
253
254 throw new ScriptingException(e);
255 }
256 }
257
258 protected void initRubyGems() throws Exception {
259 File rubyGemsJarFile = new File(
260 PropsValues.LIFERAY_LIB_PORTAL_DIR, "ruby-gems.jar");
261
262 if (!rubyGemsJarFile.exists()) {
263 if (_log.isWarnEnabled()) {
264 _log.warn(rubyGemsJarFile + " does not exist");
265 }
266
267 return;
268 }
269
270 String tmpDir = SystemProperties.get(SystemProperties.TMP_DIR);
271
272 File rubyDir = new File(tmpDir + "/liferay/ruby");
273
274 if (!rubyDir.exists() ||
275 (rubyDir.lastModified() < rubyGemsJarFile.lastModified())) {
276
277 FileUtil.deltree(rubyDir);
278
279 FileUtil.mkdirs(rubyDir);
280
281 ZipUtil.unzip(rubyGemsJarFile, rubyDir);
282
283 rubyDir.setLastModified(rubyGemsJarFile.lastModified());
284 }
285 }
286
287 private static final String _COMPILE_MODE_FORCE = "force";
288
289 private static final String _COMPILE_MODE_JIT = "jit";
290
291 private static Log _log = LogFactoryUtil.getLog(RubyExecutor.class);
292
293 private static Field _globalRuntimeField;
294
295 private static ThreadFactory _threadFactory =
296 new NamedThreadFactory(
297 RubyExecutor.class.getName(), Thread.NORM_PRIORITY,
298 RubyExecutor.class.getClassLoader());
299
300 static {
301 try {
302 _globalRuntimeField = ReflectionUtil.getDeclaredField(
303 Ruby.class, "globalRuntime");
304 }
305 catch (Exception e) {
306 throw new ExceptionInInitializerError(e);
307 }
308 }
309
310 private String _basePath;
311 private boolean _executeInSeparateThread = true;
312 private List<String> _loadPaths;
313 private ScriptingContainer _scriptingContainer;
314
315 private class EvalCallable implements Callable<Map<String, Object>> {
316
317 public EvalCallable(
318 Set<String> allowedClasses, Map<String, Object> inputObjects,
319 Set<String> outputNames, File scriptFile, String script,
320 ClassLoader[] classLoaders) {
321
322 _allowedClasses = allowedClasses;
323 _inputObjects = inputObjects;
324 _outputNames = outputNames;
325 _scriptFile = scriptFile;
326 _script = script;
327 _classLoaders = classLoaders;
328 }
329
330 @Override
331 public Map<String, Object> call() throws Exception {
332 return doEval(
333 _allowedClasses, _inputObjects, _outputNames, _scriptFile,
334 _script, _classLoaders);
335 }
336
337 private final Set<String> _allowedClasses;
338 private final Map<String, Object> _inputObjects;
339 private final Set<String> _outputNames;
340 private final File _scriptFile;
341 private final String _script;
342 private final ClassLoader[] _classLoaders;
343
344 }
345
346 }