org.structr.schema.SchemaService.java Source code

Java tutorial

Introduction

Here is the source code for org.structr.schema.SchemaService.java

Source

/**
 * Copyright (C) 2010-2018 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/>.
 */
/*
 * To change this template, choose Tools | Templates
 * and open the template in the editor.
 */

package org.structr.schema;

import graphql.Scalars;
import graphql.schema.GraphQLArgument;
import graphql.schema.GraphQLFieldDefinition;
import graphql.schema.GraphQLInputObjectType;
import graphql.schema.GraphQLList;
import graphql.schema.GraphQLObjectType;
import graphql.schema.GraphQLSchema;
import graphql.schema.GraphQLType;
import java.lang.reflect.Modifier;
import java.net.URI;
import java.net.URISyntaxException;
import java.util.HashMap;
import java.util.HashSet;
import java.util.LinkedHashMap;
import java.util.LinkedHashSet;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;
import java.util.Set;
import java.util.concurrent.atomic.AtomicBoolean;
import org.apache.commons.lang3.StringUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.structr.api.DatabaseService;
import org.structr.api.config.Settings;
import org.structr.api.service.Command;
import org.structr.api.service.Service;
import org.structr.api.service.StructrServices;
import org.structr.common.AccessPathCache;
import org.structr.common.error.ErrorBuffer;
import org.structr.common.error.ErrorToken;
import org.structr.common.error.FrameworkException;
import org.structr.common.error.InstantiationErrorToken;
import org.structr.core.GraphObject;
import org.structr.core.Services;
import org.structr.core.app.App;
import org.structr.core.app.StructrApp;
import org.structr.core.entity.AbstractNode;
import org.structr.core.entity.SchemaNode;
import org.structr.core.entity.SchemaRelationshipNode;
import org.structr.core.graph.FlushCachesCommand;
import org.structr.core.graph.NodeInterface;
import org.structr.core.graph.Tx;
import org.structr.core.graph.search.SearchCommand;
import org.structr.core.property.PropertyKey;
import org.structr.schema.compiler.BlacklistSchemaNodeWhenMissingPackage;
import org.structr.schema.compiler.BlacklistUnlicensedTypes;
import org.structr.schema.compiler.ExtendNotionPropertyWithUuid;
import org.structr.schema.compiler.MigrationHandler;
import org.structr.schema.compiler.NodeExtender;
import org.structr.schema.compiler.RemoveMethodsWithUnusedSignature;
import org.structr.schema.export.StructrSchema;
import org.structr.schema.json.JsonSchema;

/**
 *
 *
 */
public class SchemaService implements Service {

    public static final URI DynamicSchemaRootURI = URI.create("https://structr.org/v2.0/#");
    private static final Logger logger = LoggerFactory.getLogger(SchemaService.class.getName());
    private static final List<MigrationHandler> migrationHandlers = new LinkedList<>();
    private static final JsonSchema dynamicSchema = StructrSchema.newInstance(DynamicSchemaRootURI);
    private static final AtomicBoolean compiling = new AtomicBoolean(false);
    private static final AtomicBoolean updating = new AtomicBoolean(false);
    private static final Set<String> blacklist = new LinkedHashSet<>();
    private static GraphQLSchema graphQLSchema = null;

    static {

        migrationHandlers.add(new BlacklistSchemaNodeWhenMissingPackage());
        migrationHandlers.add(new RemoveMethodsWithUnusedSignature());
        migrationHandlers.add(new ExtendNotionPropertyWithUuid());
        migrationHandlers.add(new BlacklistUnlicensedTypes());
    }

    @Override
    public void injectArguments(final Command command) {
    }

    @Override
    public boolean initialize(final StructrServices services)
            throws ClassNotFoundException, InstantiationException, IllegalAccessException {
        return reloadSchema(new ErrorBuffer(), null);
    }

    public static JsonSchema getDynamicSchema() {
        return dynamicSchema;
    }

    public static synchronized GraphQLSchema getGraphQLSchema() {
        return graphQLSchema;
    }

