blob: 46e98663935464670f4d670465e68aca95d98ee1 [file] [log] [blame]
/*******************************************************************************
* Copyright (c) 2015, 2016 QNX Software Systems and others.
* All rights reserved. This program and the accompanying materials
* are made available under the terms of the Eclipse Public License v1.0
* which accompanies this distribution, and is available at
* http://www.eclipse.org/legal/epl-v10.html
*
* Contributors:
* QNX Software Systems - Initial API and implementation
*******************************************************************************/
package org.eclipse.cdt.internal.qt.core;
import java.lang.invoke.MethodHandles;
import java.lang.reflect.Array;
import java.lang.reflect.Constructor;
import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Method;
import java.lang.reflect.Proxy;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import javax.script.Bindings;
import org.eclipse.cdt.internal.qt.core.location.Position;
import org.eclipse.cdt.internal.qt.core.location.SourceLocation;
import org.eclipse.cdt.qt.core.IQMLAnalyzer;
import org.eclipse.cdt.qt.core.location.ISourceLocation;
import org.eclipse.cdt.qt.core.qmljs.IJSLiteral;
import org.eclipse.cdt.qt.core.qmljs.IJSRegExpLiteral;
import org.eclipse.cdt.qt.core.qmljs.IQmlASTNode;
import org.eclipse.cdt.qt.core.qmljs.IQmlObjectDefinition;
import org.eclipse.cdt.qt.core.qmljs.IQmlRootObject;
/**
* Translates a JavaScript {@link Bindings} object into a QML AST. This class employs {@link java.lang.reflect.Proxy} in order to
* dynamically create the AST at runtime.
* <p>
* To begin translation simply call the static method <code>createQmlASTProxy</code>. The AST is translated only when it needs to be
* (i.e. when one of its 'get' methods are called).
*/
public class QmlASTNodeHandler implements InvocationHandler {
private static final String NODE_QML_PREFIX = "QML"; //$NON-NLS-1$
private static final String NODE_TYPE_PROPERTY = "type"; //$NON-NLS-1$
private static final String NODE_REGEX_PROPERTY = "regex"; //$NON-NLS-1$
private static final String CREATE_ENUM_METHOD = "fromObject"; //$NON-NLS-1$
private static final String AST_PACKAGE = "org.eclipse.cdt.qt.core.qmljs."; //$NON-NLS-1$
private static final String AST_QML_PREFIX = "IQml"; //$NON-NLS-1$
private static final String AST_JS_PREFIX = "IJS"; //$NON-NLS-1$
private static String getPropertyName(String method) {
String name = ""; //$NON-NLS-1$
if (method.startsWith("is")) { //$NON-NLS-1$
name = method.substring(2, 3).toLowerCase() + method.substring(3);
} else if (method.startsWith("get")) { //$NON-NLS-1$
name = method.substring(3, 4).toLowerCase() + method.substring(4);
}
if (name.equalsIgnoreCase("identifier")) { //$NON-NLS-1$
return "id"; //$NON-NLS-1$
} else if (name.equalsIgnoreCase("location")) { //$NON-NLS-1$
return "loc"; //$NON-NLS-1$
}
return name;
}
/**
* Constructs a new {@link IQmlASTNode} from the given {@link Bindings}. This is a helper method equivalent to
* <code>createQmlASTProxy(node, null)</code>
*
* @param node
* the AST node as retrieved from Nashorn
* @return a Proxy representing the given node
* @throws ClassNotFoundException
* if the node does not represent a valid QML AST Node
* @see {@link QmlASTNodeHandler#createQmlASTProxy(Bindings, Class)}
*/
public static IQmlASTNode createQmlASTProxy(Bindings node) throws ClassNotFoundException {
return createQmlASTProxy(node, null);
}
/**
* Constructs a new {@link IQmlASTNode} from the given {@link Bindings}. If a return type is specified, it will take precedence
* over the type retrieved from the binding. This is useful for nodes that extend, but do not add functionality to, an acorn AST
* element. A good example of this is {@link IQmlRootObject} which extends {@link IQmlObjectDefinition}. We can easily determine
* the location in the AST at which we want an IQmlRootObject over an IQmlObjectDefinition and set the returnType accordingly.
*
* @param node
* the node as retrieved from acorn
* @param returnType
* the expected node to return or null
* @return a Proxy representing the given node
* @throws ClassNotFoundException
* if the node does not represent a valid QML AST Node
*/
public static IQmlASTNode createQmlASTProxy(Bindings node, Class<?> returnType) throws ClassNotFoundException {
String type = (String) node.getOrDefault(NODE_TYPE_PROPERTY, ""); //$NON-NLS-1$
if (type.startsWith(NODE_QML_PREFIX)) {
type = AST_QML_PREFIX + type.substring(3);
} else {
type = AST_JS_PREFIX + type;
}
Class<?> astClass = Class.forName(AST_PACKAGE + type);
if (astClass.equals(IJSLiteral.class)) {
// If this is a Literal, we have to distinguish it between a RegExp Literal using the 'regex' property
if (node.get(NODE_REGEX_PROPERTY) != null) {
astClass = IJSRegExpLiteral.class;
}
}
if (returnType != null) {
if (!IQmlASTNode.class.isAssignableFrom(astClass)) {
throw new ClassCastException(astClass + " cannot be cast to " + IQmlASTNode.class); //$NON-NLS-1$
}
if (astClass.isAssignableFrom(returnType)) {
astClass = returnType;
}
}
return (IQmlASTNode) Proxy.newProxyInstance(QmlASTNodeHandler.class.getClassLoader(),
new Class<?>[] { astClass },
new QmlASTNodeHandler(node));
}
private final QMLAnalyzer analyzer;
private final Bindings node;
private final Map<String, Object> methodResults;
private QmlASTNodeHandler(Bindings node) {
this.analyzer = (QMLAnalyzer) Activator.getService(IQMLAnalyzer.class);
this.node = node;
this.methodResults = new HashMap<>();
}
@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
String mName = method.getName();
if (!methodResults.containsKey(method.getName())) {
// Invoke the default implementation of the method if possible
if (method.isDefault()) {
final Class<?> declaringClass = method.getDeclaringClass();
Constructor<MethodHandles.Lookup> constructor = MethodHandles.Lookup.class.getDeclaredConstructor(Class.class,
int.class);
constructor.setAccessible(true);
methodResults.put(mName, constructor.newInstance(declaringClass, MethodHandles.Lookup.PRIVATE)
.unreflectSpecial(method, declaringClass)
.bindTo(proxy)
.invokeWithArguments(args));
} else {
// Use the return type of the method as well as its contents of the node to get the Object to return
String pName = getPropertyName(mName);
methodResults.put(mName, handleObject(node.get(pName), method.getReturnType()));
}
}
return methodResults.get(mName);
}
private Object handleObject(Object value, Class<?> expectedType) throws Throwable {
if (expectedType.isArray()) {
Object arr = Array.newInstance(expectedType.getComponentType(), ((Bindings) value).size());
int ctr = 0;
for (Object obj : ((Bindings) value).values()) {
Array.set(arr, ctr++, handleObject(obj, expectedType.getComponentType()));
}
return arr;
} else if (expectedType.equals(Object.class)) {
return value;
} else if (expectedType.isAssignableFrom(ISourceLocation.class)) {
// ISourceLocation doesn't correspond to an AST Node and needs to be created manually from
// the given Bindings.
if (value instanceof Bindings) {
Bindings bind = (Bindings) value;
SourceLocation loc = new SourceLocation();
loc.setSource((String) bind.get("source")); //$NON-NLS-1$
Bindings start = (Bindings) bind.get("start"); //$NON-NLS-1$
loc.setStart(new Position(((Number) start.get("line")).intValue(), //$NON-NLS-1$
((Number) start.get("column")).intValue())); //$NON-NLS-1$
Bindings end = (Bindings) bind.get("end"); //$NON-NLS-1$
loc.setEnd(new Position(((Number) end.get("line")).intValue(), //$NON-NLS-1$
((Number) end.get("column")).intValue())); //$NON-NLS-1$
return loc;
}
return new SourceLocation();
} else if (expectedType.isAssignableFrom(List.class)) {
if (value instanceof Bindings) {
List<Object> list = new ArrayList<>();
for (Bindings object : analyzer.toJavaArray((Bindings) value, Bindings[].class)) {
list.add(QmlASTNodeHandler.createQmlASTProxy(object));
}
return list;
}
return null;
} else if (expectedType.isPrimitive()) {
return handlePrimitive(value, expectedType);
} else if (expectedType.isAssignableFrom(Number.class)) {
if (value instanceof Number) {
return value;
}
return 0;
} else if (expectedType.isEnum()) {
return expectedType.getMethod(CREATE_ENUM_METHOD, Object.class).invoke(null, value);
} else if (value instanceof Bindings) {
return QmlASTNodeHandler.createQmlASTProxy((Bindings) value, expectedType);
}
return value;
}
private Object handlePrimitive(Object value, Class<?> expectedType) throws Throwable {
if (expectedType.isPrimitive()) {
if (expectedType.equals(Boolean.TYPE)) {
if (value instanceof Boolean) {
return value;
}
return false;
} else if (expectedType.equals(Character.TYPE)) {
if (value instanceof Character) {
return value;
}
return '\0';
} else if (expectedType.equals(Byte.TYPE)) {
if (value instanceof Number) {
return ((Number) value).byteValue();
}
return (byte) 0;
} else if (expectedType.equals(Short.TYPE)) {
if (value instanceof Number) {
return ((Number) value).shortValue();
}
return (short) 0;
} else if (expectedType.equals(Integer.TYPE)) {
if (value instanceof Number) {
return ((Number) value).intValue();
}
return 0;
} else if (expectedType.equals(Long.TYPE)) {
if (value instanceof Number) {
return ((Number) value).longValue();
}
return 0l;
} else if (expectedType.equals(Float.TYPE)) {
if (value instanceof Number) {
return ((Number) value).floatValue();
}
return 0.0f;
} else if (expectedType.equals(Double.TYPE)) {
if (value instanceof Number) {
return ((Number) value).doubleValue();
}
return 0.0d;
}
}
throw new IllegalArgumentException("expectedType was not a primitive type"); //$NON-NLS-1$
}
}