/******************************************************************************* * 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$ } }