/** * Copyright (C) 2010-2017 Structr GmbH * * This file is part of Structr <http://structr.org>. * * Structr 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 3 of the * License, or (at your option) any later version. * * Structr 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 Structr. If not, see <http://www.gnu.org/licenses/>. */ package org.structr.schema; import java.util.Arrays; import java.util.Collection; import java.util.Collections; import java.util.Comparator; import java.util.HashMap; import java.util.HashSet; import java.util.Iterator; import java.util.LinkedHashMap; import java.util.LinkedHashSet; import java.util.LinkedList; import java.util.List; import java.util.Map; import java.util.Set; import java.util.TreeMap; import javatools.parsers.PlingStemmer; import org.apache.commons.lang3.StringUtils; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.structr.api.NotFoundException; import org.structr.api.graph.PropertyContainer; import org.structr.common.CaseHelper; import org.structr.common.GraphObjectComparator; import org.structr.common.PermissionPropagation; import org.structr.common.PropertyView; import org.structr.common.SecurityContext; import org.structr.common.ValidationHelper; import org.structr.common.View; import org.structr.common.error.ErrorBuffer; import org.structr.common.error.FrameworkException; import org.structr.common.error.InvalidPropertySchemaToken; import org.structr.core.Export; import org.structr.core.GraphObject; import org.structr.core.GraphObjectMap; import org.structr.core.Services; import org.structr.core.app.App; import org.structr.core.app.StructrApp; import org.structr.core.converter.PropertyConverter; import org.structr.core.entity.AbstractNode; import org.structr.core.entity.AbstractRelationship; import org.structr.core.entity.AbstractSchemaNode; import org.structr.core.entity.DynamicResourceAccess; import org.structr.core.entity.Relation; import org.structr.core.entity.ResourceAccess; import org.structr.core.entity.SchemaMethod; import static org.structr.core.entity.SchemaMethod.source; import org.structr.core.entity.SchemaNode; import org.structr.core.entity.SchemaProperty; import org.structr.core.entity.SchemaRelationshipNode; import org.structr.core.entity.SchemaView; import org.structr.core.graph.ModificationQueue; import org.structr.core.graph.NodeAttribute; import org.structr.core.graph.NodeInterface; import org.structr.core.graph.RelationshipInterface; import org.structr.core.property.BooleanProperty; import org.structr.core.property.GenericProperty; import org.structr.core.property.LongProperty; import org.structr.core.property.PropertyKey; import org.structr.core.property.RelationProperty; import org.structr.core.property.StringProperty; import org.structr.module.StructrModule; import org.structr.schema.action.ActionEntry; import org.structr.schema.action.Actions; import org.structr.schema.parser.BooleanArrayPropertyParser; import org.structr.schema.parser.BooleanPropertyParser; import org.structr.schema.parser.CountPropertyParser; import org.structr.schema.parser.CypherPropertyParser; import org.structr.schema.parser.DatePropertyParser; import org.structr.schema.parser.DoubleArrayPropertyParser; import org.structr.schema.parser.DoublePropertyParser; import org.structr.schema.parser.EnumPropertyParser; import org.structr.schema.parser.FunctionPropertyParser; import org.structr.schema.parser.IntPropertyParser; import org.structr.schema.parser.IntegerArrayPropertyParser; import org.structr.schema.parser.JoinPropertyParser; import org.structr.schema.parser.LongArrayPropertyParser; import org.structr.schema.parser.LongPropertyParser; import org.structr.schema.parser.NotionPropertyParser; import org.structr.schema.parser.PropertyDefinition; import org.structr.schema.parser.PropertySourceGenerator; import org.structr.schema.parser.StringArrayPropertyParser; import org.structr.schema.parser.StringBasedPropertyDefinition; import org.structr.schema.parser.StringPropertySourceGenerator; import org.structr.schema.parser.Validator; /** * * */ public class SchemaHelper { private static final Logger logger = LoggerFactory.getLogger(SchemaHelper.class.getName()); private static final String WORD_SEPARATOR = "_"; public enum Type { String, StringArray, LongArray, DoubleArray, IntegerArray, BooleanArray, Integer, Long, Double, Boolean, Enum, Date, Count, Function, Notion, Cypher, Join, Thumbnail; } public static final Map<Type, Class<? extends PropertySourceGenerator>> parserMap = new TreeMap<>(new ReverseTypeComparator()); private static final Map<String, String> normalizedEntityNameCache = new LinkedHashMap<>(); static { // IMPORTANT: parser map must be sorted by type name length // because we look up the parsers using "startsWith"! parserMap.put(Type.BooleanArray, BooleanArrayPropertyParser.class); parserMap.put(Type.IntegerArray, IntegerArrayPropertyParser.class); parserMap.put(Type.DoubleArray, DoubleArrayPropertyParser.class); parserMap.put(Type.StringArray, StringArrayPropertyParser.class); parserMap.put(Type.LongArray, LongArrayPropertyParser.class); parserMap.put(Type.Function, FunctionPropertyParser.class); parserMap.put(Type.Boolean, BooleanPropertyParser.class); parserMap.put(Type.Integer, IntPropertyParser.class); parserMap.put(Type.String, StringPropertySourceGenerator.class); parserMap.put(Type.Double, DoublePropertyParser.class); parserMap.put(Type.Notion, NotionPropertyParser.class); parserMap.put(Type.Cypher, CypherPropertyParser.class); parserMap.put(Type.Long, LongPropertyParser.class); parserMap.put(Type.Enum, EnumPropertyParser.class); parserMap.put(Type.Date, DatePropertyParser.class); parserMap.put(Type.Count, CountPropertyParser.class); parserMap.put(Type.Join, JoinPropertyParser.class); } /** * Tries to normalize (and singularize) the given string so that it * resolves to an existing entity type. * * @param possibleEntityString * @return the normalized entity name in its singular form */ public static String normalizeEntityName(String possibleEntityString) { if (possibleEntityString == null) { return null; } if ("/".equals(possibleEntityString)) { return "/"; } final StringBuilder result = new StringBuilder(); if (possibleEntityString.contains("/")) { final String[] names = StringUtils.split(possibleEntityString, "/"); for (String possibleEntityName : names) { // CAUTION: this cache might grow to a very large size, as it // contains all normalized mappings for every possible // property key / entity name that is ever called. String normalizedType = normalizedEntityNameCache.get(possibleEntityName); if (normalizedType == null) { normalizedType = StringUtils.capitalize(CaseHelper.toUpperCamelCase(stem(possibleEntityName))); } result.append(normalizedType).append("/"); } return StringUtils.removeEnd(result.toString(), "/"); } else { // CAUTION: this cache might grow to a very large size, as it // contains all normalized mappings for every possible // property key / entity name that is ever called. String normalizedType = normalizedEntityNameCache.get(possibleEntityString); if (normalizedType == null) { normalizedType = StringUtils.capitalize(CaseHelper.toUpperCamelCase(stem(possibleEntityString))); } return normalizedType; } } private static String stem(final String term) { String lastWord; String begin = ""; if (StringUtils.contains(term, WORD_SEPARATOR)) { lastWord = StringUtils.substringAfterLast(term, WORD_SEPARATOR); begin = StringUtils.substringBeforeLast(term, WORD_SEPARATOR); } else { lastWord = term; } lastWord = PlingStemmer.stem(lastWord); return begin.concat(WORD_SEPARATOR).concat(lastWord); } public static Class getEntityClassForRawType(final String rawType) { // first try: raw name Class type = getEntityClassForRawType(rawType, false); if (type == null) { // second try: normalized name type = getEntityClassForRawType(rawType, true); } return type; } private static Class getEntityClassForRawType(final String rawType, final boolean normalize) { final String normalizedEntityName = normalize ? normalizeEntityName(rawType) : rawType; final ConfigurationProvider configuration = StructrApp.getConfiguration(); // first try: node entity Class type = configuration.getNodeEntities().get(normalizedEntityName); // second try: relationship entity if (type == null) { type = configuration.getRelationshipEntities().get(normalizedEntityName); } // third try: interface if (type == null) { type = configuration.getInterfaces().get(normalizedEntityName); } // store type but only if it exists! if (type != null) { normalizedEntityNameCache.put(rawType, type.getSimpleName()); } // fallback to support generic queries on all types if (type == null) { if (AbstractNode.class.getSimpleName().equals(rawType)) { return AbstractNode.class; } if (NodeInterface.class.getSimpleName().equals(rawType)) { return NodeInterface.class; } if (AbstractRelationship.class.getSimpleName().equals(rawType)) { return AbstractRelationship.class; } if (RelationshipInterface.class.getSimpleName().equals(rawType)) { return RelationshipInterface.class; } } return type; } public static boolean reloadSchema(final ErrorBuffer errorBuffer) { try { final App app = StructrApp.getInstance(); final List<SchemaNode> existingSchemaNodes = app.nodeQuery(SchemaNode.class).getAsList(); cleanUnusedDynamicGrants(existingSchemaNodes); for (final SchemaNode schemaNode : existingSchemaNodes) { createDynamicGrants(schemaNode.getResourceSignature()); } for (final SchemaRelationshipNode schemaRelationship : StructrApp.getInstance().nodeQuery(SchemaRelationshipNode.class).getAsList()) { createDynamicGrants(schemaRelationship.getResourceSignature()); createDynamicGrants(schemaRelationship.getInverseResourceSignature()); } } catch (Throwable t) { logger.warn("", t); } return SchemaService.reloadSchema(errorBuffer); } public static void cleanUnusedDynamicGrants(final List<SchemaNode> existingSchemaNodes) { try { final List<DynamicResourceAccess> existingDynamicGrants = StructrApp.getInstance().nodeQuery(DynamicResourceAccess.class).getAsList(); final Set<String> existingSchemaNodeNames = new HashSet<>(); for (final SchemaNode schemaNode : existingSchemaNodes) { existingSchemaNodeNames.add(schemaNode.getResourceSignature()); } for (final DynamicResourceAccess grant : existingDynamicGrants) { boolean foundAllParts = true; final String sig; try { sig = grant.getResourceSignature(); } catch (NotFoundException nfe) { logger.debug("Unable to get signature from grant"); continue; } // Try to find schema nodes for all parts of the grant signature final String[] parts = StringUtils.split(sig, "/"); if (parts != null) { for (final String sigPart : parts) { if ("/".equals(sigPart) || sigPart.startsWith("_")) { continue; } // If one of the signature parts doesn't have an equivalent existing schema node, remove it foundAllParts &= existingSchemaNodeNames.contains(sigPart); } } if (!foundAllParts) { logger.info("Did not find all parts of signature, will be removed: {}, ", new Object[]{ sig }); removeDynamicGrants(sig); } } } catch (Throwable t) { logger.warn("", t); } } public static List<DynamicResourceAccess> createDynamicGrants(final String signature) { final List<DynamicResourceAccess> grants = new LinkedList<>(); final long initialFlagsValue = 0; final App app = StructrApp.getInstance(); try { ResourceAccess grant = app.nodeQuery(ResourceAccess.class).and(ResourceAccess.signature, signature).getFirst(); if (grant == null) { // create new grant grants.add(app.create(DynamicResourceAccess.class, new NodeAttribute(DynamicResourceAccess.signature, signature), new NodeAttribute(DynamicResourceAccess.flags, initialFlagsValue) )); logger.info("New signature created: {}", new Object[]{ (signature) }); } final String schemaSig = schemaResourceSignature(signature); ResourceAccess schemaGrant = app.nodeQuery(ResourceAccess.class).and(ResourceAccess.signature, schemaSig).getFirst(); if (schemaGrant == null) { // create additional grant for the _schema resource grants.add(app.create(DynamicResourceAccess.class, new NodeAttribute(DynamicResourceAccess.signature, schemaSig), new NodeAttribute(DynamicResourceAccess.flags, initialFlagsValue) )); logger.info("New signature created: {}", new Object[]{ schemaSig }); } final String uiSig = uiViewResourceSignature(signature); ResourceAccess uiViewGrant = app.nodeQuery(ResourceAccess.class).and(ResourceAccess.signature, uiSig).getFirst(); if (uiViewGrant == null) { // create additional grant for the Ui view grants.add(app.create(DynamicResourceAccess.class, new NodeAttribute(DynamicResourceAccess.signature, uiSig), new NodeAttribute(DynamicResourceAccess.flags, initialFlagsValue) )); logger.info("New signature created: {}", new Object[]{ uiSig }); } } catch (Throwable t) { logger.warn("", t); } return grants; } public static void removeAllDynamicGrants() { final App app = StructrApp.getInstance(); try { // delete grants for (DynamicResourceAccess grant : app.nodeQuery(DynamicResourceAccess.class).getAsList()) { app.delete(grant); } } catch (Throwable t) { logger.warn("", t); } } public static void removeDynamicGrants(final String signature) { final App app = StructrApp.getInstance(); try { // delete grant DynamicResourceAccess grant = app.nodeQuery(DynamicResourceAccess.class).and(DynamicResourceAccess.signature, signature).getFirst(); if (grant != null) { app.delete(grant); } // delete grant DynamicResourceAccess schemaGrant = app.nodeQuery(DynamicResourceAccess.class).and(DynamicResourceAccess.signature, "_schema/" + signature).getFirst(); if (schemaGrant != null) { app.delete(schemaGrant); } // delete grant DynamicResourceAccess viewGrant = app.nodeQuery(DynamicResourceAccess.class).and(DynamicResourceAccess.signature, signature + "/_Ui").getFirst(); if (viewGrant != null) { app.delete(viewGrant); } } catch (Throwable t) { logger.warn("", t); } } public static String extractProperties(final Schema entity, final Set<String> propertyNames, final Set<Validator> validators, final Set<String> enums, final Map<String, Set<String>> views, final ErrorBuffer errorBuffer) throws FrameworkException { final PropertyContainer propertyContainer = entity.getPropertyContainer(); final StringBuilder src = new StringBuilder(); // output property source code and collect views for (String propertyName : SchemaHelper.getProperties(propertyContainer)) { if (!propertyName.startsWith("__") && propertyContainer.hasProperty(propertyName)) { String rawType = propertyContainer.getProperty(propertyName).toString(); PropertySourceGenerator parser = SchemaHelper.getSourceGenerator(errorBuffer, entity.getClassName(), new StringBasedPropertyDefinition(propertyName, rawType)); if (parser != null) { // migrate properties if (entity instanceof AbstractSchemaNode) { parser.createSchemaPropertyNode((AbstractSchemaNode)entity, propertyName); } } } } final List<SchemaProperty> schemaProperties = entity.getSchemaProperties(); if (schemaProperties != null) { for (final SchemaProperty schemaProperty : schemaProperties) { if (!schemaProperty.getProperty(SchemaProperty.isBuiltinProperty)) { // migrate property source if (Type.Function.equals(schemaProperty.getPropertyType())) { // Note: This is a temporary migration from the old format to the new readFunction property final String format = schemaProperty.getFormat(); if (format != null) { try { schemaProperty.setProperty(SchemaProperty.readFunction, format); schemaProperty.setProperty(SchemaProperty.format, null); } catch (FrameworkException ex) { logger.warn("", ex); } } } final PropertySourceGenerator parser = SchemaHelper.getSourceGenerator(errorBuffer, entity.getClassName(), schemaProperty); if (parser != null) { final String propertyName = schemaProperty.getPropertyName(); // add property name to set for later use propertyNames.add(propertyName); // append created source from parser parser.getPropertySource(src, entity); // register global elements created by parser validators.addAll(parser.getGlobalValidators()); enums.addAll(parser.getEnumDefinitions()); // register property in default view addPropertyToView(PropertyView.Ui, propertyName, views); } } } } return src.toString(); } public static void extractViews(final Schema entity, final Map<String, Set<String>> views, final ErrorBuffer errorBuffer) throws FrameworkException { final PropertyContainer propertyContainer = entity.getPropertyContainer(); final ConfigurationProvider config = StructrApp.getConfiguration(); Class superClass = config.getNodeEntityClass(entity.getSuperclassName()); if (superClass == null) { superClass = config.getRelationshipEntityClass(entity.getSuperclassName()); } if (superClass == null) { superClass = AbstractNode.class; } for (final String rawViewName : getViews(propertyContainer)) { if (!rawViewName.startsWith("___") && propertyContainer.hasProperty(rawViewName)) { final String value = propertyContainer.getProperty(rawViewName).toString(); final String[] parts = value.split("[,\\s]+"); final String viewName = rawViewName.substring(2); if (entity instanceof AbstractSchemaNode) { final List<String> nonGraphProperties = new LinkedList<>(); final List<SchemaProperty> properties = new LinkedList<>(); final AbstractSchemaNode schemaNode = (AbstractSchemaNode)entity; final App app = StructrApp.getInstance(); if (app.nodeQuery(SchemaView.class).and(SchemaView.schemaNode, schemaNode).and(AbstractNode.name, viewName).getFirst() == null) { // add parts to view, overrides defaults (because of clear() above) for (int i = 0; i < parts.length; i++) { String propertyName = parts[i].trim(); while (propertyName.startsWith("_")) { propertyName = propertyName.substring(1); } // remove "Property" suffix in views where people needed to // append this as a workaround to include remote properties if (propertyName.endsWith("Property")) { propertyName = propertyName.substring(0, propertyName.length() - "Property".length()); } final SchemaProperty propertyNode = app.nodeQuery(SchemaProperty.class).and(SchemaProperty.schemaNode, schemaNode).andName(propertyName).getFirst(); if (propertyNode != null) { properties.add(propertyNode); } else { nonGraphProperties.add(propertyName); } } app.create(SchemaView.class, new NodeAttribute<>(SchemaView.schemaNode, schemaNode), new NodeAttribute<>(SchemaView.schemaProperties, properties), new NodeAttribute<>(SchemaView.name, viewName), new NodeAttribute<>(SchemaView.nonGraphProperties, StringUtils.join(nonGraphProperties, ",")) ); schemaNode.removeProperty(new StringProperty(rawViewName)); } } } } final List<SchemaView> schemaViews = entity.getSchemaViews(); if (schemaViews != null) { for (final SchemaView schemaView : schemaViews) { final String nonGraphProperties = schemaView.getProperty(SchemaView.nonGraphProperties); final String viewName = schemaView.getName(); // clear view before filling it again Set<String> view = views.get(viewName); if (view == null) { view = new LinkedHashSet<>(); views.put(viewName, view); } for (final SchemaProperty property : schemaView.getProperty(SchemaView.schemaProperties)) { if (property.getProperty(SchemaProperty.isBuiltinProperty) && !property.getProperty(SchemaProperty.isDynamic)) { view.add(property.getPropertyName()); } else { view.add(property.getPropertyName() + "Property"); } } // add properties that are not part of the graph if (StringUtils.isNotBlank(nonGraphProperties)) { for (final String propertyName : nonGraphProperties.split("[, ]+")) { String extendedPropertyName = propertyName; if (superClass != null) { final PropertyKey property = config.getPropertyKeyForJSONName(superClass, propertyName, false); if (property != null) { // property exists in superclass if (property.isDynamic()) { extendedPropertyName = extendedPropertyName + "Property"; } } else { extendedPropertyName = extendedPropertyName + "Property"; } } else { extendedPropertyName = extendedPropertyName + "Property"; } view.add(extendedPropertyName); } } } } } public static void extractMethods(final Schema entity, final Map<Actions.Type, List<ActionEntry>> actions) throws FrameworkException { final PropertyContainer propertyContainer = entity.getPropertyContainer(); for (final String rawActionName : getActions(propertyContainer)) { if (propertyContainer.hasProperty(rawActionName)) { final String value = propertyContainer.getProperty(rawActionName).toString(); if (entity instanceof AbstractSchemaNode) { final AbstractSchemaNode schemaNode = (AbstractSchemaNode)entity; final App app = StructrApp.getInstance(); final String methodName = rawActionName.substring(3); if (app.nodeQuery(SchemaMethod.class).and(SchemaMethod.schemaNode, schemaNode).and(AbstractNode.name, methodName).getFirst() == null) { app.create(SchemaMethod.class, new NodeAttribute<>(SchemaMethod.schemaNode, schemaNode), new NodeAttribute<>(SchemaMethod.name, methodName), new NodeAttribute<>(SchemaMethod.source, value) ); schemaNode.removeProperty(new StringProperty(rawActionName)); } } } } final List<SchemaMethod> schemaMethods = entity.getSchemaMethods(); if (schemaMethods != null) { for (final SchemaMethod schemaMethod : schemaMethods) { final ActionEntry entry = schemaMethod.getActionEntry(); List<ActionEntry> actionList = actions.get(entry.getType()); if (actionList == null) { actionList = new LinkedList<>(); actions.put(entry.getType(), actionList); } actionList.add(entry); Collections.sort(actionList); } } } public static void addPropertyToView(final String viewName, final String propertyName, final Map<String, Set<String>> views) { Set<String> view = views.get(viewName); if (view == null) { view = new LinkedHashSet<>(); views.put(viewName, view); } view.add(SchemaHelper.cleanPropertyName(propertyName) + "Property"); } public static Iterable<String> getProperties(final PropertyContainer propertyContainer) { final List<String> keys = new LinkedList<>(); for (final String key : propertyContainer.getPropertyKeys()) { if (propertyContainer.hasProperty(key) && key.startsWith("_")) { keys.add(key); } } return keys; } public static Iterable<String> getViews(final PropertyContainer propertyContainer) { final List<String> keys = new LinkedList<>(); for (final String key : propertyContainer.getPropertyKeys()) { if (propertyContainer.hasProperty(key) && key.startsWith("__")) { keys.add(key); } } return keys; } public static Iterable<String> getActions(final PropertyContainer propertyContainer) { final List<String> keys = new LinkedList<>(); for (final String key : propertyContainer.getPropertyKeys()) { if (propertyContainer.hasProperty(key) && key.startsWith("___")) { keys.add(key); } } return keys; } public static void formatView(final StringBuilder src, final String _className, final String viewName, final String view, final Set<String> viewProperties) { // output default view src.append("\n\tpublic static final View ").append(viewName).append("View = new View(").append(_className).append(".class, \"").append(view).append("\",\n"); src.append("\t\t"); for (final Iterator<String> it = viewProperties.iterator(); it.hasNext();) { String propertyName = it.next(); // convert _-prefixed property names to "real" name if (propertyName.startsWith("_")) { propertyName = propertyName.substring(1) + "Property"; } src.append(propertyName); if (it.hasNext()) { src.append(", "); } } src.append("\n\t);\n"); } public static void formatImportStatements(final AbstractSchemaNode schemaNode, final StringBuilder src, final Class baseType) { src.append("import ").append(baseType.getName()).append(";\n"); src.append("import ").append(ConfigurationProvider.class.getName()).append(";\n"); src.append("import ").append(GraphObjectComparator.class.getName()).append(";\n"); src.append("import ").append(PermissionPropagation.class.getName()).append(";\n"); src.append("import ").append(FrameworkException.class.getName()).append(";\n"); src.append("import ").append(DatePropertyParser.class.getName()).append(";\n"); src.append("import ").append(ModificationQueue.class.getName()).append(";\n"); src.append("import ").append(PropertyConverter.class.getName()).append(";\n"); src.append("import ").append(ValidationHelper.class.getName()).append(";\n"); src.append("import ").append(SecurityContext.class.getName()).append(";\n"); src.append("import ").append(LinkedHashSet.class.getName()).append(";\n"); src.append("import ").append(PropertyView.class.getName()).append(";\n"); src.append("import ").append(GraphObject.class.getName()).append(";\n"); src.append("import ").append(ErrorBuffer.class.getName()).append(";\n"); src.append("import ").append(StringUtils.class.getName()).append(";\n"); src.append("import ").append(Collections.class.getName()).append(";\n"); src.append("import ").append(StructrApp.class.getName()).append(";\n"); src.append("import ").append(LinkedList.class.getName()).append(";\n"); src.append("import ").append(Collection.class.getName()).append(";\n"); src.append("import ").append(Services.class.getName()).append(";\n"); src.append("import ").append(Actions.class.getName()).append(";\n"); src.append("import ").append(HashMap.class.getName()).append(";\n"); src.append("import ").append(Export.class.getName()).append(";\n"); src.append("import ").append(View.class.getName()).append(";\n"); src.append("import ").append(List.class.getName()).append(";\n"); src.append("import ").append(Map.class.getName()).append(";\n"); src.append("import ").append(Set.class.getName()).append(";\n"); if (hasRestClasses()) { src.append("import org.structr.rest.RestMethodResult;\n"); } src.append("import org.structr.core.property.*;\n"); if (hasUiClasses()) { src.append("import org.structr.web.property.*;\n"); } for (final StructrModule module : StructrApp.getConfiguration().getModules().values()) { module.insertImportStatements(schemaNode, src); } src.append("import org.structr.core.notion.*;\n"); src.append("import org.structr.core.entity.*;\n\n"); } public static void formatInterfacesFromModules(final StringBuilder src, final SchemaNode schemaNode) { final Set<String> interfaces = new LinkedHashSet<>(); boolean firstInterface = true; // collect all interfaces that a module wants to add to the given type for (final StructrModule module : StructrApp.getConfiguration().getModules().values()) { final Set<String> moduleInterfacesForType = module.getInterfacesForType(schemaNode); if (moduleInterfacesForType != null) { interfaces.addAll(moduleInterfacesForType); } } // add all interfaces dynamically for (final String iface : interfaces) { src.append(firstInterface ? " implements " : ", "); src.append(iface); firstInterface = false; } } public static String cleanPropertyName(final String propertyName) { return propertyName.replaceAll("[^\\w]+", ""); } public static void formatValidators(final StringBuilder src, final Set<Validator> validators) { if (!validators.isEmpty()) { src.append("\n\t@Override\n"); src.append("\tpublic boolean isValid(final ErrorBuffer errorBuffer) {\n\n"); src.append("\t\tboolean valid = super.isValid(errorBuffer);\n\n"); for (final Validator validator : validators) { src.append("\t\tvalid &= ").append(validator.getSource("this", true)).append(";\n"); } src.append("\n\t\treturn valid;\n"); src.append("\t}\n"); } } public static void formatDynamicValidators(final StringBuilder src, final Set<Validator> validators) { if (!validators.isEmpty()) { src.append("\tpublic static boolean isValid(final AbstractNode obj, final ErrorBuffer errorBuffer) {\n\n"); src.append("\t\tboolean valid = true;\n\n"); for (final Validator validator : validators) { src.append("\t\tvalid &= ").append(validator.getSource("obj", false)).append(";\n"); } src.append("\n\t\treturn valid;\n"); src.append("\t}\n\n"); } } public static void formatSaveActions(final AbstractSchemaNode schemaNode, final StringBuilder src, final Map<Actions.Type, List<ActionEntry>> saveActions) { // save actions.. for (final Map.Entry<Actions.Type, List<ActionEntry>> entry : saveActions.entrySet()) { final List<ActionEntry> actionList = entry.getValue(); final Actions.Type type = entry.getKey(); switch (type) { case Custom: // active actions are exported stored functions // that can be called by POSTing on the entity formatActiveActions(src, actionList); break; default: // passive actions are actions that are executed // automtatically on creation / modification etc. formatPassiveSaveActions(schemaNode, src, type, actionList); break; } } } public static void formatDynamicSaveActions(final StringBuilder src, final Map<Actions.Type, List<ActionEntry>> saveActions) { // save actions.. for (final Map.Entry<Actions.Type, List<ActionEntry>> entry : saveActions.entrySet()) { final List<ActionEntry> actionList = entry.getValue(); final Actions.Type type = entry.getKey(); if (!actionList.isEmpty()) { switch (type) { case Custom: // active actions are exported stored functions // that can be called by POSTing on the entity //formatDynamicActiveActions(src, actionList); break; default: // passive actions are actions that are executed // automtatically on creation / modification etc. formatDynamicPassiveSaveActions(src, type, actionList); break; } } } } public static void formatActiveActions(final StringBuilder src, final List<ActionEntry> actionList) { for (final ActionEntry action : actionList) { src.append("\n\t@Export\n"); src.append("\tpublic Object "); src.append(action.getName()); src.append("(final Map<String, Object> parameters) throws FrameworkException {\n\n"); src.append("\t\treturn "); src.append(action.getSource("this", true)); src.append(";\n\n"); src.append("\t}\n"); } } public static void formatDynamicActiveActions(final StringBuilder src, final List<ActionEntry> actionList) { for (final ActionEntry action : actionList) { src.append("\tpublic static Object "); src.append(action.getName()); src.append("(final AbstractNode obj) throws FrameworkException {\n\n"); src.append("\t\treturn "); src.append(action.getSource("obj")); src.append(";\n\n"); src.append("\t}\n"); } } public static void formatPassiveSaveActions(final AbstractSchemaNode schemaNode, final StringBuilder src, final Actions.Type type, final List<ActionEntry> actionList) { src.append("\n\t@Override\n"); src.append("\tpublic boolean "); src.append(type.getMethod()); src.append("("); src.append(type.getSignature()); src.append(") throws FrameworkException {\n\n"); src.append("\t\tboolean valid = super."); src.append(type.getMethod()); src.append("("); src.append(type.getParameters()); src.append(");\n\n"); for (final StructrModule module : StructrApp.getConfiguration().getModules().values()) { module.insertSaveAction(schemaNode, src, type); } for (final ActionEntry action : actionList) { src.append("\t\t").append(action.getSource("this")).append(";\n"); } src.append("\n\t\treturn valid;\n"); src.append("\t}\n"); } public static void formatDynamicPassiveSaveActions(final StringBuilder src, final Actions.Type type, final List<ActionEntry> actionList) { src.append("\tpublic static boolean "); src.append(type.getMethod()); src.append("(final AbstractNode obj, final SecurityContext securityContext, final ErrorBuffer errorBuffer) throws FrameworkException {\n\n"); src.append("\t\tboolean valid = obj.isValid(errorBuffer);\n\n"); for (final ActionEntry action : actionList) { src.append("\t\t").append(action.getSource("obj")).append(";\n"); } src.append("\n\t\treturn valid;\n"); src.append("\t}\n\n"); } private static Map<String, Object> getPropertiesForView(final SecurityContext securityContext, final Class type, final String propertyView) throws FrameworkException { final Set<PropertyKey> properties = new LinkedHashSet<>(StructrApp.getConfiguration().getPropertySet(type, propertyView)); final Map<String, Object> propertyConverterMap = new LinkedHashMap<>(); for (PropertyKey property : properties) { propertyConverterMap.put(property.jsonName(), getPropertyInfo(securityContext, property)); } return propertyConverterMap; } // ----- public static methods ----- public static List<GraphObjectMap> getSchemaTypeInfo(final SecurityContext securityContext, final String rawType, final Class type, final String propertyView) throws FrameworkException { List<GraphObjectMap> resultList = new LinkedList<>(); // create & add schema information if (type != null) { if (propertyView != null) { for (final Map.Entry<String, Object> entry : getPropertiesForView(securityContext, type, propertyView).entrySet()) { final GraphObjectMap property = new GraphObjectMap(); for (final Map.Entry<String, Object> prop : ((Map<String, Object>) entry.getValue()).entrySet()) { property.setProperty(new GenericProperty(prop.getKey()), prop.getValue()); } resultList.add(property); } } else { final GraphObjectMap schema = new GraphObjectMap(); resultList.add(schema); String url = "/".concat(rawType); schema.setProperty(new StringProperty("url"), url); schema.setProperty(new StringProperty("type"), type.getSimpleName()); schema.setProperty(new StringProperty("className"), type.getName()); schema.setProperty(new StringProperty("extendsClass"), type.getSuperclass().getName()); schema.setProperty(new BooleanProperty("isRel"), AbstractRelationship.class.isAssignableFrom(type)); schema.setProperty(new LongProperty("flags"), SecurityContext.getResourceFlags(rawType)); Set<String> propertyViews = new LinkedHashSet<>(StructrApp.getConfiguration().getPropertyViewsForType(type)); // list property sets for all views Map<String, Map<String, Object>> views = new TreeMap(); schema.setProperty(new GenericProperty("views"), views); for (final String view : propertyViews) { if (!View.INTERNAL_GRAPH_VIEW.equals(view)) { views.put(view, getPropertiesForView(securityContext, type, view)); } } } } return resultList; } public static Map<String, Object> getPropertyInfo(final SecurityContext securityContext, final PropertyKey property) { final Map<String, Object> map = new LinkedHashMap(); map.put("dbName", property.dbName()); map.put("jsonName", property.jsonName()); map.put("className", property.getClass().getName()); final Class declaringClass = property.getDeclaringClass(); map.put("declaringClass", declaringClass.getSimpleName()); map.put("defaultValue", property.defaultValue()); if (property instanceof StringProperty) { map.put("contentType", ((StringProperty) property).contentType()); } map.put("format", property.format()); map.put("readOnly", property.isReadOnly()); map.put("system", property.isSystemInternal()); map.put("indexed", property.isIndexed()); map.put("indexedWhenEmpty", property.isIndexedWhenEmpty()); map.put("unique", property.isUnique()); map.put("notNull", property.isNotNull()); map.put("dynamic", property.isDynamic()); final Class<? extends GraphObject> relatedType = property.relatedType(); if (relatedType != null) { map.put("relatedType", relatedType.getName()); map.put("type", relatedType.getSimpleName()); map.put("uiType", relatedType.getSimpleName() + (property.isCollection() ? "[]" : "")); } else { map.put("type", property.typeName()); map.put("uiType", property.typeName() + (property.isCollection() ? "[]" : "")); } map.put("isCollection", property.isCollection()); final PropertyConverter databaseConverter = property.databaseConverter(securityContext, null); final PropertyConverter inputConverter = property.inputConverter(securityContext); if (databaseConverter != null) { map.put("databaseConverter", databaseConverter.getClass().getName()); } if (inputConverter != null) { map.put("inputConverter", inputConverter.getClass().getName()); } //if (declaringClass != null && ("org.structr.dynamic".equals(declaringClass.getPackage().getName()))) { if (declaringClass != null && property instanceof RelationProperty) { Relation relation = ((RelationProperty) property).getRelation(); if (relation != null) { map.put("relationshipType", relation.name()); } } return map; } // ----- private methods ----- private static PropertySourceGenerator getSourceGenerator(final ErrorBuffer errorBuffer, final String className, final PropertyDefinition propertyDefinition) throws FrameworkException { final String propertyName = propertyDefinition.getPropertyName(); final Type propertyType = propertyDefinition.getPropertyType(); final Class<? extends PropertySourceGenerator> parserClass = parserMap.get(propertyType); try { return parserClass.getConstructor(ErrorBuffer.class, String.class, PropertyDefinition.class).newInstance(errorBuffer, className, propertyDefinition); } catch (Throwable t) { logger.warn("", t); } errorBuffer.add(new InvalidPropertySchemaToken(SchemaProperty.class.getSimpleName(), propertyName, "invalid_property_definition", "Unknow value type " + source + ", options are " + Arrays.asList(Type.values()) + ".")); throw new FrameworkException(422, "Invalid property definition for property " + propertyDefinition.getPropertyName(), errorBuffer); } private static boolean hasRestClasses() { try { Class.forName("org.structr.rest.RestMethodResult"); // success return true; } catch (Throwable t) { } return false; } private static boolean hasUiClasses() { try { Class.forName("org.structr.web.property.ThumbnailProperty"); // success return true; } catch (Throwable t) { } return false; } public static boolean hasPeerToPeerService() { try { Class.forName("org.structr.net.SharedNodeInterface"); // success return true; } catch (Throwable t) { } return false; } private static class ReverseTypeComparator implements Comparator<Type> { @Override public int compare(final Type o1, final Type o2) { return o2.name().compareTo(o1.name()); } } private static String schemaResourceSignature(final String signature) { return "_schema/" + signature; } private static String uiViewResourceSignature(final String signature) { return signature + "/_Ui"; } }