/******************************************************************************* * Copyright (c) 2011-2014 Fernando Petrola * * This file is part of Dragome SDK. * * All rights reserved. This program and the accompanying materials * are made available under the terms of the GNU Public License v3.0 * which accompanies this distribution, and is available at * http://www.gnu.org/licenses/gpl.html ******************************************************************************/ package com.dragome.web.enhancers.jsdelegate.serverside; import java.io.File; import java.lang.reflect.Method; import java.util.ArrayList; import java.util.Arrays; import java.util.HashSet; import java.util.List; import java.util.Set; import com.dragome.commons.DelegateCode; import com.dragome.commons.compiler.classpath.InMemoryClasspathFile; import com.dragome.commons.javascript.ScriptHelper; import com.dragome.web.enhancers.jsdelegate.DefaultDelegateStrategy; import com.dragome.web.enhancers.jsdelegate.JsCast; import com.dragome.web.enhancers.jsdelegate.interfaces.DelegateStrategy; import com.dragome.web.enhancers.jsdelegate.interfaces.SubTypeFactory; import javassist.CannotCompileException; import javassist.ClassClassPath; import javassist.ClassPool; import javassist.CtClass; import javassist.CtConstructor; import javassist.CtMethod; import javassist.CtNewConstructor; import javassist.CtNewMethod; import javassist.NotFoundException; import javassist.bytecode.AccessFlag; import javassist.bytecode.Descriptor; public class JsDelegateGenerator { private DelegateStrategy delegateStrategy; public JsDelegateGenerator(DelegateStrategy delegateStrategy) { this.delegateStrategy= delegateStrategy; } public JsDelegateGenerator() { this(new DefaultDelegateStrategy()); } public JsDelegateGenerator(String classpath, DelegateStrategy delegateStrategy) { this(classpath); this.delegateStrategy= delegateStrategy; } public JsDelegateGenerator(String classpath) { this(); appendClasspath(classpath); } private void appendClasspath(String classpath) { try { ClassPool pool= ClassPool.getDefault(); pool.appendPathList(classpath); } catch (NotFoundException e) { throw new RuntimeException(e); } } public JsDelegateGenerator(File baseDir) { this(); } public byte[] generateBytecode(String className, Class<?> interface1) { try { CtClass cc= generateCtClass(className, interface1); byte[] bytecode= cc.toBytecode(); return bytecode; } catch (Exception e) { throw new RuntimeException(e); } } private CtClass generateCtClass(String className, Class<?> interface1) { try { ClassPool pool= ClassPool.getDefault(); CtClass cc= pool.makeClass(className); CtClass resolveCtClass= resolveCtClass(interface1); cc.addInterface(resolveCtClass); Class<?> scriptHelperClass= ScriptHelper.class; pool.insertClassPath(new ClassClassPath(Object.class)); pool.insertClassPath(new ClassClassPath(scriptHelperClass)); StringBuilder constructorBody= new StringBuilder(); constructorBody.append(scriptHelperClass.getName() + ".put(\"$1\", $1, this);"); constructorBody.append(scriptHelperClass.getName() + ".eval(\"this.node= $1\", this);"); CtClass objectCtClass= resolveCtClass(Object.class); addConstructors(cc, constructorBody, objectCtClass); Set<CtMethod> methods= findAllMethods(resolveCtClass, new HashSet<CtMethod>()); for (CtMethod method : methods) { DelegateCode annotation= (DelegateCode) method.getAnnotation(DelegateCode.class); if (annotation != null && annotation.ignore()) continue; if (!method.getDeclaringClass().equals(objectCtClass)) cc.addMethod(createDelegateMethod(interface1, cc, scriptHelperClass, method, annotation)); } return cc; } catch (Exception e) { throw new RuntimeException(e); } } private Set<CtMethod> findAllMethods(CtClass resolveCtClass, Set<CtMethod> methods) throws NotFoundException { if (resolveCtClass != null) { methods.addAll(Arrays.asList(resolveCtClass.getMethods())); findAllMethods(resolveCtClass.getSuperclass(), methods); CtClass[] interfaces= resolveCtClass.getInterfaces(); for (CtClass ctClass : interfaces) findAllMethods(ctClass, methods); } return methods; } private CtMethod createDelegateMethod(Class<?> interface1, CtClass cc, Class<?> scriptHelperClass, CtMethod method, DelegateCode delegateCodeAnnotation) throws Exception { StringBuffer code= new StringBuffer(); String params= createParameters(method, code); String customCode= null; CtClass returnType= method.getReturnType(); String body= delegateStrategy.createMethodBody(toJavaMethod(interface1, method), params); if (body == null) { if (delegateCodeAnnotation != null && !delegateCodeAnnotation.eval().isEmpty()) code.append("$eval$(\"" + delegateCodeAnnotation.eval() + "\", this);"); else { customCode= delegateStrategy.createMethodCall(toJavaMethod(interface1, method), params); code.append("$eval$(\"" + customCode + "\", this);"); } body= code.toString(); body= configureEvaluation(interface1, scriptHelperClass, method, body, returnType); } body= "{" + body + "}"; CtMethod ctMethod= CtNewMethod.make(AccessFlag.PUBLIC, returnType, method.getName(), method.getParameterTypes(), method.getExceptionTypes(), body, cc); return ctMethod; } private Method toJavaMethod(Class<?> interface1, CtMethod method) throws Exception { CtClass[] parameterTypes= method.getParameterTypes(); return interface1.getMethod(method.getName(), toJavaClass(parameterTypes)); } private Class<?>[] toJavaClass(CtClass[] parameterTypes) throws Exception { List<Class<?>> result= new ArrayList<>(); for (CtClass ctClass : parameterTypes) { String javaName= toJavaName(ctClass); result.add(getJavaClass(javaName)); } return result.toArray(new Class[0]); } public static Class<?> getJavaClass(String classname) throws ClassNotFoundException { if (classname.equals("boolean")) return boolean.class; else if (classname.equals("int")) return int.class; else if (classname.equals("short")) return short.class; else if (classname.equals("long")) return long.class; else if (classname.equals("float")) return float.class; else if (classname.equals("double")) return double.class; else if (classname.equals("byte")) return byte.class; else return Class.forName(classname); } private void addConstructors(CtClass cc, StringBuilder constructorBody, CtClass objectCtClass) throws CannotCompileException { CtConstructor ctConstructor= CtNewConstructor.make(new CtClass[] { objectCtClass }, new CtClass[] {}, "{" + constructorBody.toString() + "}", cc); cc.addConstructor(ctConstructor); cc.addConstructor(CtNewConstructor.make(new CtClass[] {}, new CtClass[] {}, "{}", cc)); } private String configureEvaluation(Class<?> interface1, Class<?> scriptHelperClass, CtMethod method, String body, CtClass returnType) throws Exception { String scriptHelperClassname= scriptHelperClass.getName(); String returnTypeName= toJavaName(returnType); if (!returnTypeName.equals(Void.class.getName()) && !returnTypeName.equals(void.class.getName())) { if (returnTypeName.equals(Boolean.class.getName()) || returnTypeName.equals(boolean.class.getName())) body= body.replace("$eval$", "return " + scriptHelperClassname + ".evalBoolean"); else { boolean isInteger= returnTypeName.equals(Integer.class.getName()) || returnTypeName.equals(int.class.getName()); boolean isShort= returnTypeName.equals(Short.class.getName()) || returnTypeName.equals(short.class.getName()); boolean isByte= returnTypeName.equals(Byte.class.getName()) || returnTypeName.equals(byte.class.getName()); if (isInteger || isShort || isByte) body= body.replace("$eval$", "return " + scriptHelperClassname + ".evalInt"); else if (returnTypeName.equals(Double.class.getName()) || returnTypeName.equals(double.class.getName())) body= body.replace("$eval$", "return " + scriptHelperClassname + ".evalDouble"); else if (returnTypeName.equals(Float.class.getName()) || returnTypeName.equals(float.class.getName())) body= body.replace("$eval$", "return " + scriptHelperClassname + ".evalFloat"); else if (returnTypeName.equals(Long.class.getName()) || returnTypeName.equals(long.class.getName())) body= body.replace("$eval$", "return " + scriptHelperClassname + ".evalLong"); else if (returnTypeName.equals(String.class.getName())) body= body.replace("$eval$", "return (java.lang.String)" + scriptHelperClassname + ".eval"); else body= createBodyForReference(interface1, scriptHelperClass, method, body, returnType); } } else body= body.replace("$eval$", scriptHelperClassname + ".evalNoResult"); return body; } private String createBodyForReference(Class<?> interface1, Class<?> scriptHelperClass, CtMethod method, String body, CtClass returnType) throws Exception { body= body.replace("$eval$", "Object temp= " + scriptHelperClass.getName() + ".eval"); String returnTypeAsString= toJavaName(returnType) + ".class"; String subTypeExtractorExpression= delegateStrategy.getSubTypeExtractorFor(interface1, method.getName()); if (subTypeExtractorExpression != null) { String subTypeId= scriptHelperClass.getName() + ".eval(\"" + subTypeExtractorExpression + "\", this)"; Class<? extends SubTypeFactory> subTypeFactoryClass= delegateStrategy.getSubTypeFactoryClassFor(interface1, method.getName()); returnTypeAsString= "new " + subTypeFactoryClass.getName() + "().getSubTypeWith(" + subTypeId + ")"; body+= scriptHelperClass.getName() + ".put (\"temp\", temp, this);"; } Method javaMethod= toJavaMethod(interface1, method); body= body + delegateStrategy.createReturnExpression(interface1, javaMethod, returnTypeAsString); return body; } private static String toJavaName(CtClass returnType) { return Descriptor.toJavaName(Descriptor.toJvmName(returnType)); } private static String createParameters(CtMethod method, StringBuffer code) throws NotFoundException { Class<?> scriptHelperClass= ScriptHelper.class; int parametersCount= method.getParameterTypes().length; if (parametersCount > 0) { StringBuilder parameters= new StringBuilder(); for (int i= 0; i < parametersCount; i++) { String variableName= "$" + (i + 1); CtClass parameterType= method.getParameterTypes()[i]; code.append("" + scriptHelperClass.getName() + ".put(\"" + variableName + "\", " + createParameterForPut(variableName, parameterType) + ", this);"); if (i != 0) parameters.append(", "); parameters.append(createVariableForEval(variableName, parameterType)); } return parameters.toString(); } return null; } private static String createParameterForPut(String string, CtClass ctClass) { String javaName= toJavaName(ctClass); if (javaName.equals(Integer.class.getName()) || javaName.equals(int.class.getName()) || // javaName.equals(Double.class.getName()) || javaName.equals(double.class.getName()) || // javaName.equals(Short.class.getName()) || javaName.equals(short.class.getName()) || // javaName.equals(Float.class.getName()) || javaName.equals(float.class.getName()) || // javaName.equals(Byte.class.getName()) || javaName.equals(byte.class.getName()) || // javaName.equals(Long.class.getName()) || javaName.equals(long.class.getName())) return "(double)" + string; else return string; } public static String createVariableForEval(String string, Class<?> ctClass) { return createVariableForEval(string, resolveCtClass(ctClass)); } public static String createVariableForEval(String string, CtClass ctClass) { if (ctClass.isInterface()) return string + " ? " + string + ".node" + " : " + string; else return string; } private static CtClass resolveCtClass(Class<?> clazz) { try { ClassPool pool= ClassPool.getDefault(); return pool.get(clazz.getName()); } catch (NotFoundException e) { throw new RuntimeException(e); } } public byte[] generateBytecode(Class<?> interface1) { return generateBytecode("JsDelegate" + interface1.getSimpleName(), interface1); } public byte[] generate(Class<?> interface1) { try { String type= interface1.getName(); String classname= JsCast.createDelegateClassName(type); CtClass ctClass= ClassPool.getDefault().getOrNull(classname); if (ctClass == null) { ctClass= generateCtClass(classname, interface1); } return ctClass.toBytecode(); } catch (Exception e) { throw new RuntimeException(e); } } public InMemoryClasspathFile generateAsClasspathFile(Class<?> class1) { return new InMemoryClasspathFile(JsCast.createDelegateClassName(class1.getName()), generate(class1)); } }