/* * Copyright 2009-2016 Tilmann Zaeschke. All rights reserved. * * This file is part of ZooDB. * * ZooDB is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. * * ZooDB is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with ZooDB. If not, see <http://www.gnu.org/licenses/>. * * See the README and COPYING files for further information. */ package org.zoodb.internal; import java.lang.reflect.Field; import java.lang.reflect.Modifier; import java.math.BigDecimal; import java.math.BigInteger; import java.net.URI; import java.net.URL; import java.util.ArrayList; import java.util.Collection; import java.util.Dictionary; import java.util.Enumeration; import java.util.IdentityHashMap; import java.util.List; import java.util.Map; import javax.jdo.JDOHelper; import javax.jdo.PersistenceManager; import javax.jdo.PersistenceManagerFactory; import javax.jdo.Query; import org.zoodb.api.DBArrayList; import org.zoodb.api.DBCollection; import org.zoodb.api.DBHashMap; import org.zoodb.api.DBLargeVector; import org.zoodb.api.impl.ZooPC; import org.zoodb.internal.client.session.ClientSessionCache; import org.zoodb.internal.util.DBLogger; import org.zoodb.internal.util.ObjectIdentitySet; import org.zoodb.jdo.ZooJdoHelper; import org.zoodb.jdo.impl.PersistenceManagerFactoryImpl; import org.zoodb.jdo.impl.PersistenceManagerImpl; /** * This class traverses all objects in the Java cache. It looks for new * objects that have not been made persistent and makes them persistent. * <p> * This class is only public so it can be accessed by the test harness. * Please do not use. * <p> * WARNING: In this context, ObjectIdentityHashsets should be used with care. * Since this kind of Set uses the hashcode of an object, it is likely to * load the object into the client, even when calling contains(). Using such * a Set in context of the cache may result in loading hollow objects as well. * * @author Tilmann Zaeschke */ public class ObjectGraphTraverser { private final Session session; private final ClientSessionCache cache; private boolean traversalRequired = true; private final IdentityHashMap<Class<? extends Object>, Field[]> SEEN_CLASSES = new IdentityHashMap<Class<? extends Object>, Field[]>(); private final ObjectIdentitySet<Object> seenObjects; private final ArrayList<Object> workList; private boolean isTraversingCache = false; private int mpCount = 0; private final ArrayList<ZooPC> toBecomePersistent; /** * This HashSet contains types that are not persistent and that * cannot refer to other objects/classes (as a containers can do). */ private final static ObjectIdentitySet<Class<?>> SIMPLE_TYPES; static { SIMPLE_TYPES = new ObjectIdentitySet<Class<?>>(); SIMPLE_TYPES.add(Boolean.class); SIMPLE_TYPES.add(Byte.class); SIMPLE_TYPES.add(Character.class); SIMPLE_TYPES.add(Double.class); SIMPLE_TYPES.add(Float.class); SIMPLE_TYPES.add(Integer.class); SIMPLE_TYPES.add(Long.class); SIMPLE_TYPES.add(Short.class); SIMPLE_TYPES.add(Boolean.TYPE); SIMPLE_TYPES.add(Byte.TYPE); SIMPLE_TYPES.add(Character.TYPE); SIMPLE_TYPES.add(Double.TYPE); SIMPLE_TYPES.add(Float.TYPE); SIMPLE_TYPES.add(Integer.TYPE); SIMPLE_TYPES.add(Long.TYPE); SIMPLE_TYPES.add(Short.TYPE); SIMPLE_TYPES.add(BigDecimal.class); SIMPLE_TYPES.add(BigInteger.class); SIMPLE_TYPES.add(String.class); SIMPLE_TYPES.add(URI.class); SIMPLE_TYPES.add(URL.class); //more? //TODO use PERSISTENT_CONTAINER_TYPES } final static ObjectIdentitySet<Class<?>> ILLEGAL_TYPES; static { //This list does NOT need to be complete, if it doesn't fail for the first JDO/ZooDB //object, if will fail when it encounters PM or Session. ILLEGAL_TYPES = new ObjectIdentitySet<Class<?>>(); ILLEGAL_TYPES.add(PersistenceManager.class); ILLEGAL_TYPES.add(PersistenceManagerImpl.class); ILLEGAL_TYPES.add(PersistenceManagerFactory.class); ILLEGAL_TYPES.add(PersistenceManagerFactoryImpl.class); ILLEGAL_TYPES.add(JDOHelper.class); ILLEGAL_TYPES.add(ZooJdoHelper.class); ILLEGAL_TYPES.add(Session.class); ILLEGAL_TYPES.add(Query.class); //more? //Class should NOT be used as persistent attribute ILLEGAL_TYPES.add(Class.class); ILLEGAL_TYPES.add(Field.class); ILLEGAL_TYPES.add(Thread.class); } /** * This class is only public so it can be accessed by the test harness. * Please do not use. * @param cache the client cache */ public ObjectGraphTraverser(ClientSessionCache cache) { this.cache = cache; this.session = cache.getSession(); //We need to copy the cache to a local list, because the cache we might make additional //objects persistent while iterating. We need to ensure that these new objects are covered //as well. And we have the problem of concurrent updates in the cache. workList = new ArrayList<Object>(); seenObjects = new ObjectIdentitySet<Object>(); //TODO ObjIdentSet? -> ArrayList might be faster in most cases toBecomePersistent = new ArrayList<ZooPC>(); } /** * Tell the OGT that the object graph has changed and that a new traversal is required. */ public void flagTraversalRequired() { traversalRequired = true; } /** * This class is only public so it can be accessed by the test harness. * Please do not use. */ public final void traverse() { //We have to check for && because 'traversalRequired' is not triggered by new objects, //but new objects may have new objects referenced that are not marked as persistent. //See issue #57. if (!traversalRequired && !cache.hasDirtyPojos()) { //shortcut return; } //Intention is to find the NEW objects that will become persistent //through reachability. //For this, we have to check objects that are DIRTY or NEW (by //makePersistent()). // DBLogger.debugPrintln(1, "Starting OGT: " + workList.size()); // long t1 = System.currentTimeMillis(); // long nObjects = 0; // // nObjects += traverseCache(); // nObjects += traverseWorkList(); // // long t2 = System.currentTimeMillis(); // DBLogger.debugPrintln(1, "Finished OGT: " + nObjects + " (seen=" // + seenObjects.size() + " ) / " + (t2-t1)/1000.0 // + " MP=" + mpCount); traverseCache(); traverseWorkList(); //We have to clear the seenObjects here, see also issue #58. seenObjects.clear(); traversalRequired = false; } private int traverseCache() { isTraversingCache = true; int nObjects = 0; //TODO is this really necessary? Looks VERY ugly. //Profiling FlatObject.commit(): ~7% spent in next()! for (ZooPC co: cache.getDirtyObjects()) { //ignore clean objects. Ignore hollow objects? Don't follow deleted objects. //we require objects that are dirty or new (=dirty and not deleted?) if (co.jdoZooIsDirty() & !co.jdoZooIsDeleted()) { traverseObject(co); nObjects++; } } isTraversingCache = false; //make objects persistent. This has to be delayed after traversing the cache to avoid //concurrent modification on the cache. for (ZooPC pc: toBecomePersistent) { if (!pc.jdoZooIsPersistent()) { cache.getSession().makePersistent(pc); } } mpCount += toBecomePersistent.size(); return nObjects; } private int traverseWorkList() { int nObjects = 0; while (!workList.isEmpty()) { nObjects++; Object object = workList.remove(workList.size()-1); //Objects in the work-list are always already made persistent: //Objects in the work-list are either added by the constructor //(already made persistent). //or have been added by addToWorkList (which uses makePersistent() first). traverseObject(object); } return nObjects; } @SuppressWarnings("rawtypes") private void traverseObject(Object object) { //TODO optimisations: //- Check first for ZooPC // -> Implement separate doObject(ZooPC, which uses ZooCLassDef-fields i.o. Class-fields // This avoid SEEN_CLASSES lookup // -> What about persistent collections (also if 3rd party?)? //- Remove static modifier for SEEN_CLASSES -> make OGT final field in Session if (object instanceof DBCollection) { doPersistentContainer(object); } else if (object instanceof Object[]) { doArray((Object[]) object); } else if (object instanceof Collection) { doCollection((Collection) object); } else if (object instanceof Map) { doCollection(((Map) object).keySet()); doCollection(((Map) object).values()); } else if (object instanceof Dictionary) { doEnumeration(((Dictionary) object).keys()); doEnumeration(((Dictionary) object).elements()); } else if (object instanceof Enumeration) { doEnumeration((Enumeration) object); } else if (object instanceof GenericObject){ //Ignore for now } else { doObject(object); } } final private void addToWorkList(Object object) { if (object == null) return; Class<? extends Object> cls = object.getClass(); if (SIMPLE_TYPES.contains(cls)) { //This can happen when called from doMap(), doContainer(), ... return; } if (ILLEGAL_TYPES.contains(cls)) { throw DBLogger.newUser("Objects of this type cannot be stored. Could the field be " + "made 'static' or 'transient'? Type: " + cls); } if (object instanceof ZooPC) { ZooPC pc = (ZooPC) object; //This can happen if e.g. a LinkedList contains new persistent capable objects. if (!pc.jdoZooIsPersistent()) { //Make object persistent, if necessary if (isTraversingCache) { //during cache traversal: toBecomePersistent.add(pc); } else { //during work list traversal: session.makePersistent(pc); mpCount++; } } else { //This object is already persistent. It is either in the worklist or it is //uninteresting (not dirty). return; } } if (!seenObjects.contains(object)) { workList.add(object); seenObjects.add(object); } } final private void doArray(Object[] array) { for (Object o: array) { addToWorkList(o); } } @SuppressWarnings("rawtypes") final private void doEnumeration(Enumeration enumeration) { while (enumeration.hasMoreElements()) { addToWorkList(enumeration.nextElement()); } } final private void doCollection(Collection<?> col) { for (Object o: col) { addToWorkList(o); } } private final void doObject(Object parent) { for (Field field: getFields(parent.getClass())) { try { //add the value to the working list addToWorkList(field.get(parent)); } catch (IllegalAccessException e) { throw new IllegalStateException( "Unable to access field \"" + field.getName() + "\" of class \"" + parent.getClass().getName() + "\" from OGT.", e); } } } /** * Handles persistent Collection classes. */ @SuppressWarnings("rawtypes") private final void doPersistentContainer(Object container) { if (container instanceof DBArrayList) { doCollection((DBArrayList)container); } else if (container instanceof DBLargeVector) { // doEnumeration(((DBLargeVector)container).elements(), container); doCollection(((DBLargeVector)container)); } else if (container instanceof DBHashMap) { DBHashMap t = (DBHashMap)container; doCollection(t.keySet()); doCollection(t.values()); } else { throw new IllegalArgumentException( "Unrecognized persistent container in OGT: " + container.getClass()); } } private static final boolean isSimpleType(Field field) { int mod = field.getModifiers(); if (Modifier.isStatic(mod) || Modifier.isTransient(mod)) { return true; } Class<?> cls = field.getType(); while (cls.isArray()) { cls = cls.getComponentType(); } if (ILLEGAL_TYPES.contains(cls)) { throw DBLogger.newUser("Class fields of this type cannot be stored. Could they be " + "made 'static' or 'transient'? Type: " + cls + " in " + field.getDeclaringClass().getName() + "." + field.getName()); } return SIMPLE_TYPES.contains(cls); } /** * Returns a List containing all of the Field objects for the given class. * The fields include all public and private fields from the given class * and its super classes. * * @param c Class object * @return Returns list of interesting fields */ private final Field[] getFields (Class<? extends Object> cls) { Field[] ret = SEEN_CLASSES.get(cls); if (ret != null) { return ret; } List<Field> retL = new ArrayList<Field>(); for (Field f: cls.getDeclaredFields ()) { if (!isSimpleType(f)) { retL.add(f); f.setAccessible(true); } } //the 2nd case can occur if the incoming object is of type Object.class //--> See Test_084_SerailizationBugRefToPM. if (cls.getSuperclass() != Object.class && cls != Object.class) { for (Field f: getFields(cls.getSuperclass())) { retL.add(f); } } ret = retL.toArray(new Field[retL.size()]); SEEN_CLASSES.put(cls, ret); return ret; } }