/*
* Copyright (c) 2006, Sun Microsystems, Inc.
* All rights reserved.
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions are met:
*
* - Redistributions of source code must retain the above copyright notice, this
* list of conditions and the following disclaimer.
*
* - Redistributions in binary form must reproduce the above copyright notice,
* this list of conditions and the following disclaimer in the documentation
* and/or other materials provided with the distribution.
*
* - Neither the name of the Sun Microsystems, Inc. nor the names of
* contributors may be used to endorse or promote products derived from this
* software without specific prior written permission.
*
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND
* CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED
* WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
* WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A
* PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE
* COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR ANY
* DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
* CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
* PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF
* USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
* CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
* CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE
* OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
* SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH
* DAMAGE.
*/
package org.apache.tuscany.sca.implementation.script.engines;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.io.PrintStream;
import java.io.Reader;
import java.io.StringWriter;
import java.io.UnsupportedEncodingException;
import java.io.Writer;
import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Method;
import java.lang.reflect.Proxy;
import java.nio.ByteBuffer;
import java.nio.CharBuffer;
import java.nio.charset.Charset;
import java.nio.charset.CharsetDecoder;
import java.nio.charset.CoderResult;
import java.nio.charset.CodingErrorAction;
import java.security.AccessController;
import java.security.PrivilegedAction;
import java.util.Arrays;
import java.util.Collections;
import java.util.HashSet;
import java.util.Iterator;
import java.util.List;
import java.util.Set;
import javax.script.AbstractScriptEngine;
import javax.script.Bindings;
import javax.script.Compilable;
import javax.script.CompiledScript;
import javax.script.Invocable;
import javax.script.ScriptContext;
import javax.script.ScriptEngine;
import javax.script.ScriptEngineFactory;
import javax.script.ScriptException;
import javax.script.SimpleBindings;
import org.jruby.Ruby;
import org.jruby.RubyException;
import org.jruby.RubyIO;
import org.jruby.RubyObject;
import org.jruby.ast.Node;
import org.jruby.exceptions.RaiseException;
import org.jruby.internal.runtime.GlobalVariable;
import org.jruby.internal.runtime.GlobalVariables;
import org.jruby.internal.runtime.ReadonlyAccessor;
import org.jruby.internal.runtime.ValueAccessor;
import org.jruby.javasupport.Java;
import org.jruby.javasupport.JavaEmbedUtils;
import org.jruby.javasupport.JavaObject;
import org.jruby.javasupport.JavaUtil;
import org.jruby.runtime.Block;
import org.jruby.runtime.IAccessor;
import org.jruby.runtime.builtin.IRubyObject;
import org.jruby.util.KCode;
import com.sun.script.jruby.JRubyScriptEngineFactory;
/**
* This class is a copy of the class com.sun.script.ruby.JRubyScriptEngine with some minor modifications
* to work around problems with Tuscany setting SCA properties and references as global variable in JRuby
* Should only need it temporarily till a new BSF release fixes it.
*
* @version $Rev$ $Date$
*/
@SuppressWarnings("unchecked")
public class TuscanyJRubyScriptEngine extends AbstractScriptEngine
implements Compilable, Invocable {
// my factory, may be null
private ScriptEngineFactory factory;
private Ruby runtime;
public TuscanyJRubyScriptEngine() {
// Allow privileged access to ready properties. Requires PropertyPermission in security
// policy.
String rubyPath = AccessController.doPrivileged(new PrivilegedAction<String>() {
public String run() {
return System.getProperty("com.sun.script.jruby.loadpath");
}
});
init(rubyPath);
}
public TuscanyJRubyScriptEngine(String loadPath) {
init(loadPath);
}
// my implementation for CompiledScript
private class JRubyCompiledScript extends CompiledScript {
// my compiled code
private Node node;
JRubyCompiledScript (Node node) {
this.node = node;
}
public ScriptEngine getEngine() {
return TuscanyJRubyScriptEngine.this;
}
public Object eval(ScriptContext ctx) throws ScriptException {
return evalNode(node, ctx);
}
}
// Compilable methods
public CompiledScript compile(String script)
throws ScriptException {
Node node = compileScript(script, context);
return new JRubyCompiledScript(node);
}
public CompiledScript compile (Reader reader)
throws ScriptException {
Node node = compileScript(reader, context);
return new JRubyCompiledScript(node);
}
// Invocable methods
public Object invokeFunction(String name, Object... args)
throws ScriptException, NoSuchMethodException {
return invokeImpl(null, name, args, Object.class);
}
public Object invokeMethod(Object obj, String name, Object... args)
throws ScriptException, NoSuchMethodException {
if (obj == null) {
throw new IllegalArgumentException("script object is null");
}
return invokeImpl(obj, name, args, Object.class);
}
public Object getInterface(Object obj, Class clazz) {
if (obj == null) {
throw new IllegalArgumentException("script object is null");
}
return makeInterface(obj, clazz);
}
public Object getInterface(Class clazz) {
return makeInterface(null, clazz);
}
private <T> T makeInterface(Object obj, Class<T> clazz) {
if (clazz == null || !clazz.isInterface()) {
throw new IllegalArgumentException("interface Class expected");
}
final Object thiz = obj;
return (T) Proxy.newProxyInstance(
clazz.getClassLoader(),
new Class[] { clazz },
new InvocationHandler() {
public Object invoke(Object proxy, Method m, Object[] args)
throws Throwable {
return invokeImpl(thiz, m.getName(),
args, m.getReturnType());
}
});
}
// ScriptEngine methods
public synchronized Object eval(String str, ScriptContext ctx)
throws ScriptException {
Node node = compileScript(str, ctx);
return evalNode(node, ctx);
}
public synchronized Object eval(Reader reader, ScriptContext ctx)
throws ScriptException {
Node node = compileScript(reader, ctx);
return evalNode(node, ctx);
}
public ScriptEngineFactory getFactory() {
synchronized (this) {
if (factory == null) {
factory = new JRubyScriptEngineFactory();
}
}
return factory;
}
public Bindings createBindings() {
return new SimpleBindings();
}
// package-private methods
void setFactory(ScriptEngineFactory factory) {
this.factory = factory;
}
// internals only below this point
private Object rubyToJava(IRubyObject value) {
return rubyToJava(value, Object.class);
}
private Object rubyToJava(IRubyObject value, Class type) {
return JavaUtil.convertArgument(
runtime,
Java.ruby_to_java(value, value, Block.NULL_BLOCK),
type);
}
private IRubyObject javaToRuby(Object value) {
if (value instanceof IRubyObject) {
return (IRubyObject) value;
}
IRubyObject result = JavaUtil.convertJavaToRuby(runtime, value);
if (result instanceof JavaObject) {
return runtime.getModule("JavaUtilities").callMethod(runtime.getCurrentContext(), "wrap", result);
}
return result;
}
private synchronized Node compileScript(String script, ScriptContext ctx)
throws ScriptException {
GlobalVariables oldGlobals = runtime.getGlobalVariables();
try {
setErrorWriter(ctx.getErrorWriter());
setGlobalVariables(ctx);
String filename = (String) ctx.getAttribute(ScriptEngine.FILENAME);
if (filename == null) {
filename = "<unknown>";
}
return runtime.parseEval(script, filename, null, 0);
} catch (RaiseException e) {
RubyException re = e.getException();
runtime.printError(re);
throw new ScriptException(e);
} catch (Exception e) {
throw new ScriptException(e);
} finally {
if (oldGlobals != null) {
setGlobalVariables(oldGlobals);
}
}
}
private synchronized Node compileScript(Reader reader, ScriptContext ctx)
throws ScriptException {
GlobalVariables oldGlobals = runtime.getGlobalVariables();
try {
setErrorWriter(ctx.getErrorWriter());
setGlobalVariables(ctx);
String filename = (String) ctx.getAttribute(ScriptEngine.FILENAME);
if (filename == null) {
filename = "<unknown>";
String script = getRubyScript(reader);
return runtime.parseEval(script, filename, null, 0);
}
InputStream inputStream = getRubyReader(filename);
return runtime.parseFile(inputStream, filename, null);
} catch (RaiseException e) {
RubyException re = e.getException();
runtime.printError(re);
throw new ScriptException(e);
} catch (Exception exp) {
throw new ScriptException(exp);
} finally {
if (oldGlobals != null) {
setGlobalVariables(oldGlobals);
}
}
}
private String getRubyScript(Reader reader) throws IOException {
StringBuffer sb = new StringBuffer();
char[] cbuf;
while (true) {
cbuf = new char[8*1024];
int chars = reader.read(cbuf, 0, cbuf.length);
if (chars < 0) {
break;
}
sb.append(cbuf, 0, chars);
}
cbuf = null;
return (new String(sb)).trim();
}
private InputStream getRubyReader(String filename) throws FileNotFoundException {
File file = new File(filename);
return new FileInputStream(file);
}
private void setGlobalVariables(final ScriptContext ctx) {
ctx.setAttribute("context", ctx, ScriptContext.ENGINE_SCOPE);
setGlobalVariables(new GlobalVariables(runtime) {
GlobalVariables parent = runtime.getGlobalVariables();
@Override
public void define(String name, IAccessor accessor) {
assert name != null;
assert accessor != null;
assert name.startsWith("$");
synchronized (ctx) {
Bindings engineScope = ctx.getBindings(ScriptContext.ENGINE_SCOPE);
engineScope.put(name, new GlobalVariable(accessor));
}
}
@Override
public void defineReadonly(String name, IAccessor accessor) {
assert name != null;
assert accessor != null;
assert name.startsWith("$");
synchronized (ctx) {
Bindings engineScope = ctx.getBindings(ScriptContext.ENGINE_SCOPE);
engineScope.put(name, new GlobalVariable(new
ReadonlyAccessor(name, accessor)));
}
}
@Override
public boolean isDefined(String name) {
assert name != null;
assert name.startsWith("$");
synchronized (ctx) {
String modifiedName = name.substring(1);
boolean defined = ctx.getAttributesScope(modifiedName) != -1;
return defined ? true : parent.isDefined(name);
}
}
@Override
public void alias(String name, String oldName) {
assert name != null;
assert oldName != null;
assert name.startsWith("$");
assert oldName.startsWith("$");
if (runtime.getSafeLevel() >= 4) {
throw runtime.newSecurityError("Insecure: can't alias global variable");
}
synchronized (ctx) {
int scope = ctx.getAttributesScope(name);
if (scope == -1) {
scope = ScriptContext.ENGINE_SCOPE;
}
IRubyObject value = get(oldName);
ctx.setAttribute(name, rubyToJava(value), scope);
}
}
@Override
public IRubyObject get(String name) {
assert name != null;
assert name.startsWith("$");
synchronized (ctx) {
// skip '$' and try
String modifiedName = name.substring(1);
int scope = ctx.getAttributesScope(modifiedName);
if (scope == -1) {
return parent.get(name);
}
Object obj = ctx.getAttribute(modifiedName, scope);
if (obj instanceof IAccessor) {
return ((IAccessor)obj).getValue();
} else {
return javaToRuby(obj);
}
}
}
@Override
public IRubyObject set(String name, IRubyObject value) {
assert name != null;
assert name.startsWith("$");
if (runtime.getSafeLevel() >= 4) {
throw runtime.newSecurityError("Insecure: can't change global variable value");
}
synchronized (ctx) {
// skip '$' and try
String modifiedName = name.substring(1);
int scope = ctx.getAttributesScope(modifiedName);
if (scope == -1) {
scope = ScriptContext.ENGINE_SCOPE;
}
IRubyObject oldValue = get(name);
Object obj = ctx.getAttribute(modifiedName, scope);
if (obj instanceof IAccessor) {
((IAccessor)obj).setValue(value);
} else {
ctx.setAttribute(modifiedName, rubyToJava(value), scope);
if ("KCODE".equals(modifiedName)) {
setKCode((String)rubyToJava(value));
} else if ("stdout".equals(modifiedName)) {
equalOutputs((RubyObject)value);
}
}
return oldValue;
}
}
@Override
public Set<String> getNames() {
HashSet set = new HashSet();
synchronized (ctx) {
for (Object scope : ctx.getScopes()) {
Bindings b = ctx.getBindings((Integer)scope);
if (b != null) {
for (Object key: b.keySet()) {
set.add(key);
}
}
}
}
for (Iterator<String> names = parent.getNames().iterator(); names.hasNext();) {
set.add(names.next());
}
return Collections.unmodifiableSet(set);
}
@Override
public IRubyObject getDefaultSeparator() {
return parent.getDefaultSeparator();
}
});
}
private void setGlobalVariables(GlobalVariables globals) {
runtime.setGlobalVariables(globals);
}
private synchronized Object evalNode(Node node, ScriptContext ctx)
throws ScriptException {
GlobalVariables oldGlobals = runtime.getGlobalVariables();
try {
setWriterOutputStream(ctx.getWriter());
setErrorWriter(ctx.getErrorWriter());
setGlobalVariables(ctx);
return rubyToJava(runtime.runNormally(node, false));
} catch (Exception exp) {
throw new ScriptException(exp);
} finally {
try {
JavaEmbedUtils.terminate(runtime);
} catch (RaiseException e) {
RubyException re = e.getException();
runtime.printError(re);
if (!runtime.fastGetClass("SystemExit").isInstance(re)) {
throw new ScriptException(e);
}
} finally {
if (oldGlobals != null) {
setGlobalVariables(oldGlobals);
}
}
}
}
private void init(final String loadPath) {
AccessController.doPrivileged(new PrivilegedAction<Object>() {
public Object run() {
runtime = Ruby.newInstance();
IAccessor d = new ValueAccessor(runtime.newString("<script>"));
runtime.getGlobalVariables().define("$PROGRAM_NAME", d);
runtime.getGlobalVariables().define("$0", d);
String path = loadPath;
if (path == null) {
path = System.getProperty("java.class.path");
}
List list = Arrays.asList(path.split(File.pathSeparator));
runtime.getLoadService().init(list);
runtime.getLoadService().require("java");
return null;
}
});
}
private synchronized Object invokeImpl(final Object obj, String method,
Object[] args, Class returnType)
throws ScriptException {
if (method == null) {
throw new NullPointerException("method name is null");
}
GlobalVariables oldGlobals = runtime.getGlobalVariables();
try {
setWriterOutputStream(context.getWriter());
setErrorWriter(context.getErrorWriter());
setGlobalVariables(context);
IRubyObject rubyRecv = obj != null ?
JavaUtil.convertJavaToRuby(runtime, obj) : runtime.getTopSelf();
IRubyObject result;
if (args != null && args.length > 0) {
IRubyObject[] rubyArgs = JavaUtil.convertJavaArrayToRuby(runtime, args);
// Create Ruby proxies for any input arguments that are not primitives.
IRubyObject javaUtilities = runtime.getObject().getConstant("JavaUtilities");
for (int i = 0; i < rubyArgs.length; i++) {
IRubyObject tmp = rubyArgs[i];
if (tmp instanceof JavaObject) {
rubyArgs[i] = javaUtilities.callMethod(runtime.getCurrentContext(), "wrap", tmp);
}
}
result = rubyRecv.callMethod(runtime.getCurrentContext(), method, rubyArgs);
} else {
result = rubyRecv.callMethod(runtime.getCurrentContext(), method);
}
return rubyToJava(result, returnType);
} catch (Exception exp) {
throw new ScriptException(exp);
} finally {
try {
JavaEmbedUtils.terminate(runtime);
} catch (RaiseException e) {
RubyException re = e.getException();
runtime.printError(re);
if (!runtime.fastGetClass("SystemExit").isInstance(re)) {
throw new ScriptException(e);
}
} finally {
if (oldGlobals != null) {
setGlobalVariables(oldGlobals);
}
}
}
}
private void setKCode(String encoding) {
KCode kcode = KCode.create(runtime, encoding);
runtime.setKCode(kcode);
}
private void equalOutputs(RubyObject value) {
runtime.getGlobalVariables().set("$>", value);
runtime.getGlobalVariables().set("$defout", value);
}
private void setWriterOutputStream(Writer writer) {
try {
RubyIO dummy_io =
new RubyIO(runtime, new PrintStream(new WriterOutputStream(new StringWriter())));
runtime.getGlobalVariables().set("$stderr", dummy_io); //discard unwanted warnings
RubyIO io =
new RubyIO(runtime, new PrintStream(new WriterOutputStream(writer)));
io.getOpenFile().getMainStream().setSync(true);
runtime.defineGlobalConstant("STDOUT", io);
runtime.getGlobalVariables().set("$>", io);
runtime.getGlobalVariables().set("$stdout", io);
runtime.getGlobalVariables().set("$defout", io);
} catch (UnsupportedEncodingException exp) {
throw new IllegalArgumentException(exp);
}
}
private void setErrorWriter(Writer writer) {
try {
RubyIO dummy_io =
new RubyIO(runtime, new PrintStream(new WriterOutputStream(new StringWriter())));
runtime.getGlobalVariables().set("$stderr", dummy_io); //discard unwanted warnings
RubyIO io =
new RubyIO(runtime, new PrintStream(new WriterOutputStream(writer)));
io.getOpenFile().getMainStream().setSync(true);
runtime.defineGlobalConstant("STDERR", io);
runtime.getGlobalVariables().set("$stderr", io);
runtime.getGlobalVariables().set("$deferr", io);
} catch (UnsupportedEncodingException exp) {
throw new IllegalArgumentException(exp);
}
}
private String getEncoding() {
String enc = System.getProperty("sun.jnu.encoding");
if (enc != null) {
return enc;
}
return ((enc = System.getProperty("file.encoding")) == null) ? "UTF-8" : enc;
}
private class WriterOutputStream extends OutputStream {
private Writer writer;
private CharsetDecoder decoder;
private WriterOutputStream(Writer writer) throws UnsupportedEncodingException {
this(writer, getEncoding());
}
private WriterOutputStream(Writer writer, String enc) throws UnsupportedEncodingException {
this.writer = writer;
if (enc == null) {
throw new UnsupportedEncodingException("encoding is " + enc);
}
try {
decoder = Charset.forName(enc).newDecoder();
} catch (Exception e) {
throw new UnsupportedEncodingException("Unsupported: " + enc);
}
decoder.onMalformedInput(CodingErrorAction.REPLACE);
decoder.onUnmappableCharacter(CodingErrorAction.REPLACE);
}
@Override
public void close() throws IOException {
synchronized(writer) {
decoder = null;
writer.close();
}
}
@Override
public void flush() throws IOException {
synchronized(writer) {
writer.flush();
}
}
@Override
public void write(int b) throws IOException {
byte[] buffer = new byte[1];
write(buffer, 0, 1);
}
@Override
public void write(byte[] buffer) throws IOException {
write(buffer, 0, buffer.length);
}
@Override
public void write(byte[] buffer, int offset, int length) throws IOException {
synchronized(writer) {
if (offset < 0 || offset > buffer.length - length || length < 0) {
throw new IndexOutOfBoundsException();
}
if (length == 0) {
return;
}
ByteBuffer bytes = ByteBuffer.wrap(buffer, offset, length);
CharBuffer chars = CharBuffer.allocate(length);
convert(bytes, chars);
char[] cbuf = new char[chars.length()];
chars.get(cbuf, 0, chars.length());
writer.write(cbuf);
writer.flush();
}
}
private void convert(ByteBuffer bytes, CharBuffer chars) throws IOException {
decoder.reset();
chars.clear();
CoderResult result = decoder.decode(bytes, chars, true);
if (result.isError() || result.isOverflow()) {
throw new IOException(result.toString());
} else if (result.isUnderflow()) {
chars.flip();
}
}
}
}