    public static boolean reloadSchema(final ErrorBuffer errorBuffer, final String initiatedBySessionId) {

        final ConfigurationProvider config = StructrApp.getConfiguration();
        final App app = StructrApp.getInstance();
        boolean success = true;

        // compiling must only be done once
        if (compiling.compareAndSet(false, true)) {

            FlushCachesCommand.flushAll();

            final long t0 = System.currentTimeMillis();

            try {

                final Map<String, Map<String, PropertyKey>> removedClasses = new HashMap<>(
                        config.getTypeAndPropertyMapping());
                final Map<String, GraphQLType> graphQLTypes = new LinkedHashMap<>();
                final Map<String, SchemaNode> schemaNodes = new LinkedHashMap<>();
                final NodeExtender nodeExtender = new NodeExtender(initiatedBySessionId);
                final Set<String> dynamicViews = new LinkedHashSet<>();

                try (final Tx tx = app.tx()) {

                    // collect auto-generated schema nodes
                    SchemaService.ensureBuiltinTypesExist(app);

                    // collect list of schema nodes
                    app.nodeQuery(SchemaNode.class).getAsList().stream().forEach(n -> {
                        schemaNodes.put(n.getName(), n);
                    });

                    // check licenses prior to source code generation
                    for (final SchemaNode schemaInfo : schemaNodes.values()) {
                        blacklist.addAll(SchemaHelper.getUnlicensedTypes(schemaInfo));
                    }

                    // add schema nodes from database
                    for (final SchemaNode schemaInfo : schemaNodes.values()) {

                        final String name = schemaInfo.getName();
                        if (blacklist.contains(name)) {

                            continue;
                        }

                        schemaInfo.handleMigration();

                        final String sourceCode = SchemaHelper.getSource(schemaInfo, schemaNodes, blacklist,
                                errorBuffer);
                        if (sourceCode != null) {

                            final String className = schemaInfo.getClassName();

                            // only load dynamic node if there were no errors while generating
                            // the source code (missing modules etc.)
                            nodeExtender.addClass(className, sourceCode);
                            dynamicViews.addAll(schemaInfo.getDynamicViews());

                            // initialize GraphQL engine as well
                            schemaInfo.initializeGraphQL(schemaNodes, graphQLTypes, blacklist);
                        }
                    }

                    // collect relationship classes
                    for (final SchemaRelationshipNode schemaRelationship : app
                            .nodeQuery(SchemaRelationshipNode.class).getAsList()) {

                        final String sourceType = schemaRelationship.getSchemaNodeSourceType();
                        final String targetType = schemaRelationship.getSchemaNodeTargetType();

                        if (!blacklist.contains(sourceType) && !blacklist.contains(targetType)) {

                            nodeExtender.addClass(schemaRelationship.getClassName(),
                                    schemaRelationship.getSource(schemaNodes, errorBuffer));
                            dynamicViews.addAll(schemaRelationship.getDynamicViews());

                            // initialize GraphQL engine as well
                            schemaRelationship.initializeGraphQL(graphQLTypes);
                        }
                    }

                    // this is a very critical section :)
                    synchronized (SchemaService.class) {

                        // clear propagating relationship cache
                        SchemaRelationshipNode.clearPropagatingRelationshipTypes();

                        // compile all classes at once and register
                        final Map<String, Class> newTypes = nodeExtender.compile(errorBuffer);

                        for (final Class newType : newTypes.values()) {

                            // instantiate classes to execute static initializer of helpers
                            try {

                                // do full reload
                                config.registerEntityType(newType);
                                newType.newInstance();

                            } catch (final Throwable t) {

                                // abstract classes and interfaces will throw errors here
                                if (newType.isInterface() || Modifier.isAbstract(newType.getModifiers())) {
                                    // ignore
                                } else {

                                    // everything else is a severe problem and should be not only reported but also
                                    // make the schema compilation fail (otherwise bad things will happen later)
                                    errorBuffer.add(new InstantiationErrorToken(newType.getName(), t));
                                    logger.error("Unable to instantiate dynamic entity {}", newType.getName(), t);
                                }
                            }
                        }

                        // calculate difference between previous and new classes
                        removedClasses.keySet()
                                .removeAll(StructrApp.getConfiguration().getTypeAndPropertyMapping().keySet());
                    }

                    // create properties and views etc.
                    for (final SchemaNode schemaNode : schemaNodes.values()) {
                        schemaNode.createBuiltInSchemaEntities(errorBuffer);
                    }

                    success = !errorBuffer.hasError();

                    if (success) {

                        // prevent inheritance map from leaking
                        SearchCommand.clearInheritanceMap();
                        AccessPathCache.invalidate();

                        // clear relationship instance cache
                        AbstractNode.clearRelationshipTemplateInstanceCache();

                        // clear permission cache
                        AbstractNode.clearCaches();

                        // inject views in configuration provider
                        config.registerDynamicViews(dynamicViews);

                        if (Services.calculateHierarchy() || !Services.isTesting()) {

                            calculateHierarchy(schemaNodes);
                        }

                        if (Services.updateIndexConfiguration() || !Services.isTesting()) {

                            updateIndexConfiguration(removedClasses);
                        }

                        tx.success();

                        final GraphQLObjectType.Builder queryTypeBuilder = GraphQLObjectType.newObject();
                        final Map<String, GraphQLInputObjectType> selectionTypes = new LinkedHashMap<>();
                        final Set<String> existingQueryTypeNames = new LinkedHashSet<>();

                        // register types in "Query" type
                        for (final Entry<String, GraphQLType> entry : graphQLTypes.entrySet()) {

                            final String className = entry.getKey();
                            final GraphQLType type = entry.getValue();

                            try {

                                // register type in query type
                                queryTypeBuilder.field(GraphQLFieldDefinition.newFieldDefinition().name(className)
                                        .type(new GraphQLList(type))
                                        .argument(GraphQLArgument.newArgument().name("id")
                                                .type(Scalars.GraphQLString).build())
                                        .argument(GraphQLArgument.newArgument().name("type")
                                                .type(Scalars.GraphQLString).build())
                                        .argument(GraphQLArgument.newArgument().name("name")
                                                .type(Scalars.GraphQLString).build())
                                        .argument(GraphQLArgument.newArgument().name("_page")
                                                .type(Scalars.GraphQLInt).build())
                                        .argument(GraphQLArgument.newArgument().name("_pageSize")
                                                .type(Scalars.GraphQLInt).build())
                                        .argument(GraphQLArgument.newArgument().name("_sort")
                                                .type(Scalars.GraphQLString).build())
                                        .argument(GraphQLArgument.newArgument().name("_desc")
                                                .type(Scalars.GraphQLBoolean).build())
                                        .argument(SchemaHelper.getGraphQLQueryArgumentsForType(schemaNodes,
                                                selectionTypes, existingQueryTypeNames, className)));

                            } catch (Throwable t) {
                                logger.warn("Unable to add GraphQL type {}: {}", className, t.getMessage());
                            }
                        }

                        // exchange graphQL schema after successful build
                        synchronized (SchemaService.class) {

                            try {

                                graphQLSchema = GraphQLSchema.newSchema()
                                        .query(queryTypeBuilder.name("Query").build())
                                        .build(new LinkedHashSet<>(graphQLTypes.values()));

                            } catch (Throwable t) {
                                logger.warn("Unable to build GraphQL schema: {}", t.getMessage());
                            }
                        }
                    }

                } catch (FrameworkException fex) {

                    FlushCachesCommand.flushAll();

                    logger.error("Unable to compile dynamic schema: {}", fex.getMessage());
                    success = false;

                    errorBuffer.getErrorTokens().addAll(fex.getErrorBuffer().getErrorTokens());

                } catch (Throwable t) {

                    FlushCachesCommand.flushAll();

                    t.printStackTrace();

                    logger.error("Unable to compile dynamic schema: {}", t.getMessage());
                    success = false;
                }

                if (!success) {

                    FlushCachesCommand.flushAll();

                    logger.error("Errors encountered during compilation:");
                    for (ErrorToken token : errorBuffer.getErrorTokens()) {
                        logger.error(" - {}", token.toString());
                    }

                    if (Settings.SchemAutoMigration.getValue()) {

                        logger.info("Attempting auto-migration...");

                        // handle migration in separate transaction
                        try (final Tx tx = app.tx()) {

                            // try to handle certain errors automatically
                            handleAutomaticMigration(errorBuffer);

                            tx.success();

                        } catch (FrameworkException fex) {

                        }

                    } else {

                        logger.error(
                                "Unable to compile dynamic schema, and automatic migration is not enabled. Please set application.schema.automigration = true in structr.conf to enable modification of existing schema classes.");

                    }
                }

            } finally {

                logger.info("Schema build took a total of {} ms", System.currentTimeMillis() - t0);

                // compiling done
                compiling.set(false);

            }
        }

        return success;
    }

