/*
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.Field;
import java.lang.reflect.Method;
import java.lang.reflect.Modifier;
import java.security.AccessControlException;
import java.util.HashMap;
import java.util.Map;
import org.lobobrowser.html.js.NotGetterSetter;
import org.lobobrowser.html.js.PropertyName;
import org.mozilla.javascript.Function;
import org.mozilla.javascript.Scriptable;
public class JavaClassWrapper {
private final Class<?> javaClass;
private final Map<String, JavaFunctionObject> functions = new HashMap<>();
private final Map<String, PropertyInfo> properties = new HashMap<>();
private final Map<String, Field> staticFinalProperties = new HashMap<>();
private PropertyInfo nameIndexer;
private PropertyInfo integerIndexer;
public JavaClassWrapper(final Class<?> class1) {
super();
this.javaClass = class1;
this.scanMethods();
}
public Object newInstance() throws InstantiationException, IllegalAccessException {
return this.javaClass.newInstance();
}
public String getClassName() {
final String className = this.javaClass.getName();
final int lastDotIdx = className.lastIndexOf('.');
return lastDotIdx == -1 ? className : className.substring(lastDotIdx + 1);
}
public String getCanonicalClassName() {
return this.javaClass.getCanonicalName();
}
public Function getFunction(final String name) {
return this.functions.get(name);
}
public PropertyInfo getProperty(final String name) {
return this.properties.get(name);
}
private static Field[] extractFields(final Class<?> jClass) {
try {
return jClass.getFields();
} catch (final AccessControlException ace) {
// TODO: Try looking at individual interfaces implemented by the class
//return new Field[0];
throw new RuntimeException("Couldn't access fields of a class");
}
}
private static Method[] extractMethods(final Class<?> jClass) {
try {
return jClass.getMethods();
} catch (final AccessControlException ace) {
// TODO: Try looking at individual interfaces implemented by the class
// return new Method[0];
throw new RuntimeException("Couldn't access methods of a class");
}
}
private void scanMethods() {
final Field[] fields = extractFields(javaClass);
for (final Field f : fields) {
final int modifiers = f.getModifiers();
if (Modifier.isPublic(modifiers) && Modifier.isStatic(modifiers) && Modifier.isFinal(modifiers)) {
staticFinalProperties.put(f.getName(), f);
}
}
final Method[] methods = extractMethods(javaClass);
final int len = methods.length;
for (int i = 0; i < len; i++) {
final Method method = methods[i];
// TODO: Need a more robust blocking mechanism. GH #125
final boolean blocked = method.getDeclaringClass().getCanonicalName().startsWith("java");
if (!(blocked || method.isAnnotationPresent(HideFromJS.class))) {
final String name = method.getName();
if (isPropertyMethod(name, method)) {
this.ensurePropertyKnown(name, method);
} else {
if (isNameIndexer(name, method)) {
this.updateNameIndexer(name, method);
} else if (isIntegerIndexer(name, method)) {
this.updateIntegerIndexer(name, method);
}
JavaFunctionObject f = this.functions.get(name);
if (f == null) {
f = new JavaFunctionObject(name, javaClass.getName());
this.functions.put(name, f);
}
f.addMethod(method);
}
}
}
}
private static boolean isNameIndexer(final String name, final Method method) {
return ("namedItem".equals(name) && (method.getParameterTypes().length == 1))
|| ("setNamedItem".equals(name) && (method.getParameterTypes().length == 2));
}
private static boolean isIntegerIndexer(final String name, final Method method) {
return ("item".equals(name) && (method.getParameterTypes().length == 1))
|| ("setItem".equals(name) && (method.getParameterTypes().length == 2));
}
private void updateNameIndexer(final String methodName, final Method method) {
final boolean getter = !methodName.startsWith("set");
PropertyInfo indexer = this.nameIndexer;
if (indexer == null) {
indexer = new PropertyInfo("$item", Object.class);
this.nameIndexer = indexer;
}
if (getter) {
indexer.setGetter(method);
} else {
indexer.setSetter(method);
}
}
private void updateIntegerIndexer(final String methodName, final Method method) {
final boolean getter = !methodName.startsWith("set");
PropertyInfo indexer = this.integerIndexer;
if (indexer == null) {
final Class<?> pt = getter ? method.getReturnType() : method.getParameterTypes()[1];
indexer = new PropertyInfo("$item", pt);
this.integerIndexer = indexer;
}
if (getter) {
indexer.setGetter(method);
} else {
indexer.setSetter(method);
}
}
public PropertyInfo getIntegerIndexer() {
return this.integerIndexer;
}
public PropertyInfo getNameIndexer() {
return this.nameIndexer;
}
private static boolean isPropertyMethod(final String name, final Method method) {
if (method.isAnnotationPresent(NotGetterSetter.class)) {
return false;
} else {
if (name.startsWith("get") || name.startsWith("is")) {
return method.getParameterTypes().length == 0;
} else if (name.startsWith("set")) {
return method.getParameterTypes().length == 1;
} else {
return false;
}
}
}
private static String propertyUncapitalize(final String text) {
try {
if ((text.length() > 1) && Character.isUpperCase(text.charAt(1))) {
// If second letter is capitalized, don't uncapitalize,
// e.g. getURL.
return text;
}
return Character.toLowerCase(text.charAt(0)) + text.substring(1);
} catch (final IndexOutOfBoundsException iob) {
return text;
}
}
private void ensurePropertyKnown(final String methodName, final Method method) {
String capPropertyName;
boolean getter = false;
boolean setter = false;
if (methodName.startsWith("get")) {
capPropertyName = methodName.substring(3);
getter = true;
} else if (methodName.startsWith("set")) {
capPropertyName = methodName.substring(3);
setter = method.getReturnType() == Void.TYPE;
} else if (methodName.startsWith("is")) {
capPropertyName = methodName.substring(2);
getter = true;
} else {
throw new IllegalArgumentException("methodName=" + methodName);
}
final PropertyName propertyNameAnnotation = method.getAnnotation(PropertyName.class);
final String propertyName = (propertyNameAnnotation != null) ? propertyNameAnnotation.value() : propertyUncapitalize(capPropertyName);
PropertyInfo pinfo = this.properties.get(propertyName);
if (pinfo == null) {
final Class<?> pt = getter ? method.getReturnType() : method.getParameterTypes()[0];
pinfo = new PropertyInfo(propertyName, pt);
this.properties.put(propertyName, pinfo);
}
if (getter) {
pinfo.setGetter(method);
}
if (setter) {
pinfo.setSetter(method);
}
}
@Override
public String toString() {
return this.javaClass.getName();
}
Map<String, PropertyInfo> getProperties() {
return properties;
}
boolean hasInstance(final Scriptable instance) {
if (instance instanceof JavaObjectWrapper) {
final JavaObjectWrapper javaObjectWrapper = (JavaObjectWrapper) instance;
return javaClass.isInstance(javaObjectWrapper.getJavaObject());
}
return javaClass.isInstance(instance);
}
Map<String, Field> getStaticFinalProperties() {
return staticFinalProperties;
}
}