/* GNU LESSER GENERAL PUBLIC LICENSE Copyright (C) 2006 The Lobo Project This library is free software; you can redistribute it and/or modify it under the terms of the GNU Lesser General Public License as published by the Free Software Foundation; either version 2.1 of the License, or (at your option) any later version. This library is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public License for more details. You should have received a copy of the GNU Lesser General Public License along with this library; if not, write to the Free Software Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA Contact info: lobochief@users.sourceforge.net */ package org.lobobrowser.js; import java.lang.reflect.InvocationTargetException; import java.lang.reflect.Method; import java.security.AccessController; import java.security.PrivilegedAction; import java.util.logging.Level; import java.util.logging.Logger; import org.lobobrowser.html.js.Window; import org.mozilla.javascript.EvaluatorException; import org.mozilla.javascript.ExternalArrayData; import org.mozilla.javascript.Function; import org.mozilla.javascript.Scriptable; import org.mozilla.javascript.ScriptableObject; import org.mozilla.javascript.WrappedException; public class JavaObjectWrapper extends ScriptableObject { private static final long serialVersionUID = -2669458528000105312L; private static final Logger logger = Logger.getLogger(JavaObjectWrapper.class.getName()); private static final boolean loggableInfo = logger.isLoggable(Level.INFO); private final Object delegate; private final JavaClassWrapper classWrapper; @Override public void setParentScope(final Scriptable m) { // Don't allow Window's parent scope to be changed. Fixes GH #29 if (classWrapper.getCanonicalClassName().equals(Window.class.getCanonicalName())) { return; } if (m == this) { // TODO: This happens when running jQuery 2 super.setParentScope(null); } else { super.setParentScope(m); } } public JavaObjectWrapper(final JavaClassWrapper classWrapper) throws InstantiationException, IllegalAccessException { this.classWrapper = classWrapper; // Retaining a strong reference, but note // that the object wrapper map uses weak keys // and weak values. final Object delegate = this.classWrapper.newInstance(); this.delegate = delegate; setupProperties(); } public JavaObjectWrapper(final JavaClassWrapper classWrapper, final Object delegate) { if (delegate == null) { throw new IllegalArgumentException("Argument delegate cannot be null."); } this.classWrapper = classWrapper; // Retaining a strong reference, but note // that the object wrapper map uses weak keys // and weak values. this.delegate = delegate; setupProperties(); } private void setupProperties() { final PropertyInfo integerIndexer = classWrapper.getIntegerIndexer(); if (integerIndexer != null) { setExternalArrayData(new ExternalArrayData() { @Override public int getArrayLength() { try { // TODO: Some length() methods are returning integer while others return length. A good test case is http://web-platform.test:8000/dom/nodes/Element-classlist.html // Check if length() methods can be converted to return a single type. final Object lengthObj = classWrapper.getProperty("length").getGetter().invoke(delegate, (Object[]) null); if (lengthObj instanceof Long) { final long lengthLong = (long) lengthObj; final int lengthInt = (int) lengthLong; // TODO: Check for overflow when casting to int and throw an exception return lengthInt; } else if (lengthObj instanceof Integer) { return (int) lengthObj; } else { // TODO: Throw exception throw new RuntimeException("Can't represent length as an integer type"); } } catch (IllegalAccessException | IllegalArgumentException | InvocationTargetException e) { // TODO Auto-generated catch block e.printStackTrace(); return 0; } } @Override public Object getArrayElement(final int index) { if (index < 0) { // TODO: The interface's javadoc says that this method is only called for indices are within range. // Need to check if negative values are considered in range. Negative indices are being used in // one of the web-platform-tests return org.mozilla.javascript.Undefined.instance; } try { final Object result = JavaScript.getInstance().getJavascriptObject( integerIndexer.getGetter().invoke(delegate, new Object[] { index }), null); return result; } catch (IllegalAccessException | IllegalArgumentException | InvocationTargetException e) { throw new RuntimeException("Error accessing a indexed element"); } } @Override public void setArrayElement(final int index, final Object value) { // TODO: Can this be supported? Needs a setter. throw new UnsupportedOperationException("Writing to an indexed object"); } }); } classWrapper.getProperties().forEach((name, property) -> { // TODO: Don't setup properties if getter is null? Are write-only properties supported in JS? defineProperty(name, null, property.getGetter(), property.getSetter(), 0); }); classWrapper.getStaticFinalProperties().forEach((name, field) -> { try { defineProperty(name, field.get(null), READONLY); } catch (final Exception e) { throw new RuntimeException(e); } }); } /** * Returns the Java object. * * @return An object or <code>null</code> if garbage collected. */ public Object getJavaObject() { // Cannot retain delegate with a strong reference. return this.delegate; } @Override public String getClassName() { return this.classWrapper.getClassName(); } /* @Override public Object get(final int index, final Scriptable start) { final PropertyInfo pinfo = this.classWrapper.getIntegerIndexer(); if (pinfo == null) { return super.get(index, start); } else { try { final Method getter = pinfo.getGetter(); if (getter == null) { throw new EvaluatorException("Indexer is write-only"); } // Cannot retain delegate with a strong reference. final Object javaObject = this.getJavaObject(); if (javaObject == null) { throw new IllegalStateException("Java object (class=" + this.classWrapper + ") is null."); } final Object raw = getter.invoke(javaObject, new Object[] { new Integer(index) }); if (raw == null) { // Return this instead of null. return Scriptable.NOT_FOUND; } return JavaScript.getInstance().getJavascriptObject(raw, this.getParentScope()); } catch (final Exception err) { throw new WrappedException(err); } } }*/ @Override public Object get(final String name, final Scriptable start) { final PropertyInfo pinfo = this.classWrapper.getProperty(name); if (pinfo != null) { final Method getter = pinfo.getGetter(); if (getter == null) { throw new EvaluatorException("Property '" + name + "' is not readable"); } // Cannot retain delegate with a strong reference. final Object javaObject = this.getJavaObject(); if (javaObject == null) { throw new IllegalStateException("Java object (class=" + this.classWrapper + ") is null."); } final Object val = AccessController.doPrivileged(new PrivilegedAction<Object>() { public Object run() { try { return getter.invoke(javaObject, (Object[]) null); } catch (final Exception err) { throw new WrappedException(err); } } }); return JavaScript.getInstance().getJavascriptObject(val, start.getParentScope()); } else { final Function f = this.classWrapper.getFunction(name); if (f != null) { return f; } else { // Should check properties set in context // first. Consider element IDs should not // override Window variables set by user. final Object result = super.get(name, start); if (result != Scriptable.NOT_FOUND) { return result; } final PropertyInfo ni = this.classWrapper.getNameIndexer(); if (ni != null) { final Method getter = ni.getGetter(); if (getter != null) { // Cannot retain delegate with a strong reference. final Object javaObject = this.getJavaObject(); if (javaObject == null) { throw new IllegalStateException("Java object (class=" + this.classWrapper + ") is null."); } try { final Object val = getter.invoke(javaObject, new Object[] { name }); if (val == null) { // There might not be an indexer setter. return super.get(name, start); } else { return JavaScript.getInstance().getJavascriptObject(val, start.getParentScope()); } } catch (final Exception err) { throw new WrappedException(err); } } } return Scriptable.NOT_FOUND; } } } @Override public void put(final int index, final Scriptable start, final Object value) { final PropertyInfo pinfo = this.classWrapper.getIntegerIndexer(); if (pinfo == null) { super.put(index, start, value); } else { try { final Method setter = pinfo.getSetter(); if (setter == null) { throw new EvaluatorException("Indexer is read-only"); } Object actualValue; actualValue = JavaScript.getInstance().getJavaObject(value, pinfo.getPropertyType()); setter.invoke(this.getJavaObject(), new Object[] { new Integer(index), actualValue }); } catch (final Exception err) { throw new WrappedException(err); } } } @Override public void put(final String name, final Scriptable start, final Object value) { if (value instanceof org.mozilla.javascript.Undefined) { super.put(name, start, value); } else { final PropertyInfo pinfo = this.classWrapper.getProperty(name); if (pinfo != null) { final Method setter = pinfo.getSetter(); if (setter == null) { throw new EvaluatorException("Property '" + name + "' is not settable in " + this.classWrapper.getClassName() + "."); } try { final Object actualValue = JavaScript.getInstance().getJavaObject(value, pinfo.getPropertyType()); setter.invoke(this.getJavaObject(), new Object[] { actualValue }); } catch (final IllegalArgumentException iae) { final Exception newException = new IllegalArgumentException("Property named '" + name + "' could not be set with value " + value + ".", iae); throw new WrappedException(newException); } catch (final Exception err) { throw new WrappedException(err); } } else { final PropertyInfo ni = this.classWrapper.getNameIndexer(); if (ni != null) { final Method setter = ni.getSetter(); if (setter != null) { try { Object actualValue; actualValue = JavaScript.getInstance().getJavaObject(value, ni.getPropertyType()); setter.invoke(this.getJavaObject(), new Object[] { name, actualValue }); } catch (final Exception err) { throw new WrappedException(err); } } else { super.put(name, start, value); } } else { super.put(name, start, value); } } } } public static Function getConstructor(final String className, final JavaClassWrapper classWrapper, final Scriptable scope) { return new JavaConstructorObject(className, classWrapper); } public static Function getConstructor(final String className, final JavaClassWrapper classWrapper, final Scriptable scope, final JavaInstantiator instantiator) { return new JavaConstructorObject(className, classWrapper, instantiator); } @Override public java.lang.Object getDefaultValue(final java.lang.Class<?> hint) { if (loggableInfo) { logger.info("getDefaultValue(): hint=" + hint + ",this=" + this.getJavaObject()); } if ((hint == null) || String.class.equals(hint)) { final Object javaObject = this.getJavaObject(); if (javaObject == null) { throw new IllegalStateException("Java object (class=" + this.classWrapper + ") is null."); } return javaObject.toString(); } else if (Number.class.isAssignableFrom(hint)) { final Object javaObject = this.getJavaObject(); if (javaObject instanceof Number) { return javaObject; } else if (javaObject instanceof String) { return Double.valueOf((String) javaObject); } else { return super.getDefaultValue(hint); } } else { return super.getDefaultValue(hint); } } @Override public String toString() { final Object javaObject = this.getJavaObject(); final String type = javaObject == null ? "<null>" : javaObject.getClass().getName(); return "JavaObjectWrapper[object=" + this.getJavaObject() + ",type=" + type + "]"; } @Override public boolean hasInstance(final Scriptable instance) { if ((instance instanceof JavaObjectWrapper) && (this.getJavaObject() instanceof Class)) { final JavaObjectWrapper instanceObj = (JavaObjectWrapper) instance; final Class<?> myClass = (Class<?>) this.getJavaObject(); return myClass.isInstance(instanceObj.getJavaObject()); } else { return super.hasInstance(instance); } } // TODO: Override has(int index) also @Override public boolean has(String name, Scriptable start) { // TODO: should the start parameter be considered here? if (classWrapper.getProperties().containsKey(name) || classWrapper.getStaticFinalProperties().containsKey(name)) { return true; } return super.has(name, start); } }