/**
* 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.module;
import java.io.File;
import java.io.IOException;
import java.lang.reflect.Field;
import java.lang.reflect.Method;
import java.lang.reflect.Modifier;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Enumeration;
import java.util.HashMap;
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.TreeMap;
import java.util.TreeSet;
import java.util.concurrent.ConcurrentHashMap;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import java.util.zip.ZipEntry;
import java.util.zip.ZipFile;
import org.apache.commons.lang3.StringUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.structr.agent.Agent;
import org.structr.api.service.Service;
import org.structr.common.DefaultFactoryDefinition;
import org.structr.common.FactoryDefinition;
import org.structr.common.PropertyView;
import org.structr.common.SecurityContext;
import org.structr.common.View;
import org.structr.core.*;
import org.structr.core.entity.AbstractNode;
import org.structr.core.entity.AbstractRelationship;
import org.structr.core.entity.GenericNode;
import org.structr.core.entity.Relation;
import org.structr.core.graph.NodeInterface;
import org.structr.core.graph.RelationshipInterface;
import org.structr.core.property.GenericProperty;
import org.structr.core.property.PropertyKey;
import org.structr.schema.ConfigurationProvider;
import org.structr.schema.SchemaService;
//~--- classes ----------------------------------------------------------------
/**
* The module service main class.
*
*
*/
public class JarConfigurationProvider implements ConfigurationProvider {
private static final Logger logger = LoggerFactory.getLogger(JarConfigurationProvider.class.getName());
public static final String DYNAMIC_TYPES_PACKAGE = "org.structr.dynamic";
private final Map<String, Class<? extends RelationshipInterface>> relationshipEntityClassCache = new ConcurrentHashMap<>(1000);
private final Map<String, Class<? extends NodeInterface>> nodeEntityClassCache = new ConcurrentHashMap(1000);
private final Map<String, Class<? extends Agent>> agentClassCache = new ConcurrentHashMap<>(100);
private final Set<String> agentPackages = new LinkedHashSet<>();
private final Set<String> nodeEntityPackages = new LinkedHashSet<>();
private final Set<String> relationshipPackages = new LinkedHashSet<>();
private final Map<String, Class> combinedTypeRelationClassCache = new ConcurrentHashMap<>(100);
private final Map<String, Set<Class>> interfaceCache = new ConcurrentHashMap<>(2000);
private final Map<String, StructrModule> modules = new ConcurrentHashMap<>(100);
private final String fileSep = System.getProperty("file.separator");
private final String pathSep = System.getProperty("path.separator");
private final String fileSepEscaped = fileSep.replaceAll("\\\\", "\\\\\\\\");
private final String testClassesDir = fileSep.concat("test-classes");
private final String classesDir = fileSep.concat("classes");
private final Map<String, Map<String, Set<PropertyKey>>> globalPropertyViewMap = new ConcurrentHashMap<>(2000);
private final Map<String, Map<PropertyKey, Set<PropertyValidator>>> globalValidatorMap = new ConcurrentHashMap<>(100);
private final Map<String, Map<String, PropertyKey>> globalClassDBNamePropertyMap = new ConcurrentHashMap<>(2000);
private final Map<String, Map<String, PropertyKey>> globalClassJSNamePropertyMap = new ConcurrentHashMap<>(2000);
private final Map<String, Map<String, PropertyGroup>> globalAggregatedPropertyGroupMap = new ConcurrentHashMap<>(100);
private final Map<String, Map<String, PropertyGroup>> globalPropertyGroupMap = new ConcurrentHashMap<>(100);
private final Map<String, Map<String, ViewTransformation>> viewTransformations = new ConcurrentHashMap<>(100);
private final Map<String, Set<Transformation<GraphObject>>> globalTransformationMap = new ConcurrentHashMap<>(100);
private final Map<String, Map<String, Method>> exportedMethodMap = new ConcurrentHashMap<>(100);
private final Map<Class, Set<Class>> interfaceMap = new ConcurrentHashMap<>(2000);
private final Map<String, Class> reverseInterfaceMap = new ConcurrentHashMap<>(5000);
private final Set<PropertyKey> globalKnownPropertyKeys = new LinkedHashSet<>();
private final Set<String> dynamicViews = new LinkedHashSet<>();
private FactoryDefinition factoryDefinition = new DefaultFactoryDefinition();
// ----- interface Configuration -----
@Override
public void initialize() {
scanResources();
}
@Override
public void shutdown() {
}
@Override
public Map<String, Class<? extends Agent>> getAgents() {
return agentClassCache;
}
@Override
public Map<String, Class<? extends NodeInterface>> getNodeEntities() {
synchronized (SchemaService.class) {
return nodeEntityClassCache;
}
}
@Override
public Map<String, Class<? extends RelationshipInterface>> getRelationshipEntities() {
synchronized (SchemaService.class) {
return relationshipEntityClassCache;
}
}
@Override
public Set<Class> getClassesForInterface(final String simpleName) {
synchronized (SchemaService.class) {
return interfaceCache.get(simpleName);
}
}
@Override
public Class getNodeEntityClass(final String simpleName) {
Class nodeEntityClass = GenericNode.class;
if ((simpleName != null) && (!simpleName.isEmpty())) {
synchronized (SchemaService.class) {
nodeEntityClass = nodeEntityClassCache.get(simpleName);
if (nodeEntityClass == null) {
for (String possiblePath : nodeEntityPackages) {
if (possiblePath != null) {
try {
Class nodeClass = Class.forName(possiblePath + "." + simpleName);
if (!Modifier.isAbstract(nodeClass.getModifiers())) {
nodeEntityClassCache.put(simpleName, nodeClass);
nodeEntityClass = nodeClass;
// first match wins
break;
}
} catch (ClassNotFoundException ex) {
// ignore
}
}
}
}
}
}
return nodeEntityClass;
}
@Override
public Class getRelationshipEntityClass(final String name) {
Class relationClass = AbstractRelationship.class;
if ((name != null) && (name.length() > 0)) {
synchronized (SchemaService.class) {
relationClass = relationshipEntityClassCache.get(name);
if (relationClass == null) {
for (String possiblePath : relationshipPackages) {
if (possiblePath != null) {
try {
Class nodeClass = Class.forName(possiblePath + "." + name);
if (!Modifier.isAbstract(nodeClass.getModifiers())) {
relationshipEntityClassCache.put(name, nodeClass);
// first match wins
return nodeClass;
}
} catch (ClassNotFoundException ex) {
// ignore
}
}
}
}
}
}
return relationClass;
}
public Class<? extends Agent> getAgentClass(final String name) {
Class agentClass = null;
if ((name != null) && (name.length() > 0)) {
agentClass = agentClassCache.get(name);
if (agentClass == null) {
for (String possiblePath : agentPackages) {
if (possiblePath != null) {
try {
Class nodeClass = Class.forName(possiblePath + "." + name);
agentClassCache.put(name, nodeClass);
// first match wins
return nodeClass;
} catch (ClassNotFoundException ex) {
// ignore
}
}
}
}
}
return agentClass;
}
@Override
public Map<String, Class> getInterfaces() {
return reverseInterfaceMap;
}
@Override
public void setRelationClassForCombinedType(final String combinedType, final Class clazz) {
combinedTypeRelationClassCache.put(combinedType, clazz);
}
@Override
public void setRelationClassForCombinedType(final String sourceType, final String relType, final String targetType, final Class clazz) {
combinedTypeRelationClassCache.put(getCombinedType(sourceType, relType, targetType), clazz);
}
private Class getRelationClassForCombinedType(final String combinedType) {
Class cachedRelationClass = combinedTypeRelationClassCache.get(combinedType);
if (cachedRelationClass != null) {
return cachedRelationClass;
}
return null;
}
@Override
public Class getRelationClassForCombinedType(final String sourceTypeName, final String relType, final String targetTypeName) {
if (sourceTypeName == null || relType == null || targetTypeName == null) {
return null;
}
String combinedType
= sourceTypeName
.concat(DefaultFactoryDefinition.COMBINED_RELATIONSHIP_KEY_SEP)
.concat(relType)
.concat(DefaultFactoryDefinition.COMBINED_RELATIONSHIP_KEY_SEP)
.concat(targetTypeName);
Class cachedRelationClass = getRelationClassForCombinedType(combinedType);
if (cachedRelationClass != null) {
return cachedRelationClass;
}
return findNearestMatchingRelationClass(sourceTypeName, relType, targetTypeName);
}
/**
* Return a list of all relation entity classes filtered by relationship
* type.
*
* @param relType
* @return classes
*/
private List<Class<? extends RelationshipInterface>> getRelationClassCandidatesForRelType(final String relType) {
List<Class<? extends RelationshipInterface>> candidates = new ArrayList();
for (final Class<? extends RelationshipInterface> candidate : getRelationshipEntities().values()) {
Relation rel = instantiate(candidate);
if (rel == null) {
continue;
}
if (rel.name().equals(relType)) {
candidates.add(candidate);
}
}
return candidates;
}
/**
* Find the most specialized relation class matching the given
* parameters.
*
* If no direct match is found (source and target type are equal), we
* count the levels of inheritance, including interfaces.
*
* @param sourceTypeName
* @param relType
* @param targetTypeName
* @param rel
* @param candidate
* @return class
*/
private Class findNearestMatchingRelationClass(final String sourceTypeName, final String relType, final String targetTypeName) {
final Class sourceType = getNodeEntityClass(sourceTypeName);
final Class targetType = getNodeEntityClass(targetTypeName);
final Map<Integer, Class> candidates = new TreeMap<>();
for (final Class candidate : getRelationClassCandidatesForRelType(relType)) {
final Relation rel = instantiate(candidate);
final int distance = getDistance(rel.getSourceType(), sourceType, -1) + getDistance(rel.getTargetType(), targetType, -1);
if (distance >= 2000) {
candidates.put(distance - 2000, candidate);
}
}
if (candidates.isEmpty()) {
return null;
} else {
final Entry<Integer, Class> candidateEntry = candidates.entrySet().iterator().next();
final Class c = candidateEntry.getValue();
combinedTypeRelationClassCache.put(getCombinedType(sourceTypeName, relType, targetTypeName), c);
return c;
}
}
private int getDistance(final Class candidateType, final Class type, int distance) {
if (distance >= 1000) {
return distance;
}
distance++;
// Just in case...
if (type == null) {
return Integer.MIN_VALUE;
}
// Abort if type is Object.class here
if (type.equals(Object.class)) {
return Integer.MIN_VALUE;
}
// Check direct equality
if (type.equals(candidateType)) {
return distance + 1000;
}
// Abort here if type is NodeInterface.
if (type.equals(NodeInterface.class)) {
return Integer.MIN_VALUE;
}
// Relation candidate's source and target types must be superclasses or interfaces of the given relationship
if (!(candidateType.isAssignableFrom(type))) {
return Integer.MIN_VALUE;
}
distance++;
// Test source's interfaces against target class
Class[] interfaces = type.getInterfaces();
for (final Class iface : interfaces) {
if (iface.equals(candidateType)) {
return distance + 1000;
}
}
distance++;
final Class superClass = type.getSuperclass();
if (superClass != null) {
final int d = getDistance(candidateType, superClass, distance);
if (d >= 1000) {
return d;
}
}
return distance;
}
@Override
public Map<String, Method> getAnnotatedMethods(Class entityType, Class annotationType) {
final Map<String, Method> methods = new HashMap<>();
final Set<Class<?>> allTypes = getAllTypes(entityType);
for (final Class<?> type : allTypes) {
for (Method method : type.getDeclaredMethods()) {
if (method.getAnnotation(annotationType) != null) {
methods.put(method.getName(), method);
}
}
}
return methods;
}
@Override
public void unregisterEntityType(final Class oldType) {
synchronized (SchemaService.class) {
final String simpleName = oldType.getSimpleName();
final String fqcn = oldType.getName();
nodeEntityClassCache.remove(simpleName);
relationshipEntityClassCache.remove(simpleName);
nodeEntityPackages.remove(fqcn);
relationshipPackages.remove(fqcn);
globalPropertyViewMap.remove(fqcn);
globalClassDBNamePropertyMap.remove(fqcn);
globalClassJSNamePropertyMap.remove(fqcn);
interfaceMap.remove(oldType);
// clear all
combinedTypeRelationClassCache.clear();
// clear interfaceCache manually..
for (final Set<Class> classes : interfaceCache.values()) {
if (classes.contains(oldType)) {
classes.remove(oldType);
}
}
}
}
@Override
public void registerEntityType(final Class type) {
// moved here from scanEntity, no reason to have this in a separate
// method requiring two different calls instead of one
String simpleName = type.getSimpleName();
String fqcn = type.getName();
if (AbstractNode.class.isAssignableFrom(type)) {
nodeEntityClassCache.put(simpleName, type);
nodeEntityPackages.add(fqcn.substring(0, fqcn.lastIndexOf(".")));
globalPropertyViewMap.remove(fqcn);
}
if (AbstractRelationship.class.isAssignableFrom(type)) {
relationshipEntityClassCache.put(simpleName, type);
relationshipPackages.add(fqcn.substring(0, fqcn.lastIndexOf(".")));
globalPropertyViewMap.remove(fqcn);
}
for (Class interfaceClass : type.getInterfaces()) {
String interfaceName = interfaceClass.getSimpleName();
Set<Class> classesForInterface = interfaceCache.get(interfaceName);
if (classesForInterface == null) {
classesForInterface = new LinkedHashSet<>();
interfaceCache.put(interfaceName, classesForInterface);
}
classesForInterface.add(type);
}
try {
final Map<Field, PropertyKey> allProperties = getFieldValuesOfType(PropertyKey.class, type);
final Map<Field, View> views = getFieldValuesOfType(View.class, type);
for (final Map.Entry<Field, PropertyKey> entry : allProperties.entrySet()) {
final PropertyKey propertyKey = entry.getValue();
final Field field = entry.getKey();
final Class declaringClass = field.getDeclaringClass();
if (declaringClass != null) {
propertyKey.setDeclaringClass(declaringClass);
registerProperty(declaringClass, propertyKey);
}
registerProperty(type, propertyKey);
}
for (Map.Entry<Field, View> entry : views.entrySet()) {
final Field field = entry.getKey();
final View view = entry.getValue();
for (PropertyKey propertyKey : view.properties()) {
// register field in view for entity class and declaring superclass
registerPropertySet(field.getDeclaringClass(), view.name(), propertyKey);
registerPropertySet(type, view.name(), propertyKey);
}
}
} catch (Throwable t) {
logger.error("Unable to register type {}: {}", new Object[]{type, t.getMessage()});
}
Map<String, Method> typeMethods = exportedMethodMap.get(fqcn);
if (typeMethods == null) {
typeMethods = new HashMap<>();
exportedMethodMap.put(fqcn, typeMethods);
}
typeMethods.putAll(getAnnotatedMethods(type, Export.class));
// extract interfaces for later use
getInterfacesForType(type);
}
/**
* Register a transformation that will be applied to every newly created
* entity of a given type.
*
* @param type the type of the entities for which the transformation
* should be applied
* @param transformation the transformation to apply on every entity
*/
@Override
public void registerEntityCreationTransformation(Class type, Transformation<GraphObject> transformation) {
final Set<Transformation<GraphObject>> transformations = getEntityCreationTransformationsForType(type);
if (!transformations.contains(transformation)) {
transformations.add(transformation);
}
}
@Override
public Set<Class> getInterfacesForType(Class type) {
Set<Class> interfaces = interfaceMap.get(type);
if (interfaces == null) {
interfaces = new LinkedHashSet<>();
interfaceMap.put(type, interfaces);
for (Class iface : type.getInterfaces()) {
reverseInterfaceMap.put(iface.getSimpleName(), iface);
interfaces.add(iface);
}
}
return interfaces;
}
@Override
public Map<String, Method> getExportedMethodsForType(Class type) {
return exportedMethodMap.get(type.getName());
}
@Override
public boolean isKnownProperty(final PropertyKey key) {
return globalKnownPropertyKeys.contains(key);
}
@Override
public FactoryDefinition getFactoryDefinition() {
return factoryDefinition;
}
@Override
public void registerFactoryDefinition(FactoryDefinition factory) {
factoryDefinition = factory;
}
/**
* Registers a property group for the given key of the given entity
* type. A property group can be used to combine a set of properties
* into an object.
*
* @param type the type of the entities for which the property group
* should be registered
* @param key the property key under which the property group should be
* visible
* @param propertyGroup the property group
*/
@Override
public void registerPropertyGroup(Class type, PropertyKey key, PropertyGroup propertyGroup) {
getPropertyGroupMapForType(type).put(key.dbName(), propertyGroup);
}
@Override
public void registerConvertedProperty(PropertyKey propertyKey) {
globalKnownPropertyKeys.add(propertyKey);
}
@Override
public synchronized Set<Transformation<GraphObject>> getEntityCreationTransformations(Class type) {
Set<Transformation<GraphObject>> transformations = new TreeSet<>();
Class localType = type;
// collect for all superclasses
while (localType != null && !localType.equals(Object.class)) {
transformations.addAll(getEntityCreationTransformationsForType(localType));
localType = localType.getSuperclass();
}
return transformations;
}
@Override
public PropertyGroup getPropertyGroup(Class type, PropertyKey key) {
return getPropertyGroup(type, key.dbName());
}
@Override
public PropertyGroup getPropertyGroup(Class type, String key) {
PropertyGroup group = getAggregatedPropertyGroupMapForType(type).get(key);
if (group == null) {
Class localType = type;
while (group == null && localType != null && !localType.equals(Object.class)) {
group = getPropertyGroupMapForType(localType).get(key);
if (group == null) {
// try interfaces as well
for (Class interfaceClass : getInterfacesForType(localType)) {
group = getPropertyGroupMapForType(interfaceClass).get(key);
if (group != null) {
break;
}
}
}
localType = localType.getSuperclass();
}
getAggregatedPropertyGroupMapForType(type).put(key, group);
}
return group;
}
@Override
public void registerViewTransformation(Class type, String view, ViewTransformation transformation) {
getViewTransformationMapForType(type).put(view, transformation);
}
@Override
public ViewTransformation getViewTransformation(Class type, String view) {
return getViewTransformationMapForType(type).get(view);
}
@Override
public Set<String> getPropertyViews() {
Set<String> views = new LinkedHashSet<>();
// add all existing views
for (Map<String, Set<PropertyKey>> view : globalPropertyViewMap.values()) {
views.addAll(view.keySet());
}
// merge dynamic views in as well
views.addAll(dynamicViews);
return Collections.unmodifiableSet(views);
}
@Override
public Set<String> getPropertyViewsForType(final Class type) {
final Map<String, Set<PropertyKey>> map = getPropertyViewMapForType(type);
if (map != null) {
return map.keySet();
}
return Collections.emptySet();
}
@Override
public void registerDynamicViews(final Set<String> dynamicViews) {
this.dynamicViews.clear();
this.dynamicViews.addAll(dynamicViews);
}
@Override
public Set<PropertyKey> getPropertySet(Class type, String propertyView) {
Map<String, Set<PropertyKey>> propertyViewMap = getPropertyViewMapForType(type);
Set<PropertyKey> properties = propertyViewMap.get(propertyView);
if (properties == null) {
properties = new LinkedHashSet<>();
}
// read-only
return Collections.unmodifiableSet(properties);
}
/**
* Registers the given set of property keys for the view with name
* <code>propertyView</code> and the given prefix of entities with the
* given type.
*
* @param type the type of the entities for which the view will be
* registered
* @param propertyView the name of the property view for which the
* property set will be registered
* @param propertySet the set of property keys to register for the given
* view
*/
@Override
public void registerPropertySet(Class type, String propertyView, PropertyKey... propertySet) {
Map<String, Set<PropertyKey>> propertyViewMap = getPropertyViewMapForType(type);
Set<PropertyKey> properties = propertyViewMap.get(propertyView);
if (properties == null) {
properties = new LinkedHashSet<>();
propertyViewMap.put(propertyView, properties);
}
// allow properties to override existing ones as they
// are most likely from a more concrete class.
for (final PropertyKey key : propertySet) {
// property keys are referenced by their names,
// that's why we seemingly remove the existing
// key, but the set does not differentiate
// between different keys
if (properties.contains(key)) {
properties.remove(key);
}
properties.add(key);
}
}
@Override
public void registerPropertySet(final Class type, final String propertyView, final String propertyName) {
this.registerPropertySet(type, propertyView, this.getPropertyKeyForJSONName(type, propertyName));
}
@Override
public PropertyKey getPropertyKeyForDatabaseName(Class type, String dbName) {
return getPropertyKeyForDatabaseName(type, dbName, true);
}
@Override
public PropertyKey getPropertyKeyForDatabaseName(Class type, String dbName, boolean createGeneric) {
Map<String, PropertyKey> classDBNamePropertyMap = getClassDBNamePropertyMapForType(type);
PropertyKey key = classDBNamePropertyMap.get(dbName);
if (key == null) {
// first try: uuid
if (GraphObject.id.dbName().equals(dbName)) {
return GraphObject.id;
}
if (createGeneric) {
key = new GenericProperty(dbName);
}
}
return key;
}
@Override
public PropertyKey getPropertyKeyForJSONName(Class type, String jsonName) {
return getPropertyKeyForJSONName(type, jsonName, true);
}
@Override
public PropertyKey getPropertyKeyForJSONName(Class type, String jsonName, boolean createIfNotFound) {
if (jsonName == null) {
return null;
}
Map<String, PropertyKey> classJSNamePropertyMap = getClassJSNamePropertyMapForType(type);
PropertyKey key = classJSNamePropertyMap.get(jsonName);
if (key == null) {
// first try: uuid
if (GraphObject.id.dbName().equals(jsonName)) {
return GraphObject.id;
}
if (createIfNotFound) {
key = new GenericProperty(jsonName);
}
}
return key;
}
@Override
public Set<PropertyValidator> getPropertyValidators(final SecurityContext securityContext, Class type, PropertyKey propertyKey) {
Set<PropertyValidator> validators = new LinkedHashSet<>();
Map<PropertyKey, Set<PropertyValidator>> validatorMap = null;
Class localType = type;
// try all superclasses
while (localType != null && !localType.equals(Object.class)) {
validatorMap = getPropertyValidatorMapForType(localType);
Set<PropertyValidator> classValidators = validatorMap.get(propertyKey);
if (classValidators != null) {
validators.addAll(validatorMap.get(propertyKey));
}
// try converters from interfaces as well
for (Class interfaceClass : getInterfacesForType(localType)) {
Set<PropertyValidator> interfaceValidators = getPropertyValidatorMapForType(interfaceClass).get(propertyKey);
if (interfaceValidators != null) {
validators.addAll(interfaceValidators);
}
}
// one level up :)
localType = localType.getSuperclass();
}
return validators;
}
@Override
public void registerProperty(Class type, PropertyKey propertyKey) {
getClassDBNamePropertyMapForType(type).put(propertyKey.dbName(), propertyKey);
getClassJSNamePropertyMapForType(type).put(propertyKey.jsonName(), propertyKey);
registerPropertySet(type, PropertyView.All, propertyKey);
// inform property key of its registration
propertyKey.registrationCallback(type);
}
@Override
public void registerDynamicProperty(Class type, PropertyKey propertyKey) {
synchronized (SchemaService.class) {
final String typeName = type.getName();
registerProperty(type, propertyKey);
// scan all existing classes and find all classes that have the given type as a supertype
for (final Class possibleSubclass : nodeEntityClassCache.values()) {
// need to compare strings not classes here..
for (final Class supertype : getAllTypes(possibleSubclass)) {
if (supertype.getName().equals(typeName)) {
registerProperty(possibleSubclass, propertyKey);
registerPropertySet(possibleSubclass, PropertyView.Ui, propertyKey);
}
}
}
}
}
@Override
public Map<String, StructrModule> getModules() {
return modules;
}
@Override
public Map<String, Map<String, PropertyKey>> getTypeAndPropertyMapping() {
return Collections.unmodifiableMap(globalClassJSNamePropertyMap);
}
// ----- private methods -----
private void scanResources() {
Set<String> resourcePaths = getResourcesToScan();
for (String resourcePath : resourcePaths) {
scanResource(resourcePath);
}
logger.info("{} JARs scanned", resourcePaths.size());
}
private void scanResource(final String resourceName) {
try {
final StructrModuleInfo module = loadResource(resourceName);
if (module != null) {
importResource(module);
} else {
logger.warn("Module was null!");
}
} catch (IOException ioex) {
logger.warn("Error loading module {}: {}", new Object[]{resourceName, ioex.getMessage()});
}
}
private void importResource(StructrModuleInfo module) throws IOException {
final Set<String> classes = module.getClasses();
for (final String name : classes) {
String className = StringUtils.removeStart(name, ".");
try {
// instantiate class..
final Class clazz = Class.forName(className);
final int modifiers = clazz.getModifiers();
// register node entity classes
if (NodeInterface.class.isAssignableFrom(clazz)) {
registerEntityType(clazz);
}
// register entity classes
if (AbstractRelationship.class.isAssignableFrom(clazz) && !(Modifier.isAbstract(modifiers))) {
registerEntityType(clazz);
}
// register services
if (Service.class.isAssignableFrom(clazz) && !(Modifier.isAbstract(modifiers))) {
Services.getInstance().registerServiceClass(clazz);
}
// register agents
if (Agent.class.isAssignableFrom(clazz) && !(Modifier.isAbstract(modifiers))) {
final String simpleName = clazz.getSimpleName();
final String fullName = clazz.getName();
agentClassCache.put(simpleName, clazz);
agentPackages.add(fullName.substring(0, fullName.lastIndexOf(".")));
}
// register modules
if (StructrModule.class.isAssignableFrom(clazz) && !(Modifier.isAbstract(modifiers))) {
try {
// we need to make sure that a module is initialized exactly once
final StructrModule structrModule = (StructrModule) clazz.newInstance();
final String moduleName = structrModule.getName();
if (!modules.containsKey(moduleName)) {
modules.put(moduleName, structrModule);
logger.info("Activating module {}", moduleName);
structrModule.onLoad();
}
} catch (Throwable t) {
logger.warn("Unable to instantiate module " + clazz.getName(), t);
}
}
} catch (Throwable t) {
logger.debug("Error trying to load class " + className, t);
}
}
}
private StructrModuleInfo loadResource(String resource) throws IOException {
// create module
final StructrModuleInfo ret = new StructrModuleInfo(resource);
final Set<String> classes = ret.getClasses();
if (resource.endsWith(".jar") || resource.endsWith(".war")) {
final ZipFile zipFile = new ZipFile(new File(resource), ZipFile.OPEN_READ);
// conventions that might be useful here:
// ignore entries beginning with meta-inf/
// handle entries beginning with images/ as IMAGE
// handle entries beginning with pages/ as PAGES
// handle entries ending with .jar as libraries, to be deployed to WEB-INF/lib
// handle other entries as potential page and/or entity classes
// .. to be extended
// (entries that end with "/" are directories)
for (final Enumeration<? extends ZipEntry> entries = zipFile.entries(); entries.hasMoreElements();) {
final ZipEntry entry = entries.nextElement();
final String entryName = entry.getName();
if (entryName.endsWith(".class")) {
String fileEntry = entry.getName().replaceAll("[/]+", ".");
// add class entry to Module
classes.add(fileEntry.substring(0, fileEntry.length() - 6));
}
}
zipFile.close();
} else if (resource.endsWith(classesDir)) {
addClassesRecursively(new File(resource), classesDir, classes);
} else if (resource.endsWith(testClassesDir)) {
addClassesRecursively(new File(resource), testClassesDir, classes);
}
return ret;
}
private void addClassesRecursively(File dir, String prefix, Set<String> classes) {
if (dir == null) {
return;
}
int prefixLen = prefix.length();
File[] files = dir.listFiles();
if (files == null) {
return;
}
for (final File file : files) {
if (file.isDirectory()) {
addClassesRecursively(file, prefix, classes);
} else {
try {
String fileEntry = file.getAbsolutePath();
fileEntry = fileEntry.substring(0, fileEntry.length() - 6);
fileEntry = fileEntry.substring(fileEntry.indexOf(prefix) + prefixLen);
fileEntry = fileEntry.replaceAll("[".concat(fileSepEscaped).concat("]+"), ".");
if (fileEntry.startsWith(".")) {
fileEntry = fileEntry.substring(1);
}
classes.add(fileEntry);
} catch (Throwable t) {
// ignore
logger.warn("", t);
}
}
}
}
private Relation instantiate(final Class clazz) {
try {
return (Relation) clazz.newInstance();
} catch (Throwable t) {
// ignore
}
return null;
}
private String getCombinedType(final String sourceType, final String relType, final String targetType) {
return sourceType
.concat(DefaultFactoryDefinition.COMBINED_RELATIONSHIP_KEY_SEP)
.concat(relType)
.concat(DefaultFactoryDefinition.COMBINED_RELATIONSHIP_KEY_SEP)
.concat(targetType);
}
/**
* Scans the class path and returns a Set containing all structr
* modules.
*
* @return a Set of active module names
*/
private Set<String> getResourcesToScan() {
final String classPath = System.getProperty("java.class.path");
final Set<String> modules = new LinkedHashSet<>();
final Pattern pattern = Pattern.compile(".*(structr).*(war|jar)");
final Matcher matcher = pattern.matcher("");
for (final String jarPath : classPath.split("[".concat(pathSep).concat("]+"))) {
final String lowerPath = jarPath.toLowerCase();
if (lowerPath.endsWith(classesDir) || lowerPath.endsWith(testClassesDir)) {
modules.add(jarPath);
} else {
final String moduleName = lowerPath.substring(lowerPath.lastIndexOf(pathSep) + 1);
matcher.reset(moduleName);
if (matcher.matches()) {
modules.add(jarPath);
}
}
}
for (final String resource : Services.getInstance().getResources()) {
final String lowerResource = resource.toLowerCase();
if (lowerResource.endsWith(".jar") || lowerResource.endsWith(".war")) {
modules.add(resource);
}
}
return modules;
}
private <T> Map<Field, T> getFieldValuesOfType(final Class<T> fieldType, final Class entityType) {
final Map<Field, T> fields = new LinkedHashMap<>();
final Set<Class<?>> allTypes = getAllTypes(entityType);
for (final Class<?> type : allTypes) {
for (final Field field : type.getDeclaredFields()) {
// only use static fields, because field.get(null) will throw a NPE on non-static fields
if (fieldType.isAssignableFrom(field.getType()) && Modifier.isStatic(field.getModifiers())) {
try {
// ensure access
field.setAccessible(true);
// fetch value
final T value = (T) field.get(null);
if (value != null) {
fields.put(field, value);
}
} catch (Throwable t) {
// ignore
}
}
}
}
return fields;
}
private Set<Class<?>> getAllTypes(final Class<?> type) {
final List<Class<?>> types = new LinkedList<>();
Class localType = type;
do {
collectAllInterfaces(localType, types);
types.add(localType);
localType = localType.getSuperclass();
} while (localType != null && !localType.equals(Object.class));
Collections.reverse(types);
return new LinkedHashSet<>(types);
}
private void collectAllInterfaces(final Class<?> type, final List<Class<?>> interfaces) {
if (interfaces.contains(type)) {
return;
}
for (final Class iface : type.getInterfaces()) {
collectAllInterfaces(iface, interfaces);
interfaces.add(iface);
}
}
private Map<String, Set<PropertyKey>> getPropertyViewMapForType(final Class type) {
Map<String, Set<PropertyKey>> propertyViewMap = globalPropertyViewMap.get(type.getName());
if (propertyViewMap == null) {
propertyViewMap = new LinkedHashMap<>();
globalPropertyViewMap.put(type.getName(), propertyViewMap);
}
return propertyViewMap;
}
private Map<String, PropertyKey> getClassDBNamePropertyMapForType(final Class type) {
Map<String, PropertyKey> classDBNamePropertyMap = globalClassDBNamePropertyMap.get(type.getName());
if (classDBNamePropertyMap == null) {
classDBNamePropertyMap = new LinkedHashMap<>();
globalClassDBNamePropertyMap.put(type.getName(), classDBNamePropertyMap);
}
return classDBNamePropertyMap;
}
private Map<String, PropertyKey> getClassJSNamePropertyMapForType(final Class type) {
Map<String, PropertyKey> classJSNamePropertyMap = globalClassJSNamePropertyMap.get(type.getName());
if (classJSNamePropertyMap == null) {
classJSNamePropertyMap = new LinkedHashMap<>();
globalClassJSNamePropertyMap.put(type.getName(), classJSNamePropertyMap);
}
return classJSNamePropertyMap;
}
private Map<PropertyKey, Set<PropertyValidator>> getPropertyValidatorMapForType(final Class type) {
Map<PropertyKey, Set<PropertyValidator>> validatorMap = globalValidatorMap.get(type.getName());
if (validatorMap == null) {
validatorMap = new LinkedHashMap<>();
globalValidatorMap.put(type.getName(), validatorMap);
}
return validatorMap;
}
private Map<String, PropertyGroup> getAggregatedPropertyGroupMapForType(final Class type) {
Map<String, PropertyGroup> groupMap = globalAggregatedPropertyGroupMap.get(type.getName());
if (groupMap == null) {
groupMap = new LinkedHashMap<>();
globalAggregatedPropertyGroupMap.put(type.getName(), groupMap);
}
return groupMap;
}
private Map<String, PropertyGroup> getPropertyGroupMapForType(final Class type) {
Map<String, PropertyGroup> groupMap = globalPropertyGroupMap.get(type.getName());
if (groupMap == null) {
groupMap = new LinkedHashMap<>();
globalPropertyGroupMap.put(type.getName(), groupMap);
}
return groupMap;
}
private Set<Transformation<GraphObject>> getEntityCreationTransformationsForType(final Class type) {
final String name = type.getName();
Set<Transformation<GraphObject>> transformations = globalTransformationMap.get(name);
if (transformations == null) {
transformations = new LinkedHashSet<>();
globalTransformationMap.put(name, transformations);
}
return transformations;
}
private Map<String, ViewTransformation> getViewTransformationMapForType(final Class type) {
Map<String, ViewTransformation> viewTransformationMap = viewTransformations.get(type.getName());
if (viewTransformationMap == null) {
viewTransformationMap = new LinkedHashMap<>();
viewTransformations.put(type.getName(), viewTransformationMap);
}
return viewTransformationMap;
}
public void printCacheStats() {
System.out.println("###################################################");
System.out.println("" + relationshipEntityClassCache.size());
System.out.println("" + nodeEntityClassCache.size());
System.out.println("" + nodeEntityPackages.size());
System.out.println("" + relationshipPackages.size());
System.out.println("" + combinedTypeRelationClassCache.size());
System.out.println("" + interfaceCache.size());
System.out.println("" + globalPropertyViewMap.size());
System.out.println("" + globalValidatorMap.size());
System.out.println("" + globalClassDBNamePropertyMap.size());
System.out.println("" + globalClassJSNamePropertyMap.size());
System.out.println("" + globalAggregatedPropertyGroupMap.size());
System.out.println("" + globalPropertyGroupMap.size());
System.out.println("" + viewTransformations.size());
System.out.println("" + globalTransformationMap.size());
System.out.println("" + exportedMethodMap.size());
System.out.println("" + interfaceMap.size());
System.out.println("" + reverseInterfaceMap.size());
System.out.println("" + globalKnownPropertyKeys.size());
System.out.println("" + dynamicViews.size());
System.out.println("###################################################");
}
}