/* * Copyright (C) 2015 Red Hat, Inc. and/or its affiliates. * * 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 org.jboss.errai.codegen; import static org.jboss.errai.codegen.util.PrettyPrinter.prettyPrintJava; import java.util.Arrays; import java.util.Collections; import java.util.Comparator; import java.util.HashSet; import java.util.IdentityHashMap; import java.util.List; import java.util.Set; import org.jboss.errai.codegen.builder.AnonymousClassStructureBuilder; import org.jboss.errai.codegen.builder.ClassStructureBuilder; import org.jboss.errai.codegen.builder.impl.ObjectBuilder; import org.jboss.errai.codegen.exception.CyclicalObjectGraphException; import org.jboss.errai.codegen.exception.GenerationException; import org.jboss.errai.codegen.exception.NotLiteralizableException; import org.jboss.errai.codegen.literal.NullLiteral; import org.jboss.errai.codegen.meta.MetaClass; import org.jboss.errai.codegen.meta.MetaClassFactory; import org.jboss.errai.codegen.meta.MetaMethod; import org.jboss.errai.codegen.util.Stmt; import org.slf4j.Logger; import org.slf4j.LoggerFactory; //import com.google.gwt.dev.util.collect.IdentityHashSet; /** * Utility class for creating code-generated snapshots of certain types of live value objects. * The classes and interfaces that SnapshotMaker works with have the following characteristics: * <ul> * <li>It must be an interface or a non-final class with a public no-args constructor. * <li>None of the public methods take arguments (except {@code equals(Object)}, which is always ignored) * <li>Each public method must be handled by the given MethodBodyCallback, or return one * of the following values for which a snapshot can be generated automatically: * <ul> * <li>{@code void} * <li>a Java primitive type * <li>a {@link Context#addLiteralizableClass(Class) literalizable type} in the current code generator context * <li>a type that is explicitly mentioned as a "type to recurse on" (these types must in turn follow this set of rules) * </ul> * </ul> * * @author Jonathan Fuerth <jfuerth@gmail.com> * @author Mike Brock */ public final class SnapshotMaker { final static Logger logger = LoggerFactory.getLogger(SnapshotMaker.class); /** * Callback interface for providing custom method bodies in snapshots. There are three major use cases: * <ol> * <li>To implement methods that take parameters (snapshots of * these methods cannot be generated automatically) * <li>To return a reference to some object that's already in the * scope of the snapshot (such as a reference to a parent object * from a getParent() method) * <li>To implement additional methods in the case that the snapshot * type is not the same as the type to extend. * </ol> * * @author Jonathan Fuerth <jfuerth@gmail.com> */ public interface MethodBodyCallback { /** * Optionally returns the statement that should be used as the body of the * given method for the given object's snapshot. If the default snapshot * behaviour provided by SnapshotMaker is sufficient for the given method, * this callback can simply return null. * * @param method * The method to provide the body for. * @param o * The instance object that we are taking the snapshot of. You can * use this reference if you need to invoke {@code method}. * @param containingClass * The class that will contain the generated method. During the * callback, you can generate additional methods and fields within * this class if you like. * @return The Statement to use as the method body (must return a type * compatible with {@code method}'s return type), or null if the * snapshot maker should generate the method body by invoking method * on {@code o} and returning a Literal of its value. */ Statement generateMethodBody(MetaMethod method, Object o, ClassStructureBuilder<?> containingClass); } /** This class should not be instantiated. */ private SnapshotMaker() {} /** * Code-generates an object whose methods return (snapshots of) the same * values as the given object. * * @param o * The object to snapshot. * @param typeToSnapshot * The type to read the snapshot attributes from. Must be a * superclass of o or an interface implemented by o, and methods not * supplied by {@code methodBodyCallback} must meed the requirements * laid out in the class-level SnapshotMaker documentation. * @param typeToExtend * The type of the snapshot to produce. Must be a subclass or * subinterface of typeToSnapshot, and the additional methods present * in typeToExtend vs. typeToSnapshot must be provided by the * MethodMaker callback, since they can't be generated from o. * @param methodBodyCallback * A callback that can provide method bodies, preventing the standard * snapshot behaviour for those methods. This callback is optional; * null is acceptable as "no callback." * @param typesToRecurseOn * The types for which the snapshot maker should be applied * recursively. * @return A Statement representing the value of the object * @throws CyclicalObjectGraphException * if any objects reachable from {@code o} form a reference cycle. * The simplest example of this would be a method on {@code o} that * returns {@code o} itself. You may be able to work around such a * problem by supplying a canned representation of one of the * objects in the cycle. */ public static Statement makeSnapshotAsSubclass( final Object o, final Class<?> typeToSnapshot, final Class<?> typeToExtend, final MethodBodyCallback methodBodyCallback, final Class<?> ... typesToRecurseOn) { final MetaClass metaTypeToSnapshot = MetaClassFactory.get(typeToSnapshot); final MetaClass metaTypeToExtend = MetaClassFactory.get(typeToExtend); final MetaClass[] metaTypesToRecurseOn = new MetaClass[typesToRecurseOn.length]; for (int i = 0; i < typesToRecurseOn.length; i++) { metaTypesToRecurseOn[i] = MetaClassFactory.get(typesToRecurseOn[i]); } return makeSnapshotAsSubclass(o, metaTypeToSnapshot, metaTypeToExtend, methodBodyCallback, metaTypesToRecurseOn); } /** * Code-generates an object whose methods return (snapshots of) the same * values as the given object. * * @param o * The object to snapshot. * @param typeToSnapshot * The type to read the snapshot attributes from. Must be a * superclass of o or an interface implemented by o, and methods not * supplied by {@code methodBodyCallback} must meed the requirements * laid out in the class-level SnapshotMaker documentation. * @param typeToExtend * The type of the snapshot to produce. Must be a subclass or * subinterface of typeToSnapshot, and the additional methods present * in typeToExtend vs. typeToSnapshot must be provided by the * MethodMaker callback, since they can't be generated from o. * @param methodBodyCallback * A callback that can provide method bodies, preventing the standard * snapshot behaviour for those methods. This callback is optional; * null is acceptable as "no callback." * @param typesToRecurseOn * The types for which the snapshot maker should be applied * recursively. * @return A Statement representing the value of the object * @throws CyclicalObjectGraphException * if any objects reachable from {@code o} form a reference cycle. * The simplest example of this would be a method on {@code o} that * returns {@code o} itself. You may be able to work around such a * problem by supplying a canned representation of one of the objects * in the cycle. */ public static Statement makeSnapshotAsSubclass( final Object o, final MetaClass typeToSnapshot, final MetaClass typeToExtend, final MethodBodyCallback methodBodyCallback, final MetaClass ... typesToRecurseOn) { return makeSnapshotAsSubclass( o, typeToSnapshot, typeToExtend, methodBodyCallback, new HashSet<MetaClass>(Arrays.asList(typesToRecurseOn)), new IdentityHashMap<Object, Statement>(), Collections.newSetFromMap(new IdentityHashMap<Object, Boolean>())); } /** * Implementation for the same-named public methods. * * @param o * The object to snapshot. * @param typeToSnapshot * The type to read the snapshot attributes from. Must be a * superclass of o or an interface implemented by o, and methods not * supplied by {@code methodBodyCallback} must meed the requirements * laid out in the class-level SnapshotMaker documentation. * @param typeToExtend * The type of the snapshot to produce. Must be a subclass or * subinterface of typeToSnapshot, and the additional methods present * in typeToExtend vs. typeToSnapshot must be provided by the * MethodMaker callback, since they can't be generated from o. * @param typesToRecurseOn * Types for which this method should be called recursively. * @param methodBodyCallback * A callback that can provide method bodies, preventing the standard * snapshot behaviour for those methods. This callback is optional; * null is acceptable as "no callback." * @param existingSnapshots * Object instances for which a snapshot has already been completed. * Bootstrap this with an empty IdentityHashMap. * @param unfinishedSnapshots * Object instances for which a partially-completed snapshot exists. * If one of these objects is returned by a method in {@code o}, this * causes a CyclicalObjectGraphException. * @return A Statement of type {@code typeToExtend} that represents the * current publicly visible state of {@code o}. */ private static Statement makeSnapshotAsSubclass( final Object o, final MetaClass typeToSnapshot, final MetaClass typeToExtend, final MethodBodyCallback methodBodyCallback, final Set<MetaClass> typesToRecurseOn, final IdentityHashMap<Object, Statement> existingSnapshots, final Set<Object> unfinishedSnapshots) { if (o == null) { return NullLiteral.INSTANCE; } if (!typeToSnapshot.isAssignableFrom(o.getClass())) { throw new IllegalArgumentException( "Given object (of type " + o.getClass().getName() + ") is not an instance of requested type to snapshot " + typeToSnapshot.getName()); } if (logger.isDebugEnabled()) { logger.debug("** Making snapshot of " + o); logger.debug(" Existing snapshots: " + existingSnapshots); } final List<MetaMethod> sortedMethods = Arrays.asList(typeToExtend.getMethods()); Collections.sort(sortedMethods, new Comparator<MetaMethod>() { @Override public int compare(MetaMethod m1, MetaMethod m2) { return m1.getName().compareTo(m2.getName()); } }); logger.debug(" Creating a new statement"); return new Statement() { String generatedCache; /** * We retain a mapping of return values to the methods that returned them, * in case we need to provide diagnostic information when an exception is * thrown. */ IdentityHashMap<Object, MetaMethod> methodReturnVals = new IdentityHashMap<Object, MetaMethod>(); @Override public String generate(Context context) { if (logger.isDebugEnabled()) { logger.debug("++ Statement.generate() for " + o); } if (generatedCache != null) return generatedCache; // create a subcontext and record the types we will allow the LiteralFactory to create automatic // snapshots for. final Context subContext = Context.create(context); subContext.addLiteralizableMetaClasses(typesToRecurseOn); final AnonymousClassStructureBuilder builder = ObjectBuilder.newInstanceOf(typeToExtend.getErased(), context) .extend(); unfinishedSnapshots.add(o); for (final MetaMethod method : sortedMethods) { if (method.isFinal() || method.getName().equals("toString")) continue; if (logger.isDebugEnabled()) { logger.debug(" method " + method.getName()); logger.debug(" return type " + method.getReturnType()); } if (methodBodyCallback != null) { final Statement providedMethod = methodBodyCallback.generateMethodBody(method, o, builder); if (providedMethod != null) { logger.debug(" body provided by callback"); builder .publicOverridesMethod(method.getName(), Parameter.of(method.getParameters())) .append(providedMethod) .finish(); continue; } } if (method.getName().equals("equals") || method.getName().equals("hashCode") || method.getName().equals("clone") || method.getName().equals("finalize")) { // we skip these if not provided by the callback if (logger.isDebugEnabled()) { logger.debug(" skipping special-case method " + method.getName()); } continue; } if (method.getParameters().length > 0) { throw new GenerationException("Method " + method + " in " + typeToSnapshot + " takes parameters. Such methods must be handled by the MethodBodyCallback," + " because they cannot be snapshotted."); } if (method.getReturnType().equals(MetaClassFactory.get(void.class))) { builder.publicOverridesMethod(method.getName()).finish(); if (logger.isDebugEnabled()) { logger.debug(" finished method " + method.getName()); } continue; } try { final Object retval = typeToExtend.asClass().getMethod(method.getName()).invoke(o); methodReturnVals.put(retval, method); if (logger.isDebugEnabled()) { logger.debug(" retval=" + retval); } Statement methodBody; if (existingSnapshots.containsKey(retval)) { logger.debug(" using existing snapshot"); methodBody = existingSnapshots.get(retval); } else if (subContext.isLiteralizableClass(method.getReturnType().getErased())) { if (unfinishedSnapshots.contains(retval)) { throw new CyclicalObjectGraphException(unfinishedSnapshots); } // use Stmt.create(context) to pass the context along. if (logger.isDebugEnabled()) { logger.debug(" >> recursing for " + retval); } methodBody = Stmt.create(subContext).nestedCall(makeSnapshotAsSubclass( retval, method.getReturnType(), method.getReturnType(), methodBodyCallback, typesToRecurseOn, existingSnapshots, unfinishedSnapshots)).returnValue(); } else { logger.debug(" relying on literal factory"); methodBody = Stmt.load(retval).returnValue(); } if (logger.isDebugEnabled()) { logger.debug(" finished method " + method.getName()); } builder.publicOverridesMethod(method.getName()).append(methodBody).finish(); existingSnapshots.put(retval, methodBody); } catch (final GenerationException e) { e.appendFailureInfo("In attempt to snapshot return value of " + typeToExtend.getFullyQualifiedName() + "." + method.getName() + "()"); throw e; } catch (final RuntimeException e) { throw e; } catch (final Exception e) { throw new GenerationException("Failed to extract value for snapshot", e); } } if (logger.isDebugEnabled()) { logger.debug(" finished: " + builder); } try { generatedCache = prettyPrintJava(builder.finish().toJavaString()); } catch (final NotLiteralizableException e) { final MetaMethod m = methodReturnVals.get(e.getNonLiteralizableObject()); if (m != null) { e.appendFailureInfo("This value came from method " + m.getDeclaringClass().getFullyQualifiedNameWithTypeParms() + "." + m.getName() + ", which has return type " + m.getReturnType()); } throw e; } catch (final GenerationException e) { e.appendFailureInfo("While generating a snapshot of " + o.toString() + " (actual type: " + o.getClass().getName() + "; type to extend: " + typeToExtend.getFullyQualifiedName() + ")"); throw e; } unfinishedSnapshots.remove(o); return generatedCache; } @Override public MetaClass getType() { return typeToExtend; } }; } }