/* * Copyright 2012 Jason Miller * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package jj.configuration; import static jj.util.CodeGenHelper.*; import java.util.ArrayList; import java.util.List; import javassist.CannotCompileException; import javassist.ClassPool; import javassist.CtClass; import javassist.CtConstructor; import javassist.CtField; import javassist.CtMethod; import javassist.CtNewConstructor; import javassist.CtNewMethod; import javax.inject.Inject; import javax.inject.Singleton; import jj.util.CodeGenHelper; import jj.util.RandomHelper; /** * @author jason * */ @Singleton class ConfigurationClassMaker { private static final String NAME_FORMAT = "jj.configuration.GeneratedImplementationFor$$%s$$%s"; private static final String HASHCODE_LINE = "result = 37 * result + ("; private final ClassPool classPool; private final CtClass configurationCollector; private final CtClass[] constructionExceptions = new CtClass[0]; @Inject ConfigurationClassMaker() throws Exception { classPool = CodeGenHelper.classPool(); configurationCollector = classPool.get(ConfigurationCollector.class.getName()); } <T> Class<? extends T> make(Class<T> configurationInterface) throws Exception { final String name = String.format(NAME_FORMAT, configurationInterface.getName().replace(".", "_"), configurationInterface.hashCode() ); // we may have already defined this class since we support being restarted try { @SuppressWarnings("unchecked") Class<? extends T> resultClass = (Class<? extends T>) Class.forName(name); return resultClass; } catch (ClassNotFoundException e) { /* just carry on */ } CtClass resultInterface = classPool.get(configurationInterface.getName()); CtClass result = classPool.makeClass(name); result.addInterface(resultInterface); List<CtClass> collaborators = gatherCollaborators(resultInterface); // make it! prepareForInjection(result, collaborators); implement(result, resultInterface); try { @SuppressWarnings("unchecked") Class<? extends T> resultClass = (Class<? extends T>)result.toClass(getClass().getClassLoader(), null); return resultClass; } finally { // no need to keep these around result.detach(); resultInterface.detach(); } } private List<CtClass> gatherCollaborators(final CtClass resultInterface) throws Exception { ArrayList<CtClass> result = new ArrayList<>(); for (CtMethod method : resultInterface.getMethods()) { if (method.hasAnnotation(DefaultProvider.class)) { Class<?> providerClass = ((DefaultProvider)method.getAnnotation(DefaultProvider.class)).value(); result.add(classPool.get(providerClass.getName())); } } return result; } private void prepareForInjection(final CtClass result, final List<CtClass> collaborators) throws CannotCompileException { collaborators.add(0, configurationCollector); int index = 1; StringBuilder body = new StringBuilder("{"); for (CtClass collaborator : collaborators) { CtField field = CtField.make("private final " + collaborator.getName() + " " + collaborator.getSimpleName() + ";", result); result.addField(field); body.append("this.").append(collaborator.getSimpleName()).append(" = $").append(index++).append(";"); } body.append("}"); CtConstructor ctor = CtNewConstructor.make(collaborators.toArray(new CtClass[collaborators.size()]), constructionExceptions, result); ctor.setBody(body.toString()); result.addConstructor(ctor); // @Inject CodeGenHelper.addAnnotationToMethod(ctor, Inject.class); // @Singleton CodeGenHelper.addAnnotationToClass(result, Singleton.class); } private void implement(final CtClass result, final CtClass resultInterface) throws Exception { // don't allow pathological values! int baseInt; do { baseInt = RandomHelper.nextInt(); } while (baseInt == 0 || baseInt == -1 || baseInt == 1); StringBuilder hashCodeBody = new StringBuilder("public int hashCode() {\nint result = ").append(baseInt).append(";\n"); for (CtMethod method : resultInterface.getDeclaredMethods()) { CtMethod newMethod = CtNewMethod.copy(method, result, null); Default defaultAnnotation = (Default)method.getAnnotation(Default.class); String defaultValue = defaultAnnotation != null ? "\"" + defaultAnnotation.value() + "\"" : null; DefaultProvider defaultProviderAnnotation = (DefaultProvider)method.getAnnotation(DefaultProvider.class); if (defaultAnnotation != null && defaultProviderAnnotation != null) { throw new AssertionError("only one of @Default and @DefaultProvider can be declared for a given method"); } if (defaultProviderAnnotation != null) { // assert the return type == the provider type defaultValue = defaultProviderAnnotation.value().getSimpleName() + ".get()"; } String name = resultInterface.getName() + "." + newMethod.getName(); if (method.getReturnType().isPrimitive()) { String returnType = method.getReturnType().getName(); String type = primitiveNamesToWrappers.get(returnType).getName(); newMethod.setBody( "{" + "Object value = " + configurationCollector.getSimpleName() + ".get(\"" + name + "\", " + type + ".class, " + defaultValue + ");" + "if (value == null) { return " + primitiveDefaults.get(type) + "; }" + "else { return ($r)value; }" + "}" ); switch (returnType) { // If the field f is a boolean: calculate (f ? 0 : 1); case "boolean": hashCodeBody.append(HASHCODE_LINE).append(newMethod.getName()).append("() ? 0 : 1);\n"); break; // If the field f is a byte, char, short or int: calculate (int)f; case "byte": case "char": case "short": case "int": hashCodeBody.append(HASHCODE_LINE).append("(int)").append(newMethod.getName()).append("());\n"); break; // If the field f is a long: calculate (int)(f ^ (f >>> 32)); case "long": hashCodeBody.append(HASHCODE_LINE).append("(int)(").append(newMethod.getName()).append("() ^ (").append(newMethod.getName()).append("() >>> 32)));\n"); break; // If the field f is a float: calculate Float.floatToIntBits(f); case "float": hashCodeBody.append(HASHCODE_LINE).append("Float.floatToIntBits(").append(newMethod.getName()).append("()));\n"); break; // If the field f is a double: calculate Double.doubleToLongBits(f) and handle the return value like every long value; case "double": hashCodeBody.append("long ").append(newMethod.getName()).append(" = ").append("Double.doubleToLongBits(").append(newMethod.getName()).append("());\n"); hashCodeBody.append(HASHCODE_LINE).append("(int)(").append(newMethod.getName()).append(" ^ (").append(newMethod.getName()).append(" >>> 32)));\n"); break; } } else { newMethod.setBody( "{" + "return ($r)" + configurationCollector.getSimpleName() + ".get(\"" + name + "\", " + method.getReturnType().getName() + ".class, " + defaultValue + ");" + "}" ); // // If the field f is an object: Use the result of the hashCode() method or 0 if f == null; hashCodeBody.append(method.getReturnType().getName()).append(" ").append(newMethod.getName()).append(" = ").append( newMethod.getName()).append("();\n"); hashCodeBody.append(HASHCODE_LINE).append( newMethod.getName()).append(" == null ? 0 : ").append(newMethod.getName()).append(".hashCode());\n"); // if the return type is in the same package as the resultInterface, // we can detach it } result.addMethod(newMethod); // and now deal with the hashcode // luckily we do not support arrays! // If the field f is an array: See every field as separate element and calculate the hash value in a recursive fashion and combine the values as described next. } hashCodeBody.append("return result;\n}"); result.addMethod(CtNewMethod.make(hashCodeBody.toString(), result)); } }