All Downloads are FREE. Search and download functionalities are using the official Maven repository.

xapi.util.X_GC Maven / Gradle / Ivy

package xapi.util;

import java.lang.reflect.Field;
import java.lang.reflect.Method;
import java.lang.reflect.Modifier;
import java.util.ArrayList;
import java.util.HashSet;

import xapi.annotation.gc.Indestructible;
import xapi.annotation.gc.OnGC;
import xapi.collect.impl.AbstractMultiInitMap;
import xapi.log.X_Log;
import xapi.util.api.Destroyable;
import xapi.util.api.Destroyer;

/**
 * In order to encourage and facilitate object de-referencing,
 * the X_GC class, combined with the {@link OnGC} annotation
 * allow objects to destroy each other without necessarily knowing anything about them.
 * 
 * It also allows forming "destruction chains", whereby when a root object is destroyed,
 * we can check if it's fields are destroyable and gc them too, either by annotating with OnGC, 
 * or implementing the {@link Destroyable} interface.
 * 
 * Not that this may NOT be what you want, in case a field is shared among multiple children.
 * Thus, GC chaining is disabled by default.  
 * Default behavior is to null out fields without inspecting them.
 * 
 * @author "James X. Nelson ([email protected])"
 *
 */
public class X_GC {

  private X_GC() {
  }
  
  private static final class StaticGCMethods extends AbstractMultiInitMap> {
    public StaticGCMethods() {
      super(PASS_THRU);
    }
    @Override
    protected Destroyer initialize(String method, Class cls) {
      try{
        String[] bits = method.split("#");
        assert bits.length == 2 : 
          "[ERROR] Malformed static GC method "+method+"; \n" +
              "Proper syntax is com.package.Clazz$StaticInner#methodName";
        Class toLoad;
        if (bits[0].equals(cls.getName()))
          toLoad = cls;
        else {
          toLoad = Class.forName(bits[0], true, cls.getClassLoader());
        }
        final Method m = toLoad.getMethod(bits[1], Object.class);
        if (m == null){
          X_Log.warn("Could not find static gc method ",method);
          return Destroyer.NO_OP;
        }
        else {
          assert m.getTypeParameters().length == 0 : 
            "Only zero-arg instance methods allowed";
          if (!m.isAccessible())
            m.setAccessible(true);
          return new Destroyer() {
            @Override
            public void destroyObject(Object toDestroy) {
              try {
                m.invoke(null, toDestroy);
              } catch (Throwable e) {
                X_Debug.maybeRethrow(e);
              }
            }
          };
        }
      } catch (Throwable e) {
        throw X_Util.rethrow(e);
      }
    }
  }
  
  private static final class GCMap extends AbstractMultiInitMap, Destroyer, OnGC>
 {
    
    private static final StaticGCMethods staticDestroyers = new StaticGCMethods();
    
    public GCMap() {
      super(CLASS_NAME);
    }
    @Override
    protected Destroyer initialize(Class cls, OnGC options) {
        final ArrayList zeroArg = new ArrayList();
        for (String method : options.instanceGCMethods()) {
          try{
            Method m = cls.getMethod(method);
            if (m == null)
              m = cls.getDeclaredMethod(method);
            if (m == null)
              X_Log.warn("Could not find instance level gc method ",method);
            else {
              if (!m.isAccessible())
                m.setAccessible(true);
              assert m.getTypeParameters().length == 0 : 
                "Only zero-arg instance methods allowed on instance gc methods";
              // could test for static, but we'll let that slide, as it will work...
              zeroArg.add(m);
            }
          } catch (Throwable e) {
            X_Debug.maybeRethrow(e);
          }
        }
        final ArrayList fieldDestroyers = new ArrayList();
        if (options.chainDeleteFields()||options.deleteInstanceFields()) {
          for (final Field f : cls.getDeclaredFields()) {
            if (shouldSkip(f))
              continue;
            try {
              if (!f.isAccessible()) {
                f.setAccessible(true);
              }
              final Destroyer setToNull = new Destroyer() {
                @Override
                public void destroyObject(Object toDestroy) {
                  try {
                    f.set(toDestroy, null);
                  } catch (Throwable e) {
                    X_Debug.maybeRethrow(e);
                  }
                }
              };
              // if the client wants us to chain gc to child fields:
              if (options.chainDeleteFields()) {
                fieldDestroyers.add(new Destroyer() {
                  @SuppressWarnings({"rawtypes","unchecked"})
                  @Override
                  public void destroyObject(Object toDestroy) {
                    Object obj;
                    try {
                      obj = f.get(toDestroy);
                      if (obj != null) {
                        if (obj instanceof Destroyable) {
                          ((Destroyable)obj).destroy();
                        }
                        Class cls = obj.getClass();
                        OnGC anno = (OnGC)cls.getAnnotation(OnGC.class);
                        if (anno != null)
                          // This object ref won't give generator details,
                          // However, it can still be made to work, by scanning
                          // for all OnGC annotations and generating code for any of them 
                          destroy(cls, obj); 
                      }
                    } catch (Throwable e) {
                      e.printStackTrace();
                    }
                    // also call our setToNull destroyer
                    setToNull.destroyObject(toDestroy);
                  }
                });
              } else {
                fieldDestroyers.add(setToNull);
              }
            } catch (Throwable e) {
              X_Debug.maybeRethrow(e);
            }
          }
        }
        final Destroyer instanceLevel = new Destroyer() {
          @Override
          public void destroyObject(Object toDestroy) {
            // destroy methods called
            for (Method m : zeroArg) {
              try{
                m.invoke(toDestroy);
              } catch (Throwable e){
                X_Debug.maybeRethrow(e);
              }
            }
            // fields cleared / destroy event chained
            for (Destroyer destroyer : fieldDestroyers) {
              destroyer.destroyObject(toDestroy);
            }
          }
        };
        // avoid extra work, if we can; 
        // the static methods are more overhead up front
        if (options.staticGCMethods().length == 0)
          return instanceLevel;
        
        // We have to do reflection
        final ArrayList staticLevel = new ArrayList();
        for (String method : options.staticGCMethods()) {
          assert !method.contains("(") : "Do not send parantheses with method descriptions: "+method+" on "+cls;
          // share instances of a single destroyer per method, to cut down on memory duplication
          staticLevel.add(staticDestroyers.get(method, cls));
        }
        // Initialize a composite destroyer, which notifies static methods first,
        // then instance methods and finally instance fields, if enabled.
        return new Destroyer() {
          @Override
          public void destroyObject(Object toDestroy) {
            // Give static methods a crack at the object first,
            // as they are the most likely to be doing housekeeping
            // that needs to see the objects before they are destroyed.
            for (Destroyer m : staticLevel) {
              m.destroyObject(toDestroy);
            }
            instanceLevel.destroyObject(toDestroy);
          }
        };
    }
  }
  
