/*
* Copyright 2007 Rickard Öberg
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.qi4j.lang.jruby;
import java.io.*;
import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Method;
import java.net.URL;
import java.util.HashMap;
import java.util.Map;
import org.jruby.*;
import org.jruby.exceptions.RaiseException;
import org.jruby.internal.runtime.methods.CallConfiguration;
import org.jruby.internal.runtime.methods.DynamicMethod;
import org.jruby.javasupport.JavaEmbedUtils;
import org.jruby.runtime.Block;
import org.jruby.runtime.ThreadContext;
import org.jruby.runtime.Visibility;
import org.jruby.runtime.builtin.IRubyObject;
import org.qi4j.api.common.AppliesTo;
import org.qi4j.api.common.AppliesToFilter;
import org.qi4j.api.composite.Composite;
import org.qi4j.api.composite.TransientBuilderFactory;
import org.qi4j.api.injection.scope.Service;
import org.qi4j.api.injection.scope.Structure;
import org.qi4j.api.injection.scope.This;
import org.qi4j.api.property.Property;
import org.qi4j.library.scripting.ScriptReloadable;
/**
* Generic mixin that implements interfaces by delegating to Ruby functions
* using JRuby. Each method in an interface is declared by a Ruby method
* in a file located in classpath with the name "<interface>.rb",
* where the interface name includes the package, and has "." replaced with "/".
* <p/>
* Example:
* org/qi4j/samples/hello/domain/HelloWorldSpeaker.rb
*/
@AppliesTo( JRubyMixin.AppliesTo.class )
public class JRubyMixin
implements InvocationHandler, ScriptReloadable
{
@This private Composite me;
@Service private Ruby runtime;
private Map<Class, IRubyObject> rubyObjects = new HashMap<Class, IRubyObject>();
public static class AppliesTo
implements AppliesToFilter
{
@Override
public boolean appliesTo( Method method, Class compositeType, Class mixin, Class modelClass )
{
return getFunctionResoure( method ) != null;
}
}
@Structure TransientBuilderFactory factory;
@Override
public Object invoke( Object proxy, Method method, Object[] args ) throws Throwable
{
try
{
// Get Ruby object for declaring class of the method
Class declaringClass = method.getDeclaringClass();
IRubyObject rubyObject = rubyObjects.get( declaringClass );
// If not yet created, create one
if( rubyObject == null )
{
// Create object instance
try
{
rubyObject = runtime.evalScriptlet( declaringClass.getSimpleName() + ".new()" );
}
catch( RaiseException e )
{
if( e.getException() instanceof RubyNameError )
{
// Initialize Ruby class
String script = getFunction( method );
runtime.evalScriptlet( script );
// Try creating a Ruby instance again
rubyObject = runtime.evalScriptlet( declaringClass.getSimpleName() + ".new()" );
}
else
{
throw e;
}
}
// Set @this variable to Composite
IRubyObject meRuby = JavaEmbedUtils.javaToRuby( runtime, me );
RubyClass rubyClass = meRuby.getMetaClass();
if( !rubyClass.isFrozen() )
{
SetterDynamicMethod setter = new SetterDynamicMethod( runtime.getObjectSpaceModule(), Visibility.PUBLIC, null );
GetterDynamicMethod getter = new GetterDynamicMethod( runtime.getObjectSpaceModule(), Visibility.PUBLIC, null );
Method[] compositeMethods = me.getClass().getInterfaces()[ 0 ].getMethods();
for( Method compositeMethod : compositeMethods )
{
if( Property.class.isAssignableFrom( compositeMethod.getReturnType() ) )
{
rubyClass.addMethod( compositeMethod.getName() + "=", setter );
rubyClass.addMethod( compositeMethod.getName(), getter );
}
}
rubyClass.freeze( ThreadContext.newContext( runtime ) );
}
RubyObjectAdapter rubyObjectAdapter = JavaEmbedUtils.newObjectAdapter();
rubyObjectAdapter.setInstanceVariable( rubyObject, "@this", meRuby );
rubyObjects.put( declaringClass, rubyObject );
}
// Convert method arguments and invoke the method
IRubyObject rubyResult;
if( args != null )
{
IRubyObject[] rubyArgs = new IRubyObject[args.length];
for( int i = 0; i < args.length; i++ )
{
Object arg = args[ i ];
rubyArgs[ i ] = JavaEmbedUtils.javaToRuby( runtime, arg );
}
rubyResult = rubyObject.callMethod( runtime.getCurrentContext(), method.getName(), rubyArgs );
}
else
{
rubyResult = rubyObject.callMethod( runtime.getCurrentContext(), method.getName() );
}
// Convert result to Java
Object result = JavaEmbedUtils.rubyToJava( runtime, rubyResult, method.getReturnType() );
return result;
}
catch( Exception e )
{
e.printStackTrace();
throw e;
}
}
@Override
public void reloadScripts()
{
rubyObjects.clear();
}
protected String getFunction( Method aMethod )
throws IOException
{
URL scriptUrl = getFunctionResoure( aMethod );
if( scriptUrl == null )
{
throw new IOException( "No script found for method " + aMethod.getName() );
}
InputStream in = scriptUrl.openStream();
BufferedReader scriptReader = new BufferedReader( new InputStreamReader( in ) );
String line;
StringBuilder sb = new StringBuilder();
while( ( line = scriptReader.readLine() ) != null )
{
sb.append( line ).append( "\n" );
}
return sb.toString();
}
protected static URL getFunctionResoure( Method method )
{
Class<?> declaringClass = method.getDeclaringClass();
String classname = declaringClass.getName();
String scriptFile = classname.replace( '.', File.separatorChar ) + ".rb";
ClassLoader loader = declaringClass.getClassLoader();
URL scriptUrl = loader.getResource( scriptFile );
return scriptUrl;
}
private static class SetterDynamicMethod
extends DynamicMethod
{
private SetterDynamicMethod( RubyModule rubyModule, Visibility visibility, CallConfiguration callConfiguration )
{
super( rubyModule, visibility, callConfiguration );
}
@Override
public IRubyObject call( ThreadContext threadContext, IRubyObject iRubyObject, RubyModule rubyModule, String methodName, IRubyObject[] iRubyObjects, Block block )
{
String propertyName = methodName.substring( 0, methodName.length() - 1 );
IRubyObject prop = iRubyObject.callMethod( threadContext, propertyName );
prop.callMethod( threadContext, "set", iRubyObjects );
return null;
}
@Override
public DynamicMethod dup()
{
return this;
}
}
private static class GetterDynamicMethod
extends DynamicMethod
{
private GetterDynamicMethod( RubyModule rubyModule, Visibility visibility, CallConfiguration callConfiguration )
{
super( rubyModule, visibility, callConfiguration );
}
@Override
public IRubyObject call( ThreadContext threadContext, IRubyObject iRubyObject, RubyModule rubyModule, String methodName, IRubyObject[] iRubyObjects, Block block )
{
try
{
String propertyName = methodName;
Object thisComposite = JavaEmbedUtils.rubyToJava( iRubyObject.getRuntime(), iRubyObject, Object.class );
Method propertyMethod = thisComposite.getClass().getMethod( propertyName );
Property property = (Property) propertyMethod.invoke( thisComposite );
Object propertyValue = property.get();
IRubyObject prop = JavaEmbedUtils.javaToRuby( iRubyObject.getRuntime(), propertyValue );
return prop;
}
catch( Exception e )
{
throw new RaiseException( new RubyNameError( iRubyObject.getRuntime(), iRubyObject.getMetaClass(), "Could not find property " + methodName ) );
}
}
@Override
public DynamicMethod dup()
{
return this;
}
}
}