/* * Copyright (C) 2014-2016 University of Dundee & Open Microscopy Environment. * All rights reserved. * * This program 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 2 of the License, or * (at your option) any later version. * * This program 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 this program; if not, write to the Free Software Foundation, Inc., * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. */ package ome.services.graphs; import java.lang.reflect.Modifier; import java.util.ArrayList; import java.util.Arrays; import java.util.Collection; import java.util.Collections; import java.util.HashMap; import java.util.HashSet; import java.util.LinkedList; import java.util.List; import java.util.Map; import java.util.Map.Entry; import java.util.Queue; import java.util.Set; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.apache.commons.beanutils.NestedNullException; import org.apache.commons.beanutils.PropertyUtils; import org.hibernate.engine.SessionFactoryImplementor; import org.hibernate.metadata.ClassMetadata; import org.hibernate.type.AssociationType; import org.hibernate.type.CollectionType; import org.hibernate.type.ComponentType; import org.hibernate.type.Type; import org.springframework.beans.BeanUtils; import org.springframework.context.ApplicationContext; import org.springframework.context.event.ContextRefreshedEvent; import com.google.common.base.Joiner; import com.google.common.collect.HashMultimap; import com.google.common.collect.ImmutableSet; import com.google.common.collect.Maps; import com.google.common.collect.SetMultimap; import ome.model.IObject; import ome.tools.spring.OnContextRefreshedEventListener; /** * The graph path bean holds the Hiberate object mapping and assists navigation thereof. * @author m.t.b.carroll@dundee.ac.uk * @since 5.1.0 */ public class GraphPathBean extends OnContextRefreshedEventListener { private static final Logger log = LoggerFactory.getLogger(GraphPathBean.class); /** * How entities may be <q>unlinked</q> from the value of a property. * @author m.t.b.carroll@dundee.ac.uk * @since 5.1.0 */ static enum PropertyKind { OPTIONAL, REQUIRED, COLLECTION }; /** * A tuple used in initialization to note mapping object properties for processing. * @author m.t.b.carroll@dundee.ac.uk * @since 5.1.0 */ /* private GraphPathReport */ static class PropertyDetails { final String holder; final List<String> path; final Type type; final boolean isNullable; /** * Construct a new property details tuple instance. * @param className the name of the class holding the property * @param path the HQL path by which the property is referenced * @param type the Hibernate type of the property * @param isNullable if the property is nullable */ PropertyDetails(String className, List<String> path, Type type, boolean isNullable) { this.holder = className; this.path = path; this.type = type; this.isNullable = isNullable; } } /* classes indexed by their simple name */ private final Map<String, Class<? extends IObject>> classesBySimpleName = new HashMap<String, Class<? extends IObject>>(); /* direct and indirect superclasses of mapped entities */ private final SetMultimap<String, String> allSuperclasses = HashMultimap.create(); /* direct and indirect subclasses of mapped entities */ private final SetMultimap<String, String> allSubclasses = HashMultimap.create(); /* X -> Y, Z: class X links to class Y with X's property Z */ private final SetMultimap<String, Entry<String, String>> linkedTo = HashMultimap.create(); /* X -> Y, Z: class X is linked to by class Y with Y's property Z */ private final SetMultimap<String, Entry<String, String>> linkedBy = HashMultimap.create(); /* how nullable properties are */ private final HashMap<Entry<String, String>, PropertyKind> propertyKinds = new HashMap<Entry<String, String>, PropertyKind>(); /* which properties are accessible */ private final Set<Entry<String, String>> accessibleProperties = new HashSet<Entry<String, String>>(); /* the identifier properties of classes */ private final Map<String, String> classIdProperties = new HashMap<String, String>(); /* the properties of classes that have simple values */ private final SetMultimap<String, String> simpleProperties = HashMultimap.create(); /** * The application context after refresh should contain a usable Hibernate session factory. * If not already done, process the Hibernate domain object model from that bean. * @param event the context refreshed event bearing the new application context */ @Override public void handleContextRefreshedEvent(ContextRefreshedEvent event) { if (propertyKinds.isEmpty()) { final ApplicationContext context = event.getApplicationContext(); final SessionFactoryImplementor sessionFactory = context.getBean("sessionFactory", SessionFactoryImplementor.class); initialize(sessionFactory); } } /** * If the given property of the given class is actually declared by an interface that it implements, * find the name of the interface that first declares the property. * @param className the name of an {@link IObject} class * @param propertyName the name of a property of the class * @return the interface declaring the property, or {@code null} if none */ private Class<? extends IObject> getInterfaceForProperty(String className, String propertyName) { Class<? extends IObject> interfaceForProperty = null; Set<Class<? extends IObject>> interfacesFrom, interfacesTo; try { interfacesFrom = ImmutableSet.<Class<? extends IObject>>of(Class.forName(className).asSubclass(IObject.class)); } catch (ClassNotFoundException e) { log.error("could not load " + IObject.class.getName() + " subclass " + className); return null; } while (!interfacesFrom.isEmpty()) { interfacesTo = new HashSet<Class<? extends IObject>>(); for (final Class<? extends IObject> interfaceFrom : interfacesFrom) { if (interfaceFrom.isInterface() && BeanUtils.getPropertyDescriptor(interfaceFrom, propertyName) != null) { interfaceForProperty = interfaceFrom; } for (final Class<?> newInterface : interfaceFrom.getInterfaces()) { if (newInterface != IObject.class && IObject.class.isAssignableFrom(newInterface)) { interfacesTo.add(newInterface.asSubclass(IObject.class)); classesBySimpleName.put(newInterface.getSimpleName(), newInterface.asSubclass(IObject.class)); } } } interfacesFrom = interfacesTo; } return interfaceForProperty == null ? null : interfaceForProperty; } /** * @param name the name of an object property * @return if the property should be ignored */ private static boolean ignoreProperty(String name) { return "perm1".equals(name) || name.startsWith("_") || name.endsWith("CountPerOwner"); } /** * Process the Hibernate domain object model to initialize this class' instance fields. * No other method should write to them. * @param sessionFactory the Hibernate session factory */ private void initialize(SessionFactoryImplementor sessionFactory) { /* note all the direct superclasses */ final Map<String, String> superclasses = new HashMap<String, String>(); final Map<String, ClassMetadata> classesMetadata = sessionFactory.getAllClassMetadata(); for (final String className : classesMetadata.keySet()) { try { final Class<?> actualClass = Class.forName(className); if (IObject.class.isAssignableFrom(actualClass)) { classesBySimpleName.put(actualClass.getSimpleName(), actualClass.asSubclass(IObject.class)); final Set<String> subclassNames = sessionFactory.getEntityPersister(className).getEntityMetamodel().getSubclassEntityNames(); for (final String subclassName : subclassNames) { if (!subclassName.equals(className)) { final Class<?> actualSubclass = Class.forName(subclassName); if (actualSubclass.getSuperclass() == actualClass) { superclasses.put(subclassName, className); } } } } else { log.warn("mapped class " + className + " is not a " + IObject.class.getName()); } } catch (ClassNotFoundException e) { log.error("could not instantiate class", e); } } /* note the indirect superclasses and subclasses */ for (final Entry<String, String> superclassRelationship : superclasses.entrySet()) { final String startClass = superclassRelationship.getKey(); String superclass = superclassRelationship.getValue(); while (superclass != null) { allSuperclasses.put(startClass, superclass); allSubclasses.put(superclass, startClass); superclass = superclasses.get(superclass); } } /* queue for processing all the properties of all the mapped entities: name, type, nullability */ final Queue<PropertyDetails> propertyQueue = new LinkedList<PropertyDetails>(); final Map<String, Set<String>> allPropertyNames = new HashMap<String, Set<String>>(); for (final Entry<String, ClassMetadata> classMetadata : classesMetadata.entrySet()) { final String className = classMetadata.getKey(); final ClassMetadata metadata = classMetadata.getValue(); /* note name of identifier property */ classIdProperties.put(metadata.getEntityName(), metadata.getIdentifierPropertyName()); /* queue other properties */ final String[] propertyNames = metadata.getPropertyNames(); final Type[] propertyTypes = metadata.getPropertyTypes(); final boolean[] propertyNullabilities = metadata.getPropertyNullability(); for (int i = 0; i < propertyNames.length; i++) { final List<String> propertyPath = Collections.singletonList(propertyNames[i]); propertyQueue.add(new PropertyDetails(className, propertyPath, propertyTypes[i], propertyNullabilities[i])); } final Set<String> propertyNamesSet = new HashSet<String>(propertyNames.length); propertyNamesSet.addAll(Arrays.asList(propertyNames)); allPropertyNames.put(className, propertyNamesSet); } /* process each property to note entity linkages */ while (!propertyQueue.isEmpty()) { final PropertyDetails property = propertyQueue.remove(); if (ignoreProperty(property.path.get(property.path.size() - 1))) { continue; } /* if the property has a component type, queue the parts for processing */ if (property.type instanceof ComponentType) { final ComponentType componentType = (ComponentType) property.type; final String[] componentPropertyNames = componentType.getPropertyNames(); final Type[] componentPropertyTypes = componentType.getSubtypes(); final boolean[] componentPropertyNullabilities = componentType.getPropertyNullability(); for (int i = 0; i < componentPropertyNames.length; i++) { final List<String> componentPropertyPath = new ArrayList<String>(property.path.size() + 1); componentPropertyPath.addAll(property.path); componentPropertyPath.add(componentPropertyNames[i]); propertyQueue.add(new PropertyDetails( property.holder, componentPropertyPath, componentPropertyTypes[i], componentPropertyNullabilities[i])); } } else { /* determine if another mapped entity class is linked by this property */ final boolean isAssociatedEntity; if (property.type instanceof CollectionType) { final CollectionType ct = (CollectionType) property.type; isAssociatedEntity = sessionFactory.getCollectionPersister(ct.getRole()).getElementType().isEntityType(); } else { isAssociatedEntity = property.type instanceof AssociationType; } /* the property can link to entities, so process it further */ String propertyPath = Joiner.on('.').join(property.path); /* find if the property is accessible (e.g., not protected) */ boolean propertyIsAccessible = false; String classToInstantiateName = property.holder; Class<?> classToInstantiate = null; try { classToInstantiate = Class.forName(classToInstantiateName); while (Modifier.isAbstract(classToInstantiate.getModifiers())) { classToInstantiateName = allSubclasses.get(classToInstantiateName).iterator().next(); classToInstantiate = Class.forName(classToInstantiateName); } try { PropertyUtils.getNestedProperty(classToInstantiate.newInstance(), propertyPath); propertyIsAccessible = true; } catch (NoSuchMethodException e) { /* expected for collection properties */ } catch (NestedNullException e) { log.debug("guessing " + propertyPath + " of " + property.holder + " to be accessible"); propertyIsAccessible = true; } } catch (ReflectiveOperationException e) { log.error("could not probe property " + propertyPath + " of " + property.holder, e); continue; } /* build property report line for log */ final char arrowShaft = property.isNullable ? '-' : '='; final StringBuffer sb = new StringBuffer(); sb.append(property.holder); sb.append(' '); for (final String propertyName : property.path) { sb.append(arrowShaft); sb.append(arrowShaft); sb.append(propertyName); } sb.append(arrowShaft); sb.append(arrowShaft); sb.append("> "); final String valueClassName; if (isAssociatedEntity) { valueClassName = ((AssociationType) property.type).getAssociatedEntityName(sessionFactory); sb.append(valueClassName); } else { valueClassName = null; sb.append("value"); } if (property.type.isCollectionType()) { sb.append("[]"); } if (!propertyIsAccessible) { sb.append(" (inaccessible)"); } /* determine from which class the property is inherited, if at all */ String superclassWithProperty = null; String currentClass = property.holder; while (true) { currentClass = superclasses.get(currentClass); if (currentClass == null) { break; } else if (allPropertyNames.get(currentClass).contains(property.path.get(0))) { superclassWithProperty = currentClass; } } /* check if the property actually comes from an interface */ final String declaringClassName = superclassWithProperty == null ? property.holder : superclassWithProperty; final Class<? extends IObject> interfaceForProperty = getInterfaceForProperty(declaringClassName, property.path.get(0)); /* report where the property is declared */ if (superclassWithProperty != null) { sb.append(" from "); sb.append(superclassWithProperty); } else { if (interfaceForProperty != null) { sb.append(" see "); sb.append(interfaceForProperty.getName()); /* It would be nice to set PropertyDetails to have the interface as the holder, * but then properties would not be unique by declarer class and instance ID. */ } /* entity linkages by non-inherited properties are recorded */ if (valueClassName == null && property.path.size() > 1) { /* assume that the top-level property suffices for describing simple properties */ log.debug("recording " + propertyPath + " as " + property.path.get(0)); propertyPath = property.path.get(0); } final Entry<String, String> classPropertyName = Maps.immutableEntry(property.holder, propertyPath); if (valueClassName == null) { simpleProperties.put(property.holder, propertyPath); } else { linkedTo.put(property.holder, Maps.immutableEntry(valueClassName, propertyPath)); linkedBy.put(valueClassName, classPropertyName); } final PropertyKind propertyKind; if (property.type.isCollectionType()) { propertyKind = PropertyKind.COLLECTION; } else if (property.isNullable) { propertyKind = PropertyKind.OPTIONAL; } else { propertyKind = PropertyKind.REQUIRED; } propertyKinds.put(classPropertyName, propertyKind); if (propertyIsAccessible) { accessibleProperties.add(classPropertyName); } } if (log.isDebugEnabled()) { log.debug(sb.toString()); } } } log.info("initialized graph path bean with " + propertyKinds.size() + " properties"); } /** * @param simpleName the simple name of a mapped IObject class * @return the class with that simple name, or {@code null} if one is not known */ public Class<? extends IObject> getClassForSimpleName(String simpleName) { Class<? extends IObject> namedClass = classesBySimpleName.get(simpleName); if (namedClass != null) { return namedClass; } /* may have named a subclass, try guessing */ Class<?> subclass; try { subclass = Class.forName("omero.model." + simpleName); } catch (ClassNotFoundException e) { return null; } while (namedClass == null && subclass != Object.class) { subclass = subclass.getSuperclass(); namedClass = classesBySimpleName.get(subclass.getSimpleName()); } return namedClass; } /** * Get the superclasses of the given class, if any. * @param className the name of a class * @return the class' superclasses, never {@code null} */ public Set<String> getSuperclassesOf(String className) { return allSuperclasses.get(className); } /** * Get the name of this class and of its mapped superclasses. * @param className the name of a class * @return the class and its superclasses, never {@code null} */ public Collection<String> getSuperclassesOfReflexive(String className) { final Collection<String> superclasses = getSuperclassesOf(className); final Collection<String> superclassesReflexive = new ArrayList<String>(superclasses.size() + 1); superclassesReflexive.add(className); superclassesReflexive.addAll(superclasses); return superclassesReflexive; } /** * Get the subclasses of the given class, if any. * @param className the name of a class * @return the class' subclasses, never {@code null} */ public Set<String> getSubclassesOf(String className) { return allSubclasses.get(className); } /** * Get the name of this class and of its mapped subclasses. * @param className the name of a class * @return the class and its subclasses, never {@code null} */ public Collection<String> getSubclassesOfReflexive(String className) { final Collection<String> subclasses = getSubclassesOf(className); final Collection<String> subclassesReflexive = new ArrayList<String>(subclasses.size() + 1); subclassesReflexive.add(className); subclassesReflexive.addAll(subclasses); return subclassesReflexive; } /** * Get the classes and properties to which the given class links. * @param className the name of a class * @return the classes to which the given class links, and by which properties; never {@code null} */ public Set<Entry<String, String>> getLinkedTo(String className) { return linkedTo.get(className); } /** * Get the classes and properties that link to the given class. * @param className the name of a class * @return the classes that link to the given class, and by which properties; never {@code null} */ public Set<Entry<String, String>> getLinkedBy(String className) { return linkedBy.get(className); } /** * Get what kind of property a specific class property is. * @param className the name of a class * @param propertyName the name of a property declared, not just inherited, by that class * @return the kind of property it is */ public PropertyKind getPropertyKind(String className, String propertyName) { return propertyKinds.get(Maps.immutableEntry(className, propertyName)); } /** * Find if the given property can be accessed. * @param className the name of a class * @param propertyName the name of a property declared, not just inherited, by that class * @return if the property can be accessed */ public boolean isPropertyAccessible(String className, String propertyName) { return accessibleProperties.contains(Maps.immutableEntry(className, propertyName)); } /** * Get the identifier property for the given class. * @param className the name of a class * @return the identifier property, or {@code null} if one is not known */ public String getIdentifierProperty(String className) { return classIdProperties.get(className); } /** * Get the <q>simple</q> properties for the given class, not linking to other mapped classes. * @param className the name of a class * @return the <q>simple</q> properties of the given class; never {@code null} */ public Set<String> getSimpleProperties(String className) { return simpleProperties.get(className); } }