001/* 002 * Copyright 2002-2017 the original author or authors. 003 * 004 * Licensed under the Apache License, Version 2.0 (the "License"); 005 * you may not use this file except in compliance with the License. 006 * You may obtain a copy of the License at 007 * 008 * https://www.apache.org/licenses/LICENSE-2.0 009 * 010 * Unless required by applicable law or agreed to in writing, software 011 * distributed under the License is distributed on an "AS IS" BASIS, 012 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 013 * See the License for the specific language governing permissions and 014 * limitations under the License. 015 */ 016 017package org.springframework.scripting.jruby; 018 019import java.lang.reflect.Array; 020import java.lang.reflect.InvocationHandler; 021import java.lang.reflect.Method; 022import java.lang.reflect.Proxy; 023import java.util.Collections; 024import java.util.List; 025 026import org.jruby.Ruby; 027import org.jruby.RubyArray; 028import org.jruby.RubyNil; 029import org.jruby.ast.ClassNode; 030import org.jruby.ast.Colon2Node; 031import org.jruby.ast.NewlineNode; 032import org.jruby.ast.Node; 033import org.jruby.exceptions.JumpException; 034import org.jruby.exceptions.RaiseException; 035import org.jruby.javasupport.JavaEmbedUtils; 036import org.jruby.runtime.builtin.IRubyObject; 037 038import org.springframework.core.NestedRuntimeException; 039import org.springframework.util.ClassUtils; 040import org.springframework.util.ObjectUtils; 041import org.springframework.util.ReflectionUtils; 042import org.springframework.util.StringUtils; 043 044/** 045 * Utility methods for handling JRuby-scripted objects. 046 * 047 * <p>Note: Spring 4.0 supports JRuby 1.5 and higher, with 1.7.x recommended. 048 * As of Spring 4.2, JRuby 9.0.0.0 is supported as well but primarily through 049 * {@link org.springframework.scripting.support.StandardScriptFactory}. 050 * 051 * @author Rob Harrop 052 * @author Juergen Hoeller 053 * @author Rick Evans 054 * @since 2.0 055 * @deprecated in favor of JRuby support via the JSR-223 abstraction 056 * ({@link org.springframework.scripting.support.StandardScriptFactory}) 057 */ 058@Deprecated 059public abstract class JRubyScriptUtils { 060 061 /** 062 * Create a new JRuby-scripted object from the given script source, 063 * using the default {@link ClassLoader}. 064 * @param scriptSource the script source text 065 * @param interfaces the interfaces that the scripted Java object is to implement 066 * @return the scripted Java object 067 * @throws JumpException in case of JRuby parsing failure 068 * @see ClassUtils#getDefaultClassLoader() 069 */ 070 public static Object createJRubyObject(String scriptSource, Class<?>... interfaces) throws JumpException { 071 return createJRubyObject(scriptSource, interfaces, ClassUtils.getDefaultClassLoader()); 072 } 073 074 /** 075 * Create a new JRuby-scripted object from the given script source. 076 * @param scriptSource the script source text 077 * @param interfaces the interfaces that the scripted Java object is to implement 078 * @param classLoader the {@link ClassLoader} to create the script proxy with 079 * @return the scripted Java object 080 * @throws JumpException in case of JRuby parsing failure 081 */ 082 public static Object createJRubyObject(String scriptSource, Class<?>[] interfaces, ClassLoader classLoader) { 083 Ruby ruby = initializeRuntime(); 084 085 Node scriptRootNode = ruby.parseEval(scriptSource, "", null, 0); 086 IRubyObject rubyObject = ruby.runNormally(scriptRootNode); 087 088 if (rubyObject instanceof RubyNil) { 089 String className = findClassName(scriptRootNode); 090 rubyObject = ruby.evalScriptlet("\n" + className + ".new"); 091 } 092 // still null? 093 if (rubyObject instanceof RubyNil) { 094 throw new IllegalStateException("Compilation of JRuby script returned RubyNil: " + rubyObject); 095 } 096 097 return Proxy.newProxyInstance(classLoader, interfaces, new RubyObjectInvocationHandler(rubyObject, ruby)); 098 } 099 100 /** 101 * Initializes an instance of the {@link org.jruby.Ruby} runtime. 102 */ 103 @SuppressWarnings("unchecked") 104 private static Ruby initializeRuntime() { 105 return JavaEmbedUtils.initialize(Collections.EMPTY_LIST); 106 } 107 108 /** 109 * Given the root {@link Node} in a JRuby AST will locate the name of the 110 * class defined by that AST. 111 * @throws IllegalArgumentException if no class is defined by the supplied AST 112 */ 113 private static String findClassName(Node rootNode) { 114 ClassNode classNode = findClassNode(rootNode); 115 if (classNode == null) { 116 throw new IllegalArgumentException("Unable to determine class name for root node '" + rootNode + "'"); 117 } 118 Colon2Node node = (Colon2Node) classNode.getCPath(); 119 return node.getName(); 120 } 121 122 /** 123 * Find the first {@link ClassNode} under the supplied {@link Node}. 124 * @return the corresponding {@code ClassNode}, or {@code null} if none found 125 */ 126 private static ClassNode findClassNode(Node node) { 127 if (node == null) { 128 return null; 129 } 130 if (node instanceof ClassNode) { 131 return (ClassNode) node; 132 } 133 List<Node> children = node.childNodes(); 134 for (Node child : children) { 135 if (child instanceof ClassNode) { 136 return (ClassNode) child; 137 } 138 else if (child instanceof NewlineNode) { 139 NewlineNode nn = (NewlineNode) child; 140 ClassNode found = findClassNode(nn.getNextNode()); 141 if (found != null) { 142 return found; 143 } 144 } 145 } 146 for (Node child : children) { 147 ClassNode found = findClassNode(child); 148 if (found != null) { 149 return found; 150 } 151 } 152 return null; 153 } 154 155 156 /** 157 * InvocationHandler that invokes a JRuby script method. 158 */ 159 private static class RubyObjectInvocationHandler implements InvocationHandler { 160 161 private final IRubyObject rubyObject; 162 163 private final Ruby ruby; 164 165 public RubyObjectInvocationHandler(IRubyObject rubyObject, Ruby ruby) { 166 this.rubyObject = rubyObject; 167 this.ruby = ruby; 168 } 169 170 @Override 171 public Object invoke(Object proxy, Method method, Object[] args) throws Throwable { 172 if (ReflectionUtils.isEqualsMethod(method)) { 173 return (isProxyForSameRubyObject(args[0])); 174 } 175 else if (ReflectionUtils.isHashCodeMethod(method)) { 176 return this.rubyObject.hashCode(); 177 } 178 else if (ReflectionUtils.isToStringMethod(method)) { 179 String toStringResult = this.rubyObject.toString(); 180 if (!StringUtils.hasText(toStringResult)) { 181 toStringResult = ObjectUtils.identityToString(this.rubyObject); 182 } 183 return "JRuby object [" + toStringResult + "]"; 184 } 185 try { 186 IRubyObject[] rubyArgs = convertToRuby(args); 187 IRubyObject rubyResult = 188 this.rubyObject.callMethod(this.ruby.getCurrentContext(), method.getName(), rubyArgs); 189 return convertFromRuby(rubyResult, method.getReturnType()); 190 } 191 catch (RaiseException ex) { 192 throw new JRubyExecutionException(ex); 193 } 194 } 195 196 private boolean isProxyForSameRubyObject(Object other) { 197 if (!Proxy.isProxyClass(other.getClass())) { 198 return false; 199 } 200 InvocationHandler ih = Proxy.getInvocationHandler(other); 201 return (ih instanceof RubyObjectInvocationHandler && 202 this.rubyObject.equals(((RubyObjectInvocationHandler) ih).rubyObject)); 203 } 204 205 private IRubyObject[] convertToRuby(Object[] javaArgs) { 206 if (javaArgs == null || javaArgs.length == 0) { 207 return new IRubyObject[0]; 208 } 209 IRubyObject[] rubyArgs = new IRubyObject[javaArgs.length]; 210 for (int i = 0; i < javaArgs.length; ++i) { 211 rubyArgs[i] = JavaEmbedUtils.javaToRuby(this.ruby, javaArgs[i]); 212 } 213 return rubyArgs; 214 } 215 216 private Object convertFromRuby(IRubyObject rubyResult, Class<?> returnType) { 217 Object result = JavaEmbedUtils.rubyToJava(this.ruby, rubyResult, returnType); 218 if (result instanceof RubyArray && returnType.isArray()) { 219 result = convertFromRubyArray(((RubyArray) result).toJavaArray(), returnType); 220 } 221 return result; 222 } 223 224 private Object convertFromRubyArray(IRubyObject[] rubyArray, Class<?> returnType) { 225 Class<?> targetType = returnType.getComponentType(); 226 Object javaArray = Array.newInstance(targetType, rubyArray.length); 227 for (int i = 0; i < rubyArray.length; i++) { 228 IRubyObject rubyObject = rubyArray[i]; 229 Array.set(javaArray, i, convertFromRuby(rubyObject, targetType)); 230 } 231 return javaArray; 232 } 233 } 234 235 236 /** 237 * Exception thrown in response to a JRuby {@link RaiseException} 238 * being thrown from a JRuby method invocation. 239 */ 240 @SuppressWarnings("serial") 241 public static class JRubyExecutionException extends NestedRuntimeException { 242 243 /** 244 * Create a new {@code JRubyException}, 245 * wrapping the given JRuby {@code RaiseException}. 246 * @param ex the cause (must not be {@code null}) 247 */ 248 public JRubyExecutionException(RaiseException ex) { 249 super(ex.getMessage(), ex); 250 } 251 } 252 253}