/**
* 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/>.
*/
/*
* To change this template, choose Tools | Templates
* and open the template in the editor.
*/
package org.structr.schema;
import java.util.HashMap;
import java.util.HashSet;
import java.util.LinkedHashMap;
import java.util.LinkedHashSet;
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.config.Settings;
import org.structr.api.service.Command;
import org.structr.api.service.InitializationCallback;
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.FrameworkException;
import org.structr.core.GraphObject;
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.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.NodeExtender;
/**
*
*
*/
public class SchemaService implements Service {
private static final Logger logger = LoggerFactory.getLogger(SchemaService.class.getName());
private static final AtomicBoolean compiling = new AtomicBoolean(false);
private static final AtomicBoolean updating = new AtomicBoolean(false);
private static final Map<String, String> builtinTypeMap = new LinkedHashMap<>();
@Override
public void injectArguments(final Command command) {
}
@Override
public void initialize(final StructrServices services) throws ClassNotFoundException, InstantiationException, IllegalAccessException {
services.registerInitializationCallback(new InitializationCallback() {
@Override
public void initializationDone() {
reloadSchema(new ErrorBuffer());
}
});
}
public static void registerBuiltinTypeOverride(final String type, final String fqcn) {
builtinTypeMap.put(type, fqcn);
}
public static boolean reloadSchema(final ErrorBuffer errorBuffer) {
final ConfigurationProvider config = StructrApp.getConfiguration();
boolean success = true;
// compiling must only be done once
if (compiling.compareAndSet(false, true)) {
try {
final Map<String, Map<String, PropertyKey>> removedClasses = new HashMap<>(StructrApp.getConfiguration().getTypeAndPropertyMapping());
final Set<String> dynamicViews = new LinkedHashSet<>();
final NodeExtender nodeExtender = new NodeExtender();
try (final Tx tx = StructrApp.getInstance().tx()) {
SchemaService.ensureBuiltinTypesExist();
// collect node classes
final List<SchemaNode> schemaNodes = StructrApp.getInstance().nodeQuery(SchemaNode.class).getAsList();
for (final SchemaNode schemaNode : schemaNodes) {
nodeExtender.addClass(schemaNode.getClassName(), schemaNode.getSource(errorBuffer));
final String auxSource = schemaNode.getAuxiliarySource();
if (auxSource != null) {
nodeExtender.addClass("_" + schemaNode.getClassName() + "Helper", auxSource);
}
dynamicViews.addAll(schemaNode.getViews());
}
// collect relationship classes
for (final SchemaRelationshipNode schemaRelationship : StructrApp.getInstance().nodeQuery(SchemaRelationshipNode.class).getAsList()) {
nodeExtender.addClass(schemaRelationship.getClassName(), schemaRelationship.getSource(errorBuffer));
final String auxSource = schemaRelationship.getAuxiliarySource();
if (auxSource != null) {
nodeExtender.addClass("_" + schemaRelationship.getClassName() + "Helper", auxSource);
}
dynamicViews.addAll(schemaRelationship.getViews());
}
// this is a very critical section :)
synchronized (SchemaService.class) {
// clear propagating relationship cache
SchemaRelationshipNode.clearPropagatingRelationshipTypes();
// compile all classes at once and register
Map<String, Class> newTypes = nodeExtender.compile(errorBuffer);
for (final Class newType : newTypes.values()) {
// do full reload
config.registerEntityType(newType);
// instantiate classes to execute
// static initializer of helpers
try { newType.newInstance(); } catch (Throwable t) {}
}
// calculate difference between previous and new classes
removedClasses.keySet().removeAll(StructrApp.getConfiguration().getTypeAndPropertyMapping().keySet());
}
// create properties and views etc.
for (final SchemaNode schemaNode : StructrApp.getInstance().nodeQuery(SchemaNode.class).getAsList()) {
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.clearPermissionResolutionCache();
// inject views in configuration provider
config.registerDynamicViews(dynamicViews);
tx.success();
}
} catch (Throwable t) {
logger.error("Unable to compile dynamic schema.", t);
success = false;
}
// disable hierarchy calculation and automatic index creation for testing runs
if (!Settings.Testing.getValue()) {
calculateHierarchy();
updateIndexConfiguration(removedClasses);
}
} finally {
// 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 ensureBuiltinTypesExist() throws FrameworkException {
final App app = StructrApp.getInstance();
for (final Entry<String, String> entry : builtinTypeMap.entrySet()) {
final String type = entry.getKey();
final String fqcn = entry.getValue();
SchemaNode schemaNode = app.nodeQuery(SchemaNode.class).andName(type).getFirst();
if (schemaNode == null) {
schemaNode = app.create(SchemaNode.class, type);
}
// creation can fail
if (schemaNode != null) {
schemaNode.setProperty(SchemaNode.extendsClass, fqcn);
schemaNode.unlockSystemPropertiesOnce();
schemaNode.setProperty(SchemaNode.isBuiltinType, true);
}
}
}
@Override
public boolean isVital() {
return true;
}
// ----- private methods -----
private static void calculateHierarchy() {
try (final Tx tx = StructrApp.getInstance().tx()) {
final List<SchemaNode> schemaNodes = StructrApp.getInstance().nodeQuery(SchemaNode.class).getAsList();
final Set<String> alreadyCalculated = new HashSet<>();
final Map<String, SchemaNode> map = new LinkedHashMap<>();
// populate lookup map
for (final SchemaNode schemaNode : schemaNodes) {
map.put(schemaNode.getName(), schemaNode);
}
// calc hierarchy
for (final SchemaNode schemaNode : schemaNodes) {
final int relCount = schemaNode.getProperty(SchemaNode.relatedFrom).size() + schemaNode.getProperty(SchemaNode.relatedTo).size();
final int level = recursiveGetHierarchyLevel(map, 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 Map<String, Object> params = new HashMap<>();
final App app = StructrApp.getInstance();
// create indices for properties of existing classes
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();
try (final Tx tx = app.tx()) {
for (final PropertyKey key : entry.getValue().values()) {
final String indexKey = "index." + typeName + "." + key.dbName();
final String value = app.getGlobalSetting(indexKey, null);
final boolean alreadySet = "true".equals(value);
boolean createIndex = key.isIndexed() || key.isIndexedWhenEmpty();
createIndex &= !NonIndexed.class.isAssignableFrom(type);
createIndex &= NodeInterface.class.equals(type) || !GraphObject.id.equals(key);
if (createIndex) {
if (!alreadySet) {
try {
// create index
app.cypher("CREATE INDEX ON :" + typeName + "(" + key.dbName() + ")", params);
} catch (Throwable t) {
logger.warn("", t);
}
// store the information that we already created this index
app.setGlobalSetting(indexKey, "true");
}
} else if (alreadySet) {
try {
// drop index
app.cypher("DROP INDEX ON :" + typeName + "(" + key.dbName() + ")", params);
} catch (Throwable t) {
logger.warn("", t);
}
// remove entry from config file
app.setGlobalSetting(indexKey, null);
}
}
tx.success();
} catch (Throwable ignore) {
logger.warn("", ignore);
}
}
}
// drop indices for all indexed properties of removed classes
for (final Entry<String, Map<String, PropertyKey>> entry : removedClasses.entrySet()) {
final String typeName = StringUtils.substringAfterLast(entry.getKey(), ".");
for (final PropertyKey key : entry.getValue().values()) {
try {
final String indexKey = "index." + typeName + "." + key.dbName();
final String value = app.getGlobalSetting(indexKey, null);
final boolean exists = "true".equals(value);
boolean dropIndex = key.isIndexed() || key.isIndexedWhenEmpty();
dropIndex &= !GraphObject.id.equals(key);
if (dropIndex && exists) {
try (final Tx tx = app.tx()) {
// drop index
app.cypher("DROP INDEX ON :" + typeName + "(" + key.dbName() + ")", params);
tx.success();
} catch (Throwable t) {
logger.warn("", t);
}
// remove entry from config file
app.setGlobalSetting(indexKey, null);
}
} catch (FrameworkException ignore) {}
}
}
} finally {
updating.set(false);
}
}
}
});
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, "."));
}
}