package com.psddev.cms.view; import java.io.File; import java.io.IOException; import java.lang.reflect.Method; import java.lang.reflect.ParameterizedType; import java.lang.reflect.Type; import java.lang.reflect.WildcardType; import java.util.ArrayList; import java.util.Collections; import java.util.HashSet; import java.util.LinkedHashSet; import java.util.List; import java.util.Map; import java.util.Set; import java.util.stream.Collectors; import javax.servlet.ServletException; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; import com.psddev.dari.util.ClassFinder; import com.psddev.dari.util.CodeUtils; import com.psddev.dari.util.CompactMap; import com.psddev.dari.util.DebugFilter; import com.psddev.dari.util.DebugServlet; import com.psddev.dari.util.ObjectUtils; import com.psddev.dari.util.TypeDefinition; @SuppressWarnings("serial") public class ViewInterfaceSchemaDebugServlet extends DebugServlet { @Override public String getName() { return "View Interface: Schema Viewer"; } @Override public List<String> getPaths() { return Collections.singletonList("view-interface-schema"); } @Override protected void service( HttpServletRequest request, HttpServletResponse response) throws IOException, ServletException { new DebugFilter.PageWriter(getServletContext(), request, response) { { List<TypeDefinition<?>> viewTypes = ClassFinder.findClasses(Object.class) .stream() .filter(c -> c.isAnnotationPresent(ViewInterface.class)) .map(TypeDefinition::getInstance) .collect(Collectors.toList()); Collections.sort(viewTypes, (t1, t2) -> t1.getObjectClass().getSimpleName().compareTo(t2.getObjectClass().getSimpleName())); Set<TypeDefinition<?>> selectedTypes = new HashSet<>(); List<String> typeIds = page.params(String.class, "typeId"); if (typeIds != null) { Set<String> typeIdsSet = new HashSet<>(); typeIdsSet.addAll(typeIds); for (TypeDefinition<?> t : viewTypes) { if (typeIdsSet.contains(t.getObjectClass().getName())) { selectedTypes.add(t); } } } startPage("View Interface", "Schema"); { writeStart("form", "method", "get"); { writeStart("select", "multiple", "multiple", "name", "typeId", "style", "width: 90%;"); { for (TypeDefinition<?> t : viewTypes) { writeStart("option", "selected", selectedTypes.contains(t) ? "selected" : null, "value", t.getObjectClass().getName()); { writeHtml(t.getObjectClass().getSimpleName()); writeHtml(" (").writeHtml(t.getObjectClass().getName()).writeHtml(")"); } writeEnd(); } } writeEnd(); writeElement("br"); writeElement("input", "class", "btn", "type", "submit", "value", "View"); } writeEnd(); includeStylesheet("/_resource/chosen/chosen.css"); includeScript("/_resource/chosen/chosen.jquery.min.js"); writeStart("script", "type", "text/javascript"); { write("(function() {"); { write("$('select[name=typeId]').chosen({ 'search_contains': true });"); } write("})();"); } writeEnd(); writeStart("style", "type", "text/css"); { write(".column { display: table-cell; padding-right: 15em; text-align: center; vertical-align: top; }"); write(".column dl { margin-bottom: 0; }"); write(".type { border: 1px solid black; display: inline-block; margin-bottom: 5em; padding: 0.5em; text-align: left; }"); write(".type h2 { white-space: nowrap; }"); write(".type dt { margin-bottom: 5px; }"); write(".type dd:last-child table { margin-bottom: 0; }"); write(".type .reference { color: white; white-space: nowrap; display: inline-block; padding: 0 4px; margin: 1px 0 }"); } writeEnd(); writeStart("div", "class", "types"); { Set<TypeDefinition<?>> allTypes = new HashSet<>(); Set<TypeDefinition<?>> currentTypes = new HashSet<>(selectedTypes); // how many levels deep to follow int depth = page.paramOrDefault(int.class, "d", -1); while (!currentTypes.isEmpty() && (depth--) != 0) { writeStart("div", "class", "column"); { allTypes.addAll(currentTypes); Set<TypeDefinition<?>> nextTypes = new LinkedHashSet<>(); for (TypeDefinition<?> t : currentTypes) { Map<String, List<Method>> fieldsByClass = new CompactMap<>(); for (Method field : t.getAllGetters().values()) { String declaringClass = field.getDeclaringClass().getName(); if (declaringClass != null) { List<Method> fields = fieldsByClass.get(declaringClass); if (fields == null) { fields = new ArrayList<>(); fieldsByClass.put(declaringClass, fields); } fields.add(field); } } writeStart("div").writeEnd(); writeStart("div", "class", "type", "id", "type-" + t.getObjectClass().getName().replace('.', '_')); { writeStart("h2").writeHtml(t.getObjectClass().getSimpleName()).writeEnd(); writeStart("dl"); { for (Map.Entry<String, List<Method>> entry : fieldsByClass.entrySet()) { String className = entry.getKey(); File source = CodeUtils.getSource(className); writeStart("dt"); { if (source == null) { writeHtml(className); } else { writeStart("a", "target", "_blank", "href", DebugFilter.Static.getServletPath(page.getRequest(), "code", "file", source)); { writeHtml(className); } writeEnd(); } } writeEnd(); writeStart("dd"); { writeStart("table", "class", "table table-condensed"); { writeStart("tbody"); { for (Method field : entry.getValue()) { String internalName = field.getName(); Class<?> internalType = field.getReturnType(); Class<?> internalItemType = null; String internalNameLabel = Character.toLowerCase(internalName.charAt(3)) + internalName.substring(4); String internalTypeLabel = ""; String internalItemTypeLabel; if (Iterable.class.isAssignableFrom(internalType)) { Type genericType = field.getGenericReturnType(); if (genericType instanceof ParameterizedType) { ParameterizedType parameterizedType = (ParameterizedType) genericType; Type[] typeArguments = parameterizedType.getActualTypeArguments(); if (typeArguments.length == 1) { Type typeArgument = typeArguments[0]; if (typeArgument instanceof WildcardType) { WildcardType wildcardType = (WildcardType) typeArgument; Type[] upperBounds = wildcardType.getUpperBounds(); if (upperBounds.length == 1) { Type upperBound = upperBounds[0]; if (upperBound instanceof Class) { internalItemType = (Class<?>) upperBound; } } } else if (typeArgument instanceof Class) { internalItemType = (Class<?>) typeArgument; } } } if (internalItemType == null) { internalItemType = Object.class; } internalTypeLabel = "list/"; } else { internalItemType = internalType; } if (CharSequence.class.isAssignableFrom(internalItemType)) { internalItemTypeLabel = "text"; } else if (Number.class.isAssignableFrom(internalItemType)) { internalItemTypeLabel = "number"; } else if (Boolean.class.isAssignableFrom(internalItemType)) { internalItemTypeLabel = "boolean"; } else if (Map.class.isAssignableFrom(internalItemType)) { internalItemTypeLabel = "map"; } else if (internalItemType == Object.class) { internalItemTypeLabel = "object"; } else { internalItemTypeLabel = "view"; } internalTypeLabel += internalItemTypeLabel; // only show fields that reference other types if (page.param(boolean.class, "nf") && !internalItemTypeLabel.equals("view")) { continue; } writeStart("tr"); { writeStart("td").writeHtml(internalNameLabel).writeEnd(); writeStart("td").writeHtml(internalTypeLabel).writeEnd(); writeStart("td"); { if ("view".equals(internalItemTypeLabel)) { Set<Class<?>> itemTypeClasses = new HashSet<>(ClassFinder.findClasses(internalItemType)); itemTypeClasses.add(internalItemType); List<TypeDefinition<?>> itemTypes = itemTypeClasses.stream() .filter(c -> c.isAnnotationPresent(ViewInterface.class)) .map(TypeDefinition::getInstance) .collect(Collectors.toList()); if (!ObjectUtils.isBlank(itemTypes)) { for (TypeDefinition<?> itemType : itemTypes) { if (!allTypes.contains(itemType)) { nextTypes.add(itemType); } writeStart("a", "class", "label reference", "data-typeId", itemType.getObjectClass().getName().replace('.', '_'), "href", page.url(null, "typeId", itemType.getObjectClass().getName(), "nf", page.param(Boolean.class, "nf"), "d", page.param(Integer.class, "d"))); { writeHtml(itemType.getObjectClass().getSimpleName()); } writeEnd(); writeTag("br"); } } } } writeEnd(); } writeEnd(); } } writeEnd(); } writeEnd(); } writeEnd(); } } writeEnd(); } writeEnd(); } currentTypes = nextTypes; } writeEnd(); } } writeEnd(); includeScript("/_resource/dari/db-schema.js"); } endPage(); } }; } }