package client.net.sf.saxon.ce.js;
import client.net.sf.saxon.ce.Configuration;
import client.net.sf.saxon.ce.dom.HTMLDocumentWrapper;
import client.net.sf.saxon.ce.dom.HTMLDocumentWrapper.DocType;
import client.net.sf.saxon.ce.dom.HTMLNodeWrapper;
import client.net.sf.saxon.ce.dom.XMLDOM;
import client.net.sf.saxon.ce.expr.*;
import client.net.sf.saxon.ce.lib.NamespaceConstant;
import client.net.sf.saxon.ce.om.Item;
import client.net.sf.saxon.ce.om.SequenceIterator;
import client.net.sf.saxon.ce.om.StructuredQName;
import client.net.sf.saxon.ce.om.ValueRepresentation;
import client.net.sf.saxon.ce.trans.XPathException;
import client.net.sf.saxon.ce.tree.iter.EmptyIterator;
import client.net.sf.saxon.ce.tree.iter.JsArrayIterator;
import client.net.sf.saxon.ce.tree.iter.SingletonIterator;
import client.net.sf.saxon.ce.type.AnyItemType;
import client.net.sf.saxon.ce.type.ItemType;
import client.net.sf.saxon.ce.type.TypeHierarchy;
import client.net.sf.saxon.ce.value.*;
import client.net.sf.saxon.ce.value.StringValue;
import com.google.gwt.core.client.JavaScriptException;
import com.google.gwt.core.client.JavaScriptObject;
import com.google.gwt.core.client.JsArray;
import com.google.gwt.core.client.ScriptInjector;
import com.google.gwt.dom.client.Document;
import com.google.gwt.dom.client.Node;
import com.google.gwt.user.client.Event;
import java.util.logging.Logger;
/**
* This class implements Saxon-CE extension functions designed to allow interoperability between XSLT
* and JavaScript.
*/
public class IXSLFunction extends FunctionCall {
private String localName;
private Logger logger = Logger.getLogger("IXSLFunction");
private static int injectCount = 0;
public IXSLFunction(String localName, Expression[] arguments) {
this.localName = localName;
setArguments(arguments);
}
@Override
public StructuredQName getFunctionName() {
return new StructuredQName(NamespaceConstant.IXSL, "", localName);
}
@Override
protected void checkArguments(ExpressionVisitor visitor) throws XPathException {
}
@Override
public Expression preEvaluate(ExpressionVisitor visitor) throws XPathException {
return this;
}
@Override
public ItemType getItemType(TypeHierarchy th) {
return AnyItemType.getInstance();
}
@Override
protected int computeCardinality() {
return StaticProperty.ALLOWS_ZERO_OR_MORE;
}
@Override
protected int computeSpecialProperties() {
return StaticProperty.HAS_SIDE_EFFECTS;
}
private static native JavaScriptObject jsWindow()
/*-{
return $wnd;
}-*/;
/**
* Call a JavaScript function (a method on a JavaScript object)
* @param target the JavaScript object owning the method
* @param member the name of the function to be called as a string
* @param arguments the arguments to the method call
* @return the result of evaluating the expression. To ensure that this is always
* an Object, it is returned as a type/value pair
*/
private static native JavaScriptObject jsCall(JavaScriptObject target, String member, JavaScriptObject arguments)
/*-{
var v = target[member].apply(target, arguments);
return { type: typeof v, value: v}
}-*/;
private static native Object jsProperty(JavaScriptObject target, String member)
/*-{
return target[member];
}-*/;
/**
* Set a JavaScript property value (a property of a JavaScript object)
* @param target the JavaScript object owning the method
* @param member the name of the property to be fetched as a string - may be
* a chained set of properties e.g. loction.href
*/
public static native void setProperty(JavaScriptObject target, String member, Object value)
/*-{
var props = member.split('.');
var newTarget = target;
for (var count = 0; count < props.length - 1; count++){
newTarget = newTarget[props[count]];
}
newTarget[props[props.length -1]] = value;
}-*/;
/**
* Get a JavaScript property (a property of a JavaScript object)
* @param target the JavaScript object owning the method
* @param member the name of the property to be fetched as a string - may be
* a chained set of properties e.g. loction.href
* @return the result of evaluating the expression. To ensure that this is always
* an Object, it is returned as a type/value pair
*/
private static native JavaScriptObject jsSplitPropertyTypeAndValue(JavaScriptObject target, String member)
/*-{
var props = member.split('.');
var v = target;
for (var count = 0; count < props.length; count++){
v = v[props[count]];
}
return { type: typeof v, value: v}
}-*/;
private static native double jsNumberProperty(JavaScriptObject target, String member)
/*-{
return target[member];
}-*/;
private static native boolean jsBooleanProperty(JavaScriptObject target, String member)
/*-{
return target[member];
}-*/;
public static native JavaScriptObject jsArray(int length) /*-{
return new Array(length);
}-*/;
public static native void jsSetArrayItem(JavaScriptObject array, int index, Object value) /*-{
array[index] = value;
}-*/;
private static native Object jsGetArrayItem(JavaScriptObject array, int index) /*-{
return array[index];
}-*/;
private static native int jsGetArrayLength(JavaScriptObject array) /*-{
return array.length;
}-*/;
/**
* Special issues with determining if an array - because other object
* types such as Window are represented as arrays of length 1
*/
private static native boolean isJsArray(JavaScriptObject obj) /*-{
return (obj.length != undefined);
}-*/;
@SuppressWarnings("rawtypes")
public static SequenceIterator convertFromJavaScript(Object jsValue, Configuration config) {
if (jsValue == null) {
return EmptyIterator.getInstance();
} else if (jsValue instanceof String) {
return SingletonIterator.makeIterator(new StringValue((String)jsValue));
} else if (jsValue instanceof Double) {
return SingletonIterator.makeIterator(new DoubleValue((Double)jsValue));
} else if (jsValue instanceof Boolean) {
return SingletonIterator.makeIterator(BooleanValue.get((Boolean)jsValue));
} else if (!(jsValue instanceof JavaScriptObject)) {
return EmptyIterator.getInstance();
}
JavaScriptObject jsObj = (JavaScriptObject)jsValue;
short nodeType = getNodeType(jsObj);
if (nodeType == -1) {
if (isJsArray(jsObj) && jsGetArrayLength(jsObj) > 1) {
return new JsArrayIterator((JsArray)jsObj, config);
} else {
return SingletonIterator.makeIterator(new JSObjectValue(jsObj));
}
}
com.google.gwt.dom.client.Document page = ((Node)jsValue).getOwnerDocument();
if (page == null ) {
com.google.gwt.dom.client.Document doc = (Document)jsValue;
DocType jsDocType = (doc == Document.get())? DocType.UNKNOWN : DocType.NONHTML;
HTMLDocumentWrapper docWrapper = new HTMLDocumentWrapper(doc, doc.getURL(), config, jsDocType);
return SingletonIterator.makeIterator(docWrapper);
} else {
DocType jsDocType = (page == Document.get())? DocType.UNKNOWN : DocType.NONHTML;
HTMLDocumentWrapper htmlDoc = new HTMLDocumentWrapper(page, page.getURL(), config, jsDocType);
HTMLNodeWrapper htmlNode = htmlDoc.wrap((Node) jsValue);
return SingletonIterator.makeIterator(htmlNode);
}
}
private static native short getNodeType(JavaScriptObject obj) /*-{
var out = obj.nodeType;
return (out == null) ? -1 : out;
}-*/;
public static Object convertToJavaScript(ValueRepresentation val) throws XPathException {
if (val == null || val instanceof EmptySequence) {
return null;
}
if (val instanceof Item) {
if (val instanceof StringValue) {
return ((StringValue)val).getStringValue();
} else if (val instanceof BooleanValue) {
return ((BooleanValue)val).getBooleanValue();
} else if (val instanceof NumericValue) {
return ((NumericValue)val).getDoubleValue();
} else if (val instanceof HTMLNodeWrapper) {
return ((HTMLNodeWrapper)val).getUnderlyingNode();
} else if (val instanceof JSObjectValue) {
return ((JSObjectValue)val).getJavaScriptObject();
} else {
throw new XPathException("Cannot convert " + val.getClass() + " to Javascript object");
}
} else {
int seqLength = ((Value)val).getLength();
if (seqLength == 0) {
return null;
} else if (seqLength == 1) {
return convertToJavaScript(((Value)val).asItem());
} else {
return convertSequenceToArray(val, seqLength);
}
}
}
private static JavaScriptObject convertSequenceToArray(ValueRepresentation val, int seqLength) throws XPathException {
JavaScriptObject jsItems = jsArray(seqLength);
SequenceIterator iterator = Value.getIterator(val);
int i = 0;
while (true) {
Item item = iterator.next();
if (item == null) {
break;
}
Object jsObject = convertToJavaScript(item);
jsSetArrayItem(jsItems, i, jsObject);
i++;
}
return jsItems;
}
private Object getValueFromTypeValuePair(JavaScriptObject pair) {
String type = (String)jsProperty(pair, "type");
if ("number".equals(type)) {
return new Double(jsNumberProperty(pair, "value"));
} else if ("boolean".equals(type)) {
return Boolean.valueOf(jsBooleanProperty(pair, "value"));
} else {
return jsProperty(pair, "value");
}
}
private SequenceIterator evaluateJsFunction(String script, XPathContext context) throws XPathException {
injectCount++;
String fnName = "fnName" + injectCount;
script = script.trim();
String fnScript = "function " + fnName + "() { return " + script + "; }";
JSObjectValue item = new JSObjectValue(jsWindow());
JavaScriptObject target = (JavaScriptObject)convertToJavaScript(item);
ScriptInjector.FromString fs = new ScriptInjector.FromString(fnScript);
fs.setWindow(target);
fs.inject();
JavaScriptObject jsArgs = jsArray(0);
try {
Object result = getValueFromTypeValuePair(jsCall(target, fnName, jsArgs));
return convertFromJavaScript(result, context.getConfiguration());
} catch(JavaScriptException jexc) {
throw(new XPathException("JavaScriptException: " + jexc.getDescription() +
"\noccurred on evaluating:\n" + script));
}
}
public SequenceIterator iterate(XPathContext context) throws XPathException {
try {
if (localName.equals("window")) {
Item item = new JSObjectValue(jsWindow());
return SingletonIterator.makeIterator(item);
} else if (localName.equals("eval")) {
String script = argument[0].evaluateAsString(context).toString();
return evaluateJsFunction(script, context);
} else if (localName.equals("call")) {
ValueRepresentation itemVal = (ValueRepresentation)argument[0].evaluateItem(context);
JavaScriptObject target = (JavaScriptObject)convertToJavaScript(itemVal);
if (target != null) {
String method = argument[1].evaluateAsString(context).toString();
JavaScriptObject jsArgs = jsArray(argument.length - 2);
for (int i=2; i<argument.length; i++) {
ValueRepresentation val = SequenceExtent.makeSequenceExtent(argument[i].iterate(context));
jsSetArrayItem(jsArgs, i-2, convertToJavaScript(val));
}
// Issue: if following throws an exception - GWT doesn't always allow it to be caught - it's rethrown
// as a GWT unhandled exception
try {
JavaScriptObject jsObj = jsCall(target, method, jsArgs);
Object result = getValueFromTypeValuePair(jsObj);
return convertFromJavaScript(result, context.getConfiguration());
} catch(Exception e) {
boolean doRetry = false;
// try again setting any null values to zero-length arrays
for (int i = 0; i < argument.length - 2; i++) {
if (jsGetArrayItem(jsArgs, i) == null) {
jsSetArrayItem(jsArgs, i, jsArray(0));
doRetry = true;
}
}
if (doRetry) {
try {
Object result = getValueFromTypeValuePair(jsCall(target, method, jsArgs));
return convertFromJavaScript(result, context.getConfiguration());
} catch(Exception e2) {}
}
// if we get this far then throw exception - recovery failed
throw(new XPathException("JavaScriptException in ixsl:call(): Object does not support property or method '" +
method + "' with " + (argument.length - 2) + " argument(s)."));
}
} else {
throw(new XPathException("JavaScriptException in ixsl:call(): Call target object is null or undefined"));
}
} else if (localName.equals("get")) {
ValueRepresentation itemVal = (ValueRepresentation)argument[0].evaluateItem(context);
JavaScriptObject target = (JavaScriptObject)convertToJavaScript(itemVal);
if (target != null) {
String property = argument[1].evaluateAsString(context).toString();
Object result;
try {
result = getValueFromTypeValuePair(jsSplitPropertyTypeAndValue(target, property));
} catch(Exception e) {
throw(new XPathException("JavaScriptException in ixsl:get() for property: " + property));
}
return convertFromJavaScript(result, context.getConfiguration());
} else {
throw(new XPathException("JavaScriptException in ixsl:get(): Get target object is null or undefined"));
}
} else if (localName.equals("page")) {
return SingletonIterator.makeIterator(context.getConfiguration().getHostPage());
} else if (localName.equals("source")) {
return SingletonIterator.makeIterator(context.getController().getSourceNode());
} else if (localName.equals("event")) {
Event event = (Event)context.getController().getUserData("Saxon-CE", "current-event");
return SingletonIterator.makeIterator(new JSObjectValue(event));
} else if (localName.equals("parse-xml")) {
String data = argument[0].evaluateAsString(context).toString();
return convertFromJavaScript(XMLDOM.parseXML(data), context.getConfiguration());
}else {
// previously there was no warning - strictly - this should be caught at compile time
logger.warning("No such IXSL function: '" + localName + "' - empty sequence returned");
return EmptyIterator.getInstance();
} // end of else
} // end of try block
catch(XPathException e) {
e.maybeSetLocation(this.getSourceLocator());
e.maybeSetContext(context);
throw e;
} catch(Exception e) {
XPathException xe = new XPathException("Exception in ixsl:" + localName + "() " + e.getMessage());
xe.maybeSetLocation(this.getSourceLocator());
xe.maybeSetContext(context);
throw xe;
}
}
}
// This Source Code Form is subject to the terms of the Mozilla Public License, v. 2.0.
// If a copy of the MPL was not distributed with this file, You can obtain one at http://mozilla.org/MPL/2.0/.
// This Source Code Form is “Incompatible With Secondary Licenses”, as defined by the Mozilla Public License, v. 2.0.