ome.services.graphs.GraphPathReport.java Source code

Java tutorial

Introduction

Here is the source code for ome.services.graphs.GraphPathReport.java

Source

/*
 * 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();
    }
}