    @Override
    public void initialized() {
    }

    @Override
    public void shutdown() {
    }

    @Override
    public String getName() {
        return SchemaService.class.getName();
    }

    @Override
    public boolean isRunning() {
        return true;
    }

    public static void blacklist(final String typeName) {
        SchemaService.blacklist.add(typeName);
    }

    public static Set<String> getBlacklist() {
        return SchemaService.blacklist;
    }

    public static void ensureBuiltinTypesExist(final App app) throws FrameworkException {

        try {

            StructrSchema.extendDatabaseSchema(app, dynamicSchema);

        } catch (Exception ex) {

            ex.printStackTrace();
        }
    }

    @Override
    public boolean isVital() {
        return true;
    }

    @Override
    public boolean waitAndRetry() {
        return true;
    }

    @Override
    public int getRetryCount() {
        return 3;
    }

    @Override
    public int getRetryDelay() {
        return 1;
    }

    // ----- interface Feature -----
    @Override
    public String getModuleName() {
        return "core";
    }

    // ----- private methods -----
    private static void calculateHierarchy(final Map<String, SchemaNode> schemaNodes) {

        try (final Tx tx = StructrApp.getInstance().tx()) {

            final Set<String> alreadyCalculated = new HashSet<>();

            // calc hierarchy
            for (final SchemaNode schemaNode : schemaNodes.values()) {

                final int relCount = schemaNode.getProperty(SchemaNode.relatedFrom).size()
                        + schemaNode.getProperty(SchemaNode.relatedTo).size();
                final int level = recursiveGetHierarchyLevel(schemaNodes, alreadyCalculated, schemaNode, 0);

                schemaNode.setProperty(SchemaNode.hierarchyLevel, level);
                schemaNode.setProperty(SchemaNode.relCount, relCount);
            }

            tx.success();

        } catch (FrameworkException fex) {
            logger.warn("", fex);
        }
    }

