diff --git a/java/com/google/domain/registry/tools/GetSchemaTreeCommand.java b/java/com/google/domain/registry/tools/GetSchemaTreeCommand.java new file mode 100644 index 000000000..67b976886 --- /dev/null +++ b/java/com/google/domain/registry/tools/GetSchemaTreeCommand.java @@ -0,0 +1,167 @@ +// Copyright 2016 The Domain Registry Authors. All Rights Reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package com.google.domain.registry.tools; + +import static com.google.common.collect.Ordering.arbitrary; +import static com.google.domain.registry.model.EntityClasses.ALL_CLASSES; +import static java.lang.ClassLoader.getSystemClassLoader; +import static java.lang.reflect.Modifier.isAbstract; + +import com.google.common.base.Strings; +import com.google.common.collect.Multimap; +import com.google.common.collect.Multimaps; +import com.google.common.collect.Ordering; +import com.google.common.collect.TreeMultimap; +import com.google.domain.registry.model.BackupGroupRoot; +import com.google.domain.registry.model.annotations.NotBackedUp; +import com.google.domain.registry.model.annotations.VirtualEntity; +import com.google.domain.registry.tools.Command.GtechCommand; + +import com.beust.jcommander.Parameters; +import com.googlecode.objectify.annotation.Entity; +import com.googlecode.objectify.annotation.EntitySubclass; +import com.googlecode.objectify.annotation.Parent; + +import java.io.Serializable; +import java.lang.reflect.Field; +import java.lang.reflect.ParameterizedType; +import java.util.HashMap; +import java.util.HashSet; +import java.util.Map; +import java.util.Set; + +/** Visualizes the schema parentage tree. */ +@Parameters(commandDescription = "Generate a model schema file") +final class GetSchemaTreeCommand implements GtechCommand { + + /** Mapping from parent classes in the Datastore sense to child classes. */ + private final Multimap, Class> hierarchy = + TreeMultimap.create(arbitrary(), new PrintableNameOrdering()); + + /** Mapping from superclasses used in parentage to concrete subclasses. */ + private Multimap, Class> superclassToSubclasses; + + @Override + public void run() throws Exception { + // Get the @Parent type for each class. + Map, Class> entityToParentType = new HashMap<>(); + for (Class clazz : ALL_CLASSES) { + entityToParentType.put(clazz, getParentType(clazz)); + } + // Find super types like EppResource that are used as parents in place of actual entity types. + Set> superclasses = new HashSet<>(); + for (Class clazz : ALL_CLASSES) { + Class parentType = entityToParentType.get(clazz); + if (!ALL_CLASSES.contains(parentType) && !Object.class.equals(parentType)) { + superclasses.add(parentType); + } + } + // Find the subclasses for each superclass we just found, and map them to their superclasses. + Map, Class> subclassToSuperclass = new HashMap<>(); + for (Class clazz : ALL_CLASSES) { + for (Class superclass : superclasses) { + if (superclass.isAssignableFrom(clazz)) { + subclassToSuperclass.put(clazz, superclass); + break; + } + } + } + // Map @EntitySubclass classes to their superclasses. + for (Class clazz : ALL_CLASSES) { + if (clazz.isAnnotationPresent(EntitySubclass.class)) { + Class entityClass = clazz; + while (!entityClass.isAnnotationPresent(Entity.class)) { + entityClass = entityClass.getSuperclass(); + } + if (subclassToSuperclass.containsKey(clazz)) { + subclassToSuperclass.put(entityClass, subclassToSuperclass.get(clazz)); + } + subclassToSuperclass.put(clazz, entityClass); + } + } + // Build the parentage hierarchy, replacing subclasses with superclasses wherever possible. + for (Class clazz : ALL_CLASSES) { + Class superclass = clazz; + while (subclassToSuperclass.containsKey(superclass)) { + superclass = subclassToSuperclass.get(superclass); + } + hierarchy.put(entityToParentType.get(clazz), superclass == null ? clazz : superclass); + } + // Build up the superclass to subclass mapping. + superclassToSubclasses = Multimaps.invertFrom( + Multimaps.forMap(subclassToSuperclass), + TreeMultimap., Class>create(arbitrary(), new PrintableNameOrdering())); + printTree(Object.class, 0); + } + + private Class getParentType(Class clazz) { + for (; clazz != null; clazz = clazz.getSuperclass()) { + for (Field field : clazz.getDeclaredFields()) { + if (field.isAnnotationPresent(Parent.class)) { + try { + return getSystemClassLoader().loadClass( + ((ParameterizedType) field.getGenericType()).getActualTypeArguments()[0] + .toString() + .replace("? extends ", "") + .replace("class ", "")); + } catch (ClassNotFoundException e) { + throw new RuntimeException(e); + } + } + } + } + return Object.class; + } + + private void printTree(Class parent, int indent) { + for (Class clazz : hierarchy.get(parent)) { + System.out.println(new StringBuilder(Strings.repeat(" ", indent)) + .append(indent == 0 ? "" : "↳ ") + .append(getPrintableName(clazz)) + .append(isAbstract(clazz.getModifiers()) ? " (abstract)" : "") + .append(clazz.isAnnotationPresent(VirtualEntity.class) ? " (virtual)" : "") + .append(clazz.isAnnotationPresent(NotBackedUp.class) ? " (not backed up)" : "") + .append(BackupGroupRoot.class.isAssignableFrom(clazz) ? " (bgr)" : "")); + printSubclasses(clazz, indent + 2); + printTree(clazz, indent + 2); + if (indent == 0) { + System.out.println(); // Separate the entity groups with a line. + } + } + } + + private void printSubclasses(Class parent, int indent) { + for (Class clazz : superclassToSubclasses.get(parent)) { + System.out.println(new StringBuilder(Strings.repeat(" ", indent)) + .append("- ") + .append(getPrintableName(clazz)) + .append(clazz.isAnnotationPresent(EntitySubclass.class) ? " (subclass)" : "")); + printSubclasses(clazz, indent + 2); + } + } + + static String getPrintableName(Class clazz) { + return clazz.isMemberClass() + ? getPrintableName(clazz.getDeclaringClass()) + "." + clazz.getSimpleName() + : clazz.getSimpleName(); + } + + static class PrintableNameOrdering extends Ordering> implements Serializable { + @Override + public int compare(Class left, Class right) { + return getPrintableName(left).compareTo(getPrintableName(right)); + } + } +} diff --git a/java/com/google/domain/registry/tools/GtechTool.java b/java/com/google/domain/registry/tools/GtechTool.java index 0b6a54cb0..8cb1fc755 100644 --- a/java/com/google/domain/registry/tools/GtechTool.java +++ b/java/com/google/domain/registry/tools/GtechTool.java @@ -54,6 +54,7 @@ public final class GtechTool { .put("get_host", GetHostCommand.class) .put("get_registrar", GetRegistrarCommand.class) .put("get_schema", GetSchemaCommand.class) + .put("get_schema_tree", GetSchemaTreeCommand.class) .put("get_tld", GetTldCommand.class) .put("hash_certificate", HashCertificateCommand.class) .put("list_credits", ListCreditsCommand.class)