  private static final GCMap destroyers = new GCMap();
  
  /**
   * In gwt, we can use this as a magic method to compute a destroy function,
   * but do so without bloating runtime code.  This is why you must send the class.
   * 
   * If you put OnGC on a superclass, and do not wish to repeat the annotation,
   * you can send the superclass's class literal, and it will be applied instead
   * (though it will not use any overridden methods; we literally use the super classes Method objects).
   * 
   * Note that you must send a the class LITERAL and not a class reference,
   * as that is what the gwt generator will use to define a GC strategy.
   * 
   * In any case, the code needed to destroy the fields of the object will just be plain javascript,
   * for (key in _)if (_.hasOwnProperty(key)) delete _[key]
   * 
   * Sending the class along simply means we can check for the @GC annotation, and use it to call some 
   * zero-arg destroy methods for this object, and maybe even on any particular fields.
   * 
   * @param cls
   * @param inst
   */
  public static  void destroy(Class classLiteral, T inst) {
    if (inst == null)return;
    OnGC gc = classLiteral.getAnnotation(OnGC.class);
    if (gc == null) {
      assert "true".equals(System.getProperty(
          "xapi.gcignore."+classLiteral.getName(), "false")) : "Trying to call X_GC on a class without an OnGC annotation.\n" +
      		"To suppress this assertion, set system property "+"xapi.gcignore."+classLiteral.getName()+" to true";
      return;
    }
    destroyers.get(classLiteral, gc).destroyObject(inst);
    // TODO have a flag to disable supertype gc as well.
    // Also, move this into the pre-compiled destroyer commands, instead of checking every time.
    while (classLiteral != Object.class) {
      classLiteral = classLiteral.getSuperclass();
      gc = classLiteral.getAnnotation(OnGC.class);
      if (gc != null){
        destroy(classLiteral, inst);
        return; // the above call will recurse 
      }
    }
  }
  private static boolean shouldSkip(Field f) {
    return 
        f.getType().isPrimitive() 
        || Modifier.isFinal(f.getModifiers())
        || Modifier.isStatic(f.getModifiers())
        || f.getAnnotation(Indestructible.class) != null
        || f.isSynthetic() 
        ;
  }
  public static  void deepDestroy(Class classLiteral, T inst) {
    deepDestroy(classLiteral, inst, new HashSet());
    destroy(classLiteral, inst);
  }
  @SuppressWarnings("unchecked")
  public static  void deepDestroy(Class classLiteral, T inst, HashSet seen) {
    for (Field f : classLiteral.getDeclaredFields()) {
      if (shouldSkip(f))continue;
      try {
        if (!f.isAccessible()) {
          f.setAccessible(true);
        }
        Object value = f.get(inst);
        if (value == null)continue;
        if (value instanceof Destroyable) {
          ((Destroyable)value).destroy();
        }
        if (value.getClass().getAnnotation(OnGC.class) != null) {
          destroy(Class.class.cast(value.getClass()), value);
        }
      } catch (Throwable e) {
        X_Debug.maybeRethrow(e);
      }
    }
  }
  
}