/* * Copyright (c) 2014 EMC Corporation * All Rights Reserved */ package com.emc.storageos.db.client.impl; import java.lang.reflect.InvocationTargetException; import java.lang.reflect.Method; import java.net.URI; import java.util.Collection; import java.util.HashSet; import java.util.Iterator; import java.util.List; import java.util.Set; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import com.emc.storageos.db.client.DbClient; import com.emc.storageos.db.client.impl.ColumnField.ColumnType; import com.emc.storageos.db.client.model.AbstractChangeTrackingSet; import com.emc.storageos.db.client.model.DataObject; import com.emc.storageos.db.client.model.StringSet; import com.emc.storageos.db.client.util.DbClientCallbackEvent; import com.emc.storageos.db.joiner.Joiner; /** * @author cgarber * */ public class LazyLoader { /** * */ private static final String JOINER_ALIAS_ONE = "one"; /** * */ private static final String JOINER_ALIAS_TWO = "two"; private static final Logger log = LoggerFactory.getLogger(LazyLoader.class); private DbClient dbClient; /** * @param dbClient * @param _doType */ public LazyLoader(DbClient dbClient) { super(); this.dbClient = dbClient; } public void setDbClient(DbClient dbClient) { this.dbClient = dbClient; } public DbClient getDbClient() { return dbClient; } private ColumnField getMappedByField(ColumnField col, DataObjectType doType) { ColumnField mappedByCol = doType.getColumnField(col.getMappedByField()); if (mappedByCol == null) { mappedByCol = TypeMap.getDoType(col.getMappedByType()).getColumnField(col.getMappedByField()); } return mappedByCol; } /** * @param clazz class type of the return list * @param parentId id of the owning object * @param col field to be lazy loaded * @param retList return list */ public <T extends DataObject> Iterator<T> load(String lazyLoadedFieldName, T obj, Collection<T> collection, DbClientCallbackEvent cb) { DataObjectType doType = TypeMap.getDoType(obj.getClass()); ColumnField lazyLoadedField = doType.getColumnField(lazyLoadedFieldName); if (lazyLoadedField == null) { throw new IllegalStateException( String.format( "lazy loaded field %s in class %s not found; make sure the argument passed into refreshMappedByField matches the @Name annotation on the getter method", lazyLoadedFieldName, obj.getClass())); } ColumnField mappedByField = getMappedByField(lazyLoadedField, doType); if (mappedByField == null) { throw new IllegalStateException(String.format("lazy loaded field %s in class %s could not be found;" + " make sure the mappedBy argument in the @Relation annotation matches the @Name annotation on the mapped by field", lazyLoadedFieldName, obj.getClass())); } Joiner j = queryObjects(obj, cb, lazyLoadedField, mappedByField, JOINER_ALIAS_TWO); if (collection != null) { collection.addAll((Collection<? extends T>) j.list(JOINER_ALIAS_TWO)); } return j.iterator(JOINER_ALIAS_TWO); } /** * @param parentObj * @param cb * @param col * @param mappedByCol * @return */ private Joiner queryObjects(DataObject parentObj, DbClientCallbackEvent cb, ColumnField col, ColumnField mappedByCol, String joinerAlias) { Joiner j = new Joiner(dbClient); if (mappedByCol.getType().equals(ColumnType.TrackingSet)) { // for instance A has a list of instances B // the mapped by field is a StringSet within the same class as the lazy loaded list (instance A) try { Object val = mappedByCol.getPropertyDescriptor().getReadMethod().invoke(parentObj); if (val == null) { return null; } if (AbstractChangeTrackingSet.class.isAssignableFrom(val.getClass())) { if (isStringSetOfURIs(val)) { j.join(col.getMappedByType(), joinerAlias, stringSetToURISet((StringSet) val)).go(); } else { // TODO either support or throw an unsupported exception // this would cover instances where we want to join on a non-URI type field // and is not currently supported by the joiner class j.join(parentObj.getClass(), JOINER_ALIAS_ONE, parentObj.getId()) .join(JOINER_ALIAS_ONE, mappedByCol.getName(), col.getMappedByType(), joinerAlias).go(); } // call the setCallback method on the mapped field // the callback is used to inalidate the lazy loaded list if the StringSet changes ((AbstractChangeTrackingSet) val).setCallback(cb); } } catch (IllegalAccessException | IllegalArgumentException | InvocationTargetException e) { // TODO Auto-generated catch block log.error("could not set callback method in mapped by field " + mappedByCol.getName() + " for lazy loaded field " + col.getName() + " in memory values of the lazy loaded list may become stale if the TrackingSet is modified"); log.error(e.getMessage(), e); } } else { // for instance A has a list of instances B // the mapped by field is a URI or NamedURI field in each instance B j.join(parentObj.getClass(), JOINER_ALIAS_ONE, parentObj.getId()) .join(JOINER_ALIAS_ONE, col.getMappedByType(), JOINER_ALIAS_TWO, mappedByCol.getName()).go(); } return j; } private Set<URI> stringSetToURISet(StringSet objs) { Set<URI> ret = new HashSet<URI>(); for (String obj : objs) { ret.add(URI.create(obj)); } return ret; } /** * @param val * @return */ private boolean isStringSetOfURIs(Object val) { // if only we had URISet extends AbstractChangeTrackingSet<URI> if (StringSet.class.isAssignableFrom(val.getClass()) && ((StringSet) val).iterator().hasNext()) { return true; } return false; } /** * @param lazyLoadedFieldName * @param _id * @param fieldValue */ public <T extends DataObject> void load(String lazyLoadedFieldName, DataObject obj) { DataObjectType doType = TypeMap.getDoType(obj.getClass()); ColumnField lazyLoadedField = doType.getColumnField(lazyLoadedFieldName); if (lazyLoadedField == null) { throw new IllegalStateException( String.format( "lazy loaded field %s in class %s not found; make sure the argument passed into refreshMappedByField matches the @Name annotation on the getter method", lazyLoadedFieldName, obj.getClass())); } if (!lazyLoadedField.isLazyLoaded()) { log.debug("skipping; field %s in class %s is not a lazy loadable field", lazyLoadedFieldName, obj.getClass()); return; } if (!DataObject.class.isAssignableFrom(lazyLoadedField.getPropertyDescriptor().getPropertyType())) { log.debug("skipping; field %s in class %s is a collection; lazy loading is handled by LazyLoadedCollection", lazyLoadedFieldName, obj.getClass()); return; } // make sure the lazy loaded field has a setter method Method lazyLoadedFieldWriteMethod = lazyLoadedField.getPropertyDescriptor().getWriteMethod(); if (lazyLoadedFieldWriteMethod == null) { throw new IllegalStateException(String.format("lazy loaded field %s in class %s must have a write method", lazyLoadedFieldName, obj.getClass())); } try { T retObj = null; ColumnField mappedByField = doType.getColumnField(lazyLoadedField.getMappedByField()); if (mappedByField == null) { // mapped by field is a collection in the related class; use joiner to get the lazy loaded object mappedByField = TypeMap.getDoType(lazyLoadedField.getMappedByType()).getColumnField(lazyLoadedField.getMappedByField()); if (mappedByField == null) { throw new IllegalStateException( String.format( "lazy loaded field %s in class %s could not be found;" + " make sure the mappedBy argument in the @Relation annotation matches the @Name annotation on the mapped by field", lazyLoadedFieldName, obj.getClass())); } Joiner j = new Joiner(dbClient); j.join(obj.getClass(), JOINER_ALIAS_ONE, obj.getId()) .join(JOINER_ALIAS_ONE, lazyLoadedField.getMappedByType(), JOINER_ALIAS_TWO, mappedByField.getName()).go(); if (j.iterator(JOINER_ALIAS_TWO).hasNext()) { retObj = (T) j.iterator(JOINER_ALIAS_TWO).next(); } } else { // the mapped by field is a URI field in the same class as the lazy loaded field Method mappedByFieldReadMethod = mappedByField.getPropertyDescriptor().getReadMethod(); if (mappedByFieldReadMethod == null) { throw new IllegalStateException(String.format( "mapped by field %s mapped to lazy loaded field %s in class %s must have a read method", mappedByField.getName(), lazyLoadedFieldName, obj.getClass())); } // check the mapped by type is URI (supported type) Class mappedByObjType = mappedByFieldReadMethod.getReturnType(); if (!URI.class.isAssignableFrom(mappedByObjType)) { throw new IllegalStateException(String.format( "lazy loaded field %s in class %s has mapped by field %s with an unsupported type: %s;" + " the mapped by field for a DataObject must be a URI", lazyLoadedFieldName, obj.getClass(), mappedByField.getName(), mappedByObjType.getName())); } URI id = (URI) mappedByFieldReadMethod.invoke(obj); // id could be null if the mapped by field is not set to anything in persistence if (id != null) { retObj = (T) dbClient.queryObject(lazyLoadedField.getMappedByType(), id); } } lazyLoadedFieldWriteMethod.invoke(obj, retObj); } catch (IllegalAccessException | IllegalArgumentException | InvocationTargetException e) { log.error(e.getMessage(), e); } } /** * refreshes the mapped by field when the lazy loaded field is replaced by another value * * @param obj * @param lazyLoadedFieldName */ public void refreshMappedByField(String lazyLoadedFieldName, DataObject obj) { DataObjectType doType = TypeMap.getDoType(obj.getClass()); // make sure the lazy loaded field is a valid field ColumnField lazyLoadedField = doType.getColumnField(lazyLoadedFieldName); if (lazyLoadedField == null) { throw new IllegalStateException( String.format( "lazy loaded field %s in class %s not found; make sure the argument passed into refreshMappedByField matches the @Name annotation on the getter method", lazyLoadedFieldName, obj.getClass())); } // make sure the lazy loaded field has a getter and setter Method lazyLoadedFieldReadMethod = lazyLoadedField.getPropertyDescriptor().getReadMethod(); if (lazyLoadedFieldReadMethod == null) { throw new IllegalStateException(String.format("lazy loaded field %s in class %s must have a read method", lazyLoadedFieldName, obj.getClass())); } // make sure the lazy loaded field is a supported type Class lazyLoadedObjType = doType.getColumnField(lazyLoadedFieldName).getPropertyDescriptor().getPropertyType(); if (!Set.class.isAssignableFrom(lazyLoadedObjType) && !List.class.isAssignableFrom(lazyLoadedObjType) && !DataObject.class.isAssignableFrom(lazyLoadedObjType)) { throw new IllegalStateException(String.format("lazy loaded field %s in class %s is an unsupported type: %s; " + "supported type are DataObject, List and Set", lazyLoadedFieldName, obj.getClass(), lazyLoadedObjType.getName())); } // get the mapped by field ColumnField mappedByField = null; if (doType != null) { String mappedByFieldName = doType.getColumnField(lazyLoadedFieldName).getMappedByField(); mappedByField = doType.getColumnField(mappedByFieldName); if (mappedByField == null) { // mappedByField will be null if the mapped by field is in another class // in this case, we can't sync up the the lazy loaded field with the mapped by field return; } } // make sure the lazy loaded field has a getter and setter Method mappedByFieldReadMethod = mappedByField.getPropertyDescriptor().getReadMethod(); Method mappedByFieldWriteMethod = mappedByField.getPropertyDescriptor().getWriteMethod(); if (mappedByFieldReadMethod == null || mappedByFieldWriteMethod == null) { throw new IllegalStateException(String.format( "mapped by field %s mapped to lazy loaded field %s in class %s must have both a read method and a write method", mappedByField.getName(), lazyLoadedFieldName, obj.getClass())); } // get lazy loaded object if (Collection.class.isAssignableFrom(lazyLoadedObjType)) { // make sure the mapped by type is a supported type (StringSet) Class mappedByObjType = mappedByField.getPropertyDescriptor().getReadMethod().getReturnType(); if (!StringSet.class.isAssignableFrom(mappedByObjType)) { throw new IllegalStateException(String.format( "lazy loaded field %s in class %s has mapped by field %s with an unsupported type: %s;" + " the mappedby field for a collection must be a StringSet", lazyLoadedFieldName, obj.getClass(), mappedByField.getName(), lazyLoadedObjType.getName())); } refreshMappedByStringSet(obj, lazyLoadedFieldReadMethod, mappedByFieldReadMethod, mappedByFieldWriteMethod, mappedByObjType); } else if (DataObject.class.isAssignableFrom(lazyLoadedObjType)) { Class mappedByObjType = mappedByFieldReadMethod.getReturnType(); if (!URI.class.isAssignableFrom(mappedByObjType)) { throw new IllegalStateException(String.format( "lazy loaded field %s in class %s has mapped by field %s with an unsupported type: %s;" + " the mapped by field for a DataObject must be a URI", lazyLoadedFieldName, obj.getClass(), mappedByField.getName(), mappedByObjType.getName())); } refreshMappedByDataObject(obj, lazyLoadedFieldReadMethod, mappedByFieldWriteMethod); } } /** * @param obj * @param lazyLoadedFieldReadMethod * @param mappedByFieldWriteMethod * @throws IllegalAccessException * @throws InvocationTargetException */ private void refreshMappedByDataObject(DataObject obj, Method lazyLoadedFieldReadMethod, Method mappedByFieldWriteMethod) { try { DataObject lazyLoadedObj = (DataObject) lazyLoadedFieldReadMethod.invoke(obj); if (lazyLoadedObj == null) { mappedByFieldWriteMethod.invoke(obj, null); } else { mappedByFieldWriteMethod.invoke(obj, lazyLoadedObj.getId()); } } catch (IllegalAccessException | IllegalArgumentException | InvocationTargetException e) { // we've done all the checking we can; if we end up here, it's a programming error log.error(e.getMessage(), e); } } /** * @param obj * @param lazyLoadedFieldReadMethod * @param mappedByFieldReadMethod * @param mappedByFieldWriteMethod * @param mappedByObjType * @throws IllegalAccessException * @throws InvocationTargetException * @throws InstantiationException */ private void refreshMappedByStringSet(DataObject obj, Method lazyLoadedFieldReadMethod, Method mappedByFieldReadMethod, Method mappedByFieldWriteMethod, Class mappedByObjType) { try { Collection<DataObject> lazyLoadedFieldValue = (Collection) lazyLoadedFieldReadMethod.invoke(obj); StringSet mappedByFieldValue = (StringSet) mappedByFieldReadMethod.invoke(obj); // if the lazy loaded collection is null or empty, clear the mapped by stringset; // otherwise, set the mapped by stringset to the list of id's in the lazy loaded collection if (lazyLoadedFieldValue == null || lazyLoadedFieldValue.isEmpty()) { if (mappedByFieldValue != null) { mappedByFieldValue.clear(); mappedByFieldWriteMethod.invoke(obj, mappedByFieldValue); } } else { if (mappedByFieldValue == null) { mappedByFieldValue = (StringSet) mappedByObjType.newInstance(); } DbClientCallbackEvent cb = mappedByFieldValue.getCallback(); mappedByFieldValue.setCallback(null); copyCollectionToStringSet(lazyLoadedFieldValue, mappedByFieldValue); mappedByFieldValue.setCallback(cb); mappedByFieldWriteMethod.invoke(obj, mappedByFieldValue); } } catch (IllegalAccessException | IllegalArgumentException | InstantiationException | InvocationTargetException e) { // we've done all the checking we can; if we end up here, it's a programming error log.error(e.getMessage(), e); } } /** * @param lazyLoadedFieldValue * @param mappedByFieldValue */ private void copyCollectionToStringSet(Collection<DataObject> lazyLoadedFieldValue, StringSet mappedByFieldValue) { Set<String> newSet = new HashSet<String>(); for (DataObject listElem : lazyLoadedFieldValue) { newSet.add(listElem.getId().toString()); } HashSet<String> toBeRemoved = new HashSet<String>(); for (String id : mappedByFieldValue) { if (!newSet.contains(id)) { toBeRemoved.add(id); } } mappedByFieldValue.removeAll(toBeRemoved); for (DataObject snapshot : lazyLoadedFieldValue) { mappedByFieldValue.add(snapshot.getId().toString()); } } }