blob: 2acd217b12eb39094c6f0f801b311c27f468ba8f [file] [log] [blame]
/*
* 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.apache.zest.library.javascript;
import java.io.*;
import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Method;
import java.net.URL;
import java.util.HashMap;
import java.util.StringTokenizer;
import org.mozilla.javascript.*;
import org.apache.zest.api.common.AppliesTo;
import org.apache.zest.api.common.AppliesToFilter;
import org.apache.zest.api.composite.Composite;
import org.apache.zest.api.composite.TransientBuilderFactory;
import org.apache.zest.api.injection.scope.Structure;
import org.apache.zest.api.injection.scope.This;
import org.apache.zest.library.scripting.ScriptException;
import org.apache.zest.library.scripting.ScriptReloadable;
/**
* Generic mixin that implements interfaces by delegating to JavaScript functions
* using Rhino. Each method in an interface is declared as a JS function
* in a file located in classpath with the name "<interface>.<method>.js",
* where the interface name includes the package, and has "." replaced with "/".
* <p>
* Example:
* </p>
* <pre><code>
* org/qi4j/samples/hello/domain/HelloWorldSpeaker.say.js
* </code></pre>
*/
@AppliesTo( JavaScriptMixin.AppliesTo.class )
public class JavaScriptMixin
implements InvocationHandler, ScriptReloadable
{
@This private Composite me;
static private Scriptable standardScope;
private HashMap<String, Function> cachedScripts;
@Structure private TransientBuilderFactory factory;
private Scriptable instanceScope;
static
{
Context cx = Context.enter();
standardScope = cx.initStandardObjects();
Context.exit();
}
public JavaScriptMixin()
{
cachedScripts = new HashMap<String, Function>();
Context cx = Context.enter();
instanceScope = cx.newObject( standardScope );
instanceScope.setPrototype( standardScope );
Context.exit();
}
@Override
public Object invoke( Object proxy, Method method, Object[] args ) throws Throwable
{
Context cx = Context.enter();
try
{
Scriptable proxyScope = Context.toObject( proxy, instanceScope );
proxyScope.setPrototype( instanceScope );
proxyScope.put( "compositeBuilderFactory", proxyScope, factory );
proxyScope.put( "This", proxyScope, me );
Function fn = getFunction( cx, proxyScope, method );
Object result = fn.call( cx, instanceScope, proxyScope, args );
if( result instanceof Undefined )
{
return null;
}
else if( result instanceof Wrapper )
{
return ( (Wrapper) result ).unwrap();
}
else
{
return result;
}
}
finally
{
Context.exit();
}
}
@Override
public void reloadScripts()
{
cachedScripts.clear();
}
private Function getFunction( Context cx, Scriptable scope, Method method )
throws IOException
{
Class<?> declaringClass = method.getDeclaringClass();
String classname = declaringClass.getName();
String methodName = method.getName();
String requestedFunctionName = classname + ":" + methodName;
Function fx = cachedScripts.get( requestedFunctionName );
if( fx != null )
{
return fx;
}
compileScripts( cx, scope, method );
fx = cachedScripts.get( requestedFunctionName );
return fx;
}
private void compileScripts( Context cx, Scriptable scope, Method method )
throws IOException
{
URL scriptUrl = getFunctionResource( method );
if( scriptUrl == null )
{
throw new IOException( "No script found for method " + method.getName() );
}
InputStream in = scriptUrl.openStream();
BufferedReader scriptReader = new BufferedReader( new InputStreamReader( in ) );
int lineNo = 1;
String classname = method.getDeclaringClass().getName();
while( true )
{
ScriptFragment fragment = extractFunction( scriptReader );
if( "".equals( fragment.script.trim() ) )
{
break;
}
String functionName = parseFunctionName( fragment.script, scriptUrl.toString() );
Function function = cx.compileFunction( scope, fragment.script, "<" + scriptUrl.toString() + ">", lineNo, null );
cachedScripts.put( classname + ":" + functionName, function );
lineNo = lineNo + fragment.numberOfLines;
}
}
/**
* Extracts the function name.
* <p>
* Since the fragment has been stripped of all comments, the first non-whitespace word to appear
* should be "function" and the word after that should be the function name.
* </p>
*
* @param script The script snippet.
* @param scriptName The name of the script being parsed.
* @return the name of the function declared in this snippet.
*/
private String parseFunctionName( String script, String scriptName )
{
// TODO optimize with hardcoded parser??
StringTokenizer st = new StringTokenizer( script, " \t\n\r\f(){}", false );
if( !st.hasMoreTokens() )
{
throw new ScriptException( "The word \"function\" was not found in script: " + scriptName );
}
String fx = st.nextToken();
if( !"function".equals( fx ) )
{
throw new ScriptException( "The word \"function\" was not found in script: " + scriptName );
}
if( !st.hasMoreTokens() )
{
throw new ScriptException( "Invalid syntax in: " + scriptName + "\n No function name." );
}
return st.nextToken();
}
/**
* Returns ONE function, minus comments.
*
* @param scriptReader The Reader of the script
* @return A ScriptFragment containing the Script text for the function, and how many lines it is.
* @throws IOException If a problem in the Reader occurs.
*/
private ScriptFragment extractFunction( Reader scriptReader )
throws IOException
{
ScriptFragment fragment = new ScriptFragment();
boolean inString = false;
boolean inChar = false;
boolean escaped = false;
boolean lineComment = false;
boolean blockComment = false;
char lastCh = '\0';
int braceCounter = 0;
boolean notStarted = true;
int b = scriptReader.read();
int skip = 0;
while( b != -1 && ( notStarted || braceCounter > 0 ) )
{
char ch = (char) b;
if( !blockComment && !lineComment )
{
fragment.script = fragment.script + ch;
if( !escaped )
{
if( !inString && !inChar )
{
if( ch == '{' )
{
braceCounter++;
notStarted = false;
}
if( ch == '}' )
{
braceCounter--;
}
}
if( ch == '\"' )
{
inString = !inString;
}
if( ch == '\'' )
{
inChar = !inChar;
}
if( ch == '\\' )
{
escaped = true;
}
if( ch == '\n' )
{
fragment.numberOfLines++;
}
if( ch == '/' && lastCh == '/' )
{
lineComment = true;
fragment.script = fragment.script.substring( 0, fragment.script.length() - 2 );
}
if( ch == '*' && lastCh == '/' )
{
blockComment = true;
fragment.script = fragment.script.substring( 0, fragment.script.length() - 2 );
}
}
else
{
if( ch == 'u' )
{
skip = 4;
}
else if( skip > 0 )
{
skip--;
}
else
{
escaped = false;
}
}
}
else
{
if( lineComment )
{
if( ch == '\n' )
{
lineComment = false;
}
}
if( blockComment )
{
if( ch == '/' && lastCh == '*' )
{
blockComment = false;
}
}
}
lastCh = ch;
b = scriptReader.read();
}
return fragment;
}
private static URL getFunctionResource( Method method )
{
String scriptName = getScriptName( method );
Class<?> declaringClass = method.getDeclaringClass();
ClassLoader loader = declaringClass.getClassLoader();
return loader.getResource( scriptName );
}
private static String getScriptName( Method method )
{
Class<?> declaringClass = method.getDeclaringClass();
String classname = declaringClass.getName();
return classname.replace( '.', '/' ) + ".js";
}
private static class ScriptFragment
{
String script = "";
int numberOfLines = 0;
}
public static class AppliesTo
implements AppliesToFilter
{
public boolean appliesTo( Method method, Class compositeType, Class mixin, Class modelClass )
{
return getFunctionResource( method ) != null;
}
}
}