    private static int recursiveGetHierarchyLevel(final Map<String, SchemaNode> map,
            final Set<String> alreadyCalculated, final SchemaNode schemaNode, final int depth) {

        // stop at level 20
        if (depth > 20) {
            return 20;
        }

        String superclass = schemaNode.getProperty(SchemaNode.extendsClass);
        if (superclass == null) {

            return 0;

        } else if (superclass.startsWith("org.structr.dynamic.")) {

            // find hierarchy level
            superclass = superclass.substring(superclass.lastIndexOf(".") + 1);

            // recurse upwards
            final SchemaNode superSchemaNode = map.get(superclass);
            if (superSchemaNode != null) {

                return recursiveGetHierarchyLevel(map, alreadyCalculated, superSchemaNode, depth + 1) + 1;
            }
        }

        return 0;
    }

    private static void updateIndexConfiguration(final Map<String, Map<String, PropertyKey>> removedClasses) {

        final Thread indexUpdater = new Thread(new Runnable() {

            @Override
            public void run() {

                // critical section, only one thread should update the index at a time
                if (updating.compareAndSet(false, true)) {

                    try {

                        final DatabaseService graphDb = StructrApp.getInstance().getDatabaseService();

                        final Map<String, Map<String, Boolean>> schemaIndexConfig = new HashMap();
                        final Map<String, Map<String, Boolean>> removedClassesConfig = new HashMap();

                        for (final Entry<String, Map<String, PropertyKey>> entry : StructrApp.getConfiguration()
                                .getTypeAndPropertyMapping().entrySet()) {

                            final Class type = getType(entry.getKey());
                            if (type != null) {

                                final String typeName = type.getSimpleName();

                                final Boolean alreadySeenType = schemaIndexConfig.containsKey(typeName);
                                final Map<String, Boolean> typeConfig = (alreadySeenType
                                        ? schemaIndexConfig.get(typeName)
                                        : new HashMap());

                                if (!alreadySeenType) {
                                    schemaIndexConfig.put(typeName, typeConfig);
                                }

                                for (final PropertyKey key : entry.getValue().values()) {

                                    boolean createIndex = key.isIndexed() || key.isIndexedWhenEmpty();

                                    createIndex &= !NonIndexed.class.isAssignableFrom(type);
                                    createIndex &= NodeInterface.class.equals(type) || !GraphObject.id.equals(key);

                                    typeConfig.put(key.dbName(), createIndex);
                                }
                            }
                        }

                        for (final Entry<String, Map<String, PropertyKey>> entry : removedClasses.entrySet()) {

                            final String typeName = StringUtils.substringAfterLast(entry.getKey(), ".");

                            final Map<String, Boolean> typeConfig = new HashMap();
                            removedClassesConfig.put(typeName, typeConfig);

                            for (final PropertyKey key : entry.getValue().values()) {

                                final boolean wasIndexed = key.isIndexed() || key.isIndexedWhenEmpty();
                                final boolean wasIdIndex = GraphObject.id.equals(key);
                                final boolean dropIndex = wasIndexed && !wasIdIndex;

                                typeConfig.put(key.dbName(), dropIndex);
                            }
                        }

                        graphDb.updateIndexConfiguration(schemaIndexConfig, removedClassesConfig);

                    } finally {

                        updating.set(false);
                    }
                }
            }
        });

        indexUpdater.setName("indexUpdater");
        indexUpdater.setDaemon(true);
        indexUpdater.start();
    }

    private static Class getType(final String name) {

        try {
            return Class.forName(name);
        } catch (ClassNotFoundException ignore) {
        }

        // fallback: use dynamic class from simple name
        return StructrApp.getConfiguration().getNodeEntityClass(StringUtils.substringAfterLast(name, "."));
    }

    private static void handleAutomaticMigration(final ErrorBuffer errorBuffer) throws FrameworkException {

        for (final ErrorToken errorToken : errorBuffer.getErrorTokens()) {

            for (final MigrationHandler handler : migrationHandlers) {

                handler.handleMigration(errorToken);
            }
        }

    }
}