package org.jboss.windup.graph; import java.io.File; import java.lang.reflect.Method; import java.nio.file.Path; import java.util.ArrayList; import java.util.Collections; import java.util.HashMap; import java.util.Iterator; import java.util.List; import java.util.Map; import java.util.Set; import java.util.logging.Logger; import org.apache.commons.configuration.BaseConfiguration; import org.apache.commons.configuration.Configuration; import org.apache.commons.configuration.ConfigurationException; import org.apache.commons.configuration.PropertiesConfiguration; import org.apache.commons.io.FileUtils; import org.apache.commons.lang3.RandomStringUtils; import org.apache.commons.lang3.StringUtils; import org.jboss.forge.furnace.Furnace; import org.jboss.forge.furnace.services.Imported; import org.jboss.forge.furnace.util.Annotations; import org.jboss.windup.graph.frames.TypeAwareFramedGraphQuery; import org.jboss.windup.graph.listeners.AfterGraphInitializationListener; import org.jboss.windup.graph.listeners.BeforeGraphCloseListener; import org.jboss.windup.graph.model.WindupFrame; import org.jboss.windup.graph.model.WindupVertexFrame; import org.jboss.windup.graph.service.GraphService; import com.sleepycat.je.LockMode; import com.thinkaurelius.titan.core.Cardinality; import com.thinkaurelius.titan.core.PropertyKey; import com.thinkaurelius.titan.core.TitanFactory; import com.thinkaurelius.titan.core.TitanGraph; import com.thinkaurelius.titan.core.schema.Mapping; import com.thinkaurelius.titan.core.schema.TitanManagement; import com.thinkaurelius.titan.core.util.TitanCleanup; import com.thinkaurelius.titan.diskstorage.berkeleyje.BerkeleyJEStoreManager; import com.tinkerpop.blueprints.Edge; import com.tinkerpop.blueprints.Graph; import com.tinkerpop.blueprints.Vertex; import com.tinkerpop.blueprints.util.wrappers.batch.BatchGraph; import com.tinkerpop.blueprints.util.wrappers.event.EventGraph; import com.tinkerpop.frames.FramedGraph; import com.tinkerpop.frames.FramedGraphConfiguration; import com.tinkerpop.frames.FramedGraphFactory; import com.tinkerpop.frames.Property; import com.tinkerpop.frames.modules.FrameClassLoaderResolver; import com.tinkerpop.frames.modules.Module; import com.tinkerpop.frames.modules.gremlingroovy.GremlinGroovyModule; import com.tinkerpop.frames.modules.javahandler.JavaHandlerModule; import org.jboss.windup.graph.model.WindupEdgeFrame; public class GraphContextImpl implements GraphContext { private static final Logger LOG = Logger.getLogger(GraphContextImpl.class.getName()); private final Furnace furnace; private final GraphTypeManager graphTypeManager; private final Path graphDir; private final GraphApiCompositeClassLoaderProvider classLoaderProvider; /** * Used to save all the {@link BeforeGraphCloseListener}s that are also {@link AfterGraphInitializationListener}. This is due a need to call * {@link BeforeGraphCloseListener#beforeGraphClose()} on the same instance on which * {@link AfterGraphInitializationListener#afterGraphStarted(Map, GraphContext)} } was called */ private final Map<String, BeforeGraphCloseListener> beforeGraphCloseListenerBuffer = new HashMap<>(); private Map<String, Object> configurationOptions; private EventGraph<TitanGraph> eventGraph; private BatchGraph<TitanGraph> batchGraph; private FramedGraph<EventGraph<TitanGraph>> framed; private Configuration conf; public GraphContextImpl(Furnace furnace, GraphTypeManager typeManager, GraphApiCompositeClassLoaderProvider classLoaderProvider, Path graphDir) { this.furnace = furnace; this.graphTypeManager = typeManager; this.classLoaderProvider = classLoaderProvider; this.graphDir = graphDir; } public GraphContextImpl create() { FileUtils.deleteQuietly(graphDir.toFile()); TitanGraph titan = initializeTitanGraph(); initializeTitanIndexes(titan); createFramed(titan); fireListeners(); return this; } public GraphContextImpl load() { TitanGraph titan = initializeTitanGraph(); createFramed(titan); fireListeners(); return this; } private void fireListeners() { Imported<AfterGraphInitializationListener> afterInitializationListeners = furnace.getAddonRegistry().getServices( AfterGraphInitializationListener.class); Map<String, Object> confProps = new HashMap<>(); Iterator<?> keyIter = conf.getKeys(); while (keyIter.hasNext()) { String key = (String) keyIter.next(); confProps.put(key, conf.getProperty(key)); } if (!afterInitializationListeners.isUnsatisfied()) { for (AfterGraphInitializationListener listener : afterInitializationListeners) { listener.afterGraphStarted(confProps, this); if (listener instanceof BeforeGraphCloseListener) { beforeGraphCloseListenerBuffer.put(listener.getClass().toString(), (BeforeGraphCloseListener) listener); } } } } private void createFramed(TitanGraph titanGraph) { this.eventGraph = new EventGraph<>(titanGraph); this.batchGraph = new BatchGraph<>(titanGraph, 1000L); final ClassLoader compositeClassLoader = classLoaderProvider.getCompositeClassLoader(); final FrameClassLoaderResolver classLoaderResolver = new FrameClassLoaderResolver() { public ClassLoader resolveClassLoader(Class<?> frameType) { return compositeClassLoader; } }; final Module addModules = new Module() { @Override public Graph configure(Graph baseGraph, FramedGraphConfiguration config) { config.setFrameClassLoaderResolver(classLoaderResolver); config.addFrameInitializer(new DefaultValueInitializer()); config.addMethodHandler(new MapInPropertiesHandler()); config.addMethodHandler(new MapInAdjacentPropertiesHandler()); config.addMethodHandler(new MapInAdjacentVerticesHandler()); config.addMethodHandler(new SetInPropertiesHandler()); return baseGraph; } }; FramedGraphFactory factory = new FramedGraphFactory( addModules, new JavaHandlerModule(), // Supports @JavaHandler graphTypeManager.build(), // Adds detected WindupVertexFrame/Model classes new GremlinGroovyModule() // Supports @Gremlin ); framed = factory.create(eventGraph); } private List<Indexed> getIndexAnnotations(Method method) { List<Indexed> results = new ArrayList<>(); Indexed index = method.getAnnotation(Indexed.class); if (index != null) results.add(index); Indexes indexes = method.getAnnotation(Indexes.class); if (indexes != null) { Collections.addAll(results, indexes.value()); } return results; } private void initializeTitanIndexes(TitanGraph titanGraph) { Map<String, IndexData> defaultIndexKeys = new HashMap<>(); Map<String, IndexData> searchIndexKeys = new HashMap<>(); Map<String, IndexData> listIndexKeys = new HashMap<>(); Set<Class<? extends WindupFrame<?>>> modelTypes = graphTypeManager.getRegisteredTypes(); for (Class<? extends WindupFrame<?>> type : modelTypes) { for (Method method : type.getDeclaredMethods()) { List<Indexed> annotations = getIndexAnnotations(method); for (Indexed index : annotations) { Property property = Annotations.getAnnotation(method, Property.class); if (property != null) { Class<?> dataType = index.dataType(); switch (index.value()) { case DEFAULT: defaultIndexKeys.put(property.value(), new IndexData(property.value(), index.name(), dataType)); break; case SEARCH: searchIndexKeys.put(property.value(), new IndexData(property.value(), index.name(), dataType)); break; case LIST: listIndexKeys.put(property.value(), new IndexData(property.value(), index.name(), dataType)); break; default: break; } } } } } /* * This is the root Model index that enables us to query on frame-type (by subclass type, etc.) Without this, every typed query would be slow. * Do not remove this unless something really paradigm-shifting has happened. */ listIndexKeys.put(WindupVertexFrame.TYPE_PROP, new IndexData(WindupVertexFrame.TYPE_PROP, "", String.class)); LOG.info("Detected and initialized [" + defaultIndexKeys.size() + "] default indexes: " + defaultIndexKeys); LOG.info("Detected and initialized [" + searchIndexKeys.size() + "] search indexes: " + searchIndexKeys); LOG.info("Detected and initialized [" + listIndexKeys.size() + "] list indexes: " + listIndexKeys); TitanManagement titan = titanGraph.getManagementSystem(); for (Map.Entry<String, IndexData> entry : defaultIndexKeys.entrySet()) { String key = entry.getKey(); IndexData indexData = entry.getValue(); Class<?> dataType = indexData.type; PropertyKey propKey = getOrCreatePropertyKey(titan, key, dataType, Cardinality.SINGLE); titan.buildIndex(indexData.getIndexName(), Vertex.class).addKey(propKey).buildCompositeIndex(); } for (Map.Entry<String, IndexData> entry : searchIndexKeys.entrySet()) { String key = entry.getKey(); IndexData indexData = entry.getValue(); Class<?> dataType = indexData.type; if (dataType == String.class) { PropertyKey propKey = getOrCreatePropertyKey(titan, key, String.class, Cardinality.SINGLE); titan.buildIndex(indexData.getIndexName(), Vertex.class).addKey(propKey, Mapping.STRING.getParameter()).buildMixedIndex("search"); } else { PropertyKey propKey = getOrCreatePropertyKey(titan, key, dataType, Cardinality.SINGLE); titan.buildIndex(indexData.getIndexName(), Vertex.class).addKey(propKey).buildMixedIndex("search"); } } for (Map.Entry<String, IndexData> entry : listIndexKeys.entrySet()) { String key = entry.getKey(); IndexData indexData = entry.getValue(); Class<?> dataType = indexData.type; PropertyKey propKey = getOrCreatePropertyKey(titan, key, dataType, Cardinality.LIST); titan.buildIndex(indexData.getIndexName(), Vertex.class).addKey(propKey).buildCompositeIndex(); } // Also index TYPE_PROP on Edges. /// Removed - Titan probably isn't capable of Cardinality.LIST for edges. There's no StandardEdge#addProperty(). { String indexName = "edge-typevalue"; // Titan enforces items to be String, but there can be multiple items under one property name. PropertyKey propKey = getOrCreatePropertyKey(titan, WindupEdgeFrame.TYPE_PROP, String.class, Cardinality.LIST); //PropertyKey propKey = getOrCreatePropertyKey(titan, WindupEdgeFrame.TYPE_PROP, ArrayList.class, Cardinality.SINGLE); titan.buildIndex(indexName, Edge.class).addKey(propKey).buildCompositeIndex(); }/**/ titan.commit(); } private PropertyKey getOrCreatePropertyKey(TitanManagement titanGraph, String key, Class<?> dataType, Cardinality cardinality) { PropertyKey propertyKey = titanGraph.getPropertyKey(key); if (propertyKey == null) { propertyKey = titanGraph.makePropertyKey(key).dataType(dataType).cardinality(cardinality).make(); } return propertyKey; } private TitanGraph initializeTitanGraph() { LOG.fine("Initializing graph."); Path lucene = graphDir.resolve("graphsearch"); Path berkeley = graphDir.resolve("titangraph"); // TODO: Externalize this. conf = new BaseConfiguration(); // Sets a unique id in order to fix WINDUP-697. This causes Titan to not attempt to generate and ID, // as the Titan id generation code fails on machines with broken network configurations. conf.setProperty("graph.unique-instance-id", "windup_" + System.nanoTime() + "_" + RandomStringUtils.randomAlphabetic(6)); conf.setProperty("storage.directory", berkeley.toAbsolutePath().toString()); conf.setProperty("storage.backend", "berkeleyje"); // Sets the berkeley cache to a relatively small value to reduce the memory footprint. // This is actually more important than performance on some of the smaller machines out there, and // the performance decrease seems to be minimal. conf.setProperty("storage.berkeleydb.cache-percentage", 1); // Set READ UNCOMMITTED to improve performance conf.setProperty("storage.berkeleydb.lock-mode", LockMode.READ_UNCOMMITTED); conf.setProperty("storage.berkeleydb.isolation-level", BerkeleyJEStoreManager.IsolationLevel.READ_UNCOMMITTED); // Increase storage write buffer since we basically do a large bulk load during the first phases. // See http://s3.thinkaurelius.com/docs/titan/current/bulk-loading.html conf.setProperty("storage.buffer-size", "4096"); // Turn off transactions to improve performance conf.setProperty("storage.transactions", false); conf.setProperty("ids.block-size", 25000); // conf.setProperty("ids.flush", true); // conf.setProperty("", false); // // turn on a db-cache that persists across txn boundaries, but make it relatively small conf.setProperty("cache.db-cache", true); conf.setProperty("cache.db-cache-clean-wait", 0); conf.setProperty("cache.db-cache-size", .09); conf.setProperty("cache.db-cache-time", 0); conf.setProperty("index.search.backend", "lucene"); conf.setProperty("index.search.directory", lucene.toAbsolutePath().toString()); writeToPropertiesFile(conf, graphDir.resolve("TitanConfiguration.properties").toFile()); return TitanFactory.open(conf); } public Configuration getConfiguration() { return conf; } @Override public GraphTypeManager getGraphTypeManager() { return graphTypeManager; } @Override public void close() { try { Imported<BeforeGraphCloseListener> beforeCloseListeners = furnace.getAddonRegistry().getServices(BeforeGraphCloseListener.class); for (BeforeGraphCloseListener listener : beforeCloseListeners) { if (!beforeGraphCloseListenerBuffer.containsKey(listener.getClass().toString())) { beforeGraphCloseListenerBuffer.put(listener.getClass().toString(), listener); } } for (BeforeGraphCloseListener listener : beforeGraphCloseListenerBuffer.values()) { listener.beforeGraphClose(); } beforeGraphCloseListenerBuffer.clear(); } catch (Exception e) { LOG.warning("Could not call before shutdown listeners during close due to: " + e.getMessage()); } this.eventGraph.getBaseGraph().shutdown(); } @Override public void clear() { if (this.eventGraph == null) return; if (this.eventGraph.getBaseGraph() == null) return; if (this.eventGraph.getBaseGraph().isOpen()) close(); TitanCleanup.clear(this.eventGraph.getBaseGraph()); } @Override public EventGraph<TitanGraph> getGraph() { return eventGraph; } /** * Returns a graph suitable for batchGraph processing. * <p> * Note: This bypasses the event graph (thus no events will be fired for modifications to this graph) */ public BatchGraph<TitanGraph> getBatch() { return batchGraph; } @Override public FramedGraph<EventGraph<TitanGraph>> getFramed() { return framed; } @Override public TypeAwareFramedGraphQuery getQuery() { return new TypeAwareFramedGraphQuery(getFramed()); } @Override public Path getGraphDirectory() { return graphDir; } @Override public Map<String, Object> getOptionMap() { if (this.configurationOptions == null) return Collections.emptyMap(); return Collections.unmodifiableMap(this.configurationOptions); } @Override public void setOptions(Map<String, Object> options) { this.configurationOptions = options; } @Override public String toString() { String graphHash = getGraph() == null ? "null" : "" + getGraph().hashCode(); return "GraphContextImpl(" + hashCode() + "), Graph(" + graphHash + ") + DataDir(" + getGraphDirectory() + ")"; } private void writeToPropertiesFile(Configuration conf, File file) { try { PropertiesConfiguration propConf = new PropertiesConfiguration(file); propConf.append(conf); propConf.save(); } catch (ConfigurationException ex) { throw new RuntimeException("Failed writing Titan config to " + file.getAbsolutePath() + ": " + ex.getMessage(), ex); } } @Override public <T extends WindupVertexFrame> GraphService<T> service(Class<T> clazz) { return new GraphService<>(this, clazz); } @Override public <T extends WindupVertexFrame> T getUnique(Class<T> clazz) { return service(clazz).getUnique(); } // --- Convenience delegations to new GraphService(this) -- @Override public <T extends WindupVertexFrame> Iterable<T> findAll(Class<T> clazz) { return service(clazz).findAll(); } @Override public <T extends WindupVertexFrame> T create(Class<T> clazz) { return service(clazz).create(); } @Override public void commit() { getGraph().getBaseGraph().commit(); } private class IndexData { private final String propertyName; private final String indexName; private final Class<?> type; public IndexData(String propertyName, String indexName, Class<?> type) { this.propertyName = propertyName; this.indexName = indexName; this.type = type; } public String getPropertyName() { return propertyName; } public String getIndexName() { return StringUtils.defaultIfBlank(indexName, propertyName); } public Class<?> getType() { return type; } @Override public String toString() { return String.format("IndexData{propertyName='%s', indexName='%s', type=%s}", propertyName, indexName, type); } } }