/*
* Copyright (C) 2014-2015 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.io.FileWriter;
import java.io.IOException;
import java.io.Writer;
import java.util.ArrayList;
import java.util.Arrays;
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.Queue;
import java.util.Set;
import java.util.SortedMap;
import java.util.SortedSet;
import java.util.TreeMap;
import java.util.TreeSet;
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.CustomType;
import org.hibernate.type.EnumType;
import org.hibernate.type.ListType;
import org.hibernate.type.MapType;
import org.hibernate.type.Type;
import org.hibernate.usertype.UserType;
import org.springframework.beans.BeanUtils;
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 com.google.common.collect.SortedSetMultimap;
import com.google.common.collect.TreeMultimap;
import ome.model.IObject;
import ome.model.units.GenericEnumType;
import ome.model.units.Unit;
import ome.services.graphs.GraphPathBean.PropertyDetails;
import ome.services.scheduler.ThreadPool;
import ome.system.OmeroContext;
import ome.tools.hibernate.ListAsSQLArrayUserType;
/**
* A standalone tool for producing a summary of the Hibernate object mapping for our Sphinx documentation. One may invoke it with
* <code>java -cp lib/server/\* `bin/omero config get | awk '{print"-D"$1}'` ome.services.graphs.GraphPathReport EveryObject.txt</code>.
* Comments in code indicate different formatting possibilities for the output.
* If not using {@code |} prefixes then one may transform the output via {@code fold -sw72}.
* This class is heavily based on the {@link GraphPathBean}.
* @author m.t.b.carroll@dundee.ac.uk
* @since 5.1.0
*
*/
public class GraphPathReport {
// TODO: Add enough information to the GraphPathBean that this class may use that instead of the session factory,
// and remove code from here that resembles that from the bean.
private static SessionFactoryImplementor sessionFactory;
private static Writer out;
/**
* 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 static 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) {
/* does not log error as in GraphPathBean */
System.err.println("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));
}
}
}
interfacesFrom = interfacesTo;
}
return interfaceForProperty == null ? null : interfaceForProperty;
}
/**
* Trim the package name off a full class name.
* @param fullName the full class name
* @return the class' simple name
*/
private static String getSimpleName(String fullName) {
return fullName.substring(1 + fullName.lastIndexOf('.'));
}
/**
* @param className a class name
* @return a Sphinx label for that class
*/
private static String labelFor(String className) {
return "OMERO model class " + className;
}
/**
* @param className a class name
* @param propertyName a property name
* @return a Sphinx label for that class property
*/
private static String labelFor(String className, String propertyName) {
return "OMERO model property " + className + '.' + propertyName;
}
/**
* @param className a class name
* @return a Sphinx link to that class
*/
private static String linkTo(String className) {
final StringBuffer sb = new StringBuffer();
sb.append(":ref:");
sb.append('`');
sb.append(className);
sb.append(' ');
sb.append('<');
sb.append(labelFor(className));
sb.append('>');
sb.append('`');
return sb.toString();
}
/**
* @param className a class name
* @param propertyName a property name
* @return a Sphinx link to that class property
*/
private static String linkTo(String className, String propertyName) {
final StringBuffer sb = new StringBuffer();
sb.append(":ref:");
sb.append('`');
sb.append(className);
sb.append('.');
sb.append(propertyName);
sb.append(' ');
sb.append('<');
sb.append(labelFor(className/*, propertyName*/));
sb.append('>');
sb.append('`');
return sb.toString();
}
/**
* @param className the name of an OMERO model Java class
* @return a Sphinx link to that class' documentation
*/
private static String linkToJavadoc(String className) {
final StringBuffer sb = new StringBuffer();
sb.append(":javadoc:");
sb.append('`');
sb.append(getSimpleName(className));
sb.append(' ');
sb.append('<');
sb.append(className.replace('.', '/'));
sb.append(".html");
sb.append('>');
sb.append('`');
return sb.toString();
}
/**
* @param name the name of an object property
* @return if the property should be ignored
*/
private static boolean ignoreProperty(String name) {
return name.startsWith("_") || name.endsWith("CountPerOwner");
}
/**
* Process the Hibernate domain object model and write a report of the mapped objects.
* @throws IOException if there was a problem in writing to the output file
*/
private static void report() throws IOException {
/* note all the direct superclasses and subclasses */
final Map<String, String> superclasses = new HashMap<String, String>();
final SortedSetMultimap<String, String> subclasses = TreeMultimap.create();
@SuppressWarnings("unchecked")
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)) {
@SuppressWarnings("unchecked")
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);
subclasses.put(getSimpleName(className), getSimpleName(subclassName));
}
}
}
} else {
System.err.println("error: mapped class " + className + " is not a " + IObject.class.getName());
}
} catch (ClassNotFoundException e) {
System.err.println("error: could not instantiate class: " + e);
}
}
/* 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 Map.Entry<String, ClassMetadata> classMetadata : classesMetadata.entrySet()) {
final String className = classMetadata.getKey();
final ClassMetadata metadata = classMetadata.getValue();
final String[] propertyNames = metadata.getPropertyNames();
final Type[] propertyTypes = metadata.getPropertyTypes();
final boolean[] propertyNullabilities = metadata.getPropertyNullability();
for (int i = 0; i < propertyNames.length; i++) {
if (!ignoreProperty(propertyNames[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);
}
/* for linkedBy, X -> Y, Z: class X is linked to by class Y with Y's property Z */
final SetMultimap<String, Map.Entry<String, String>> linkedBy = HashMultimap.create();
final SetMultimap<String, String> linkers = HashMultimap.create();
final SortedMap<String, SortedMap<String, String>> classPropertyReports = new TreeMap<String, SortedMap<String, String>>();
/* process each property to note entity linkages */
while (!propertyQueue.isEmpty()) {
final PropertyDetails property = propertyQueue.remove();
/* 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++) {
if (!ignoreProperty(componentPropertyNames[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 this property links to another entity */
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;
}
/* determine the class and property name for reporting */
final String holderSimpleName = getSimpleName(property.holder);
final String propertyPath = Joiner.on('.').join(property.path);
/* build a report line for this property */
final StringBuffer sb = new StringBuffer();
final String valueClassName;
if (isAssociatedEntity) {
/* entity linkages by non-inherited properties are recorded */
final String valueName = ((AssociationType) property.type).getAssociatedEntityName(sessionFactory);
final String valueSimpleName = getSimpleName(valueName);
final Map.Entry<String, String> classPropertyName = Maps.immutableEntry(holderSimpleName, propertyPath);
linkers.put(holderSimpleName, propertyPath);
linkedBy.put(valueSimpleName, classPropertyName);
valueClassName = linkTo(valueSimpleName);
} else {
/* find a Sphinx representation for this property value type */
final UserType userType;
if (property.type instanceof CustomType) {
userType = ((CustomType) property.type).getUserType();
} else {
userType = null;
}
if (property.type instanceof EnumType) {
valueClassName = "enumeration";
} else if (userType instanceof GenericEnumType) {
@SuppressWarnings("unchecked")
final Class<? extends Unit> unitQuantityClass = ((GenericEnumType) userType).getQuantityClass();
valueClassName = "enumeration of " + linkToJavadoc(unitQuantityClass.getName());
} else if (property.type instanceof ListType || userType instanceof ListAsSQLArrayUserType) {
valueClassName = "list";
} else if (property.type instanceof MapType) {
valueClassName = "map";
} else {
valueClassName = "``" + property.type.getName() + "``";
}
}
sb.append(valueClassName);
if (property.type.isCollectionType()) {
sb.append(" (multiple)");
} else if (property.isNullable) {
sb.append(" (optional)");
}
/* 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(linkTo(getSimpleName(superclassWithProperty)));
} else {
if (interfaceForProperty != null) {
sb.append(", see ");
sb.append(linkToJavadoc(interfaceForProperty.getName()));
}
}
SortedMap<String, String> byProperty = classPropertyReports.get(holderSimpleName);
if (byProperty == null) {
byProperty = new TreeMap<String, String>();
classPropertyReports.put(holderSimpleName, byProperty);
}
byProperty.put(propertyPath, sb.toString());
}
}
/* the information is gathered, now write the report */
out.write("Glossary of all OMERO Model Objects\n");
out.write("===================================\n\n");
out.write("Overview\n");
out.write("--------\n\n");
out.write("Reference\n");
out.write("---------\n\n");
for (final Map.Entry<String, SortedMap<String, String>> byClass : classPropertyReports.entrySet()) {
/* label the class heading */
final String className = byClass.getKey();
out.write(".. _" + labelFor(className) + ":\n\n");
out.write(className + "\n");
final char[] underline = new char[className.length()];
for (int i = 0; i < underline.length; i++) {
underline[i] = '"';
}
out.write(underline);
out.write("\n\n");
/* note the class' relationships */
final SortedSet<String> superclassOf = new TreeSet<String>();
for (final String subclass : subclasses.get(className)) {
superclassOf.add(linkTo(subclass));
}
final SortedSet<String> linkerText = new TreeSet<String>();
for (final Map.Entry<String, String> linker : linkedBy.get(className)) {
linkerText.add(linkTo(linker.getKey(), linker.getValue()));
}
if (!(superclassOf.isEmpty() && linkerText.isEmpty())) {
/* write the class' relationships */
/*
out.write("Relationships\n");
out.write("^^^^^^^^^^^^^\n\n");
*/
if (!superclassOf.isEmpty()) {
out.write("Subclasses: " + Joiner.on(", ").join(superclassOf) + "\n\n");
}
if (!linkerText.isEmpty()) {
out.write("Used by: " + Joiner.on(", ").join(linkerText) + "\n\n");
}
}
/* write the class' properties */
/*
out.write("Properties\n");
out.write("^^^^^^^^^^\n\n");
*/
out.write("Properties:\n");
for (final Map.Entry<String, String> byProperty : byClass.getValue().entrySet()) {
final String propertyName = byProperty.getKey();
// if (linkers.containsEntry(className, propertyName)) {
// /* label properties that have other entities as values */
// out.write(".. _" + labelFor(className, propertyName) + ":\n\n");
// }
out.write(" | " + propertyName + ": " + byProperty.getValue() + "\n" /* \n */);
}
out.write("\n");
}
}
/**
* Generate a Sphinx report of OMERO Hibernate entities.
* @param argv the output filename
* @throws IOException if the report cannot be written to the file
*/
public static void main(String[] argv) throws IOException {
if (argv.length != 1) {
System.err.println("must give output filename as single argument");
System.exit(1);
}
out = new FileWriter(argv[0]);
final OmeroContext context = OmeroContext.getManagedServerContext();
sessionFactory = context.getBean("sessionFactory", SessionFactoryImplementor.class);
report();
out.close();
context.getBean("threadPool", ThreadPool.class).getExecutor().shutdown();
context.closeAll();
}
}