/**
* 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.bolt;
import java.io.File;
import java.io.FileReader;
import java.io.FileWriter;
import java.io.IOException;
import java.io.Reader;
import java.io.Writer;
import java.text.SimpleDateFormat;
import java.util.Collections;
import java.util.Date;
import java.util.HashMap;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import java.util.Properties;
import java.util.Set;
import java.util.concurrent.ConcurrentHashMap;
import org.neo4j.driver.v1.AuthTokens;
import org.neo4j.driver.v1.Config;
import org.neo4j.driver.v1.Driver;
import org.neo4j.driver.v1.GraphDatabase;
import org.neo4j.driver.v1.exceptions.ClientException;
import org.neo4j.graphdb.GraphDatabaseService;
import org.neo4j.graphdb.factory.GraphDatabaseBuilder;
import org.neo4j.graphdb.factory.GraphDatabaseFactory;
import org.neo4j.graphdb.factory.GraphDatabaseSettings;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.structr.api.DatabaseService;
import org.structr.api.NativeResult;
import org.structr.api.NotInTransactionException;
import org.structr.api.Transaction;
import org.structr.api.config.Settings;
import org.structr.api.graph.GraphProperties;
import org.structr.api.graph.Label;
import org.structr.api.graph.Node;
import org.structr.api.graph.Relationship;
import org.structr.api.graph.RelationshipType;
import org.structr.api.index.Index;
import org.structr.api.util.Iterables;
import org.structr.bolt.index.CypherNodeIndex;
import org.structr.bolt.index.CypherRelationshipIndex;
import org.structr.bolt.mapper.NodeNodeMapper;
import org.structr.bolt.mapper.RelationshipRelationshipMapper;
import org.structr.bolt.wrapper.NodeWrapper;
import org.structr.bolt.wrapper.RelationshipWrapper;
/**
*
*/
public class BoltDatabaseService implements DatabaseService, GraphProperties {
private static final Logger logger = LoggerFactory.getLogger(BoltDatabaseService.class.getName());
private static final Map<String, RelationshipType> relTypeCache = new ConcurrentHashMap<>();
private static final Map<String, Label> labelCache = new ConcurrentHashMap<>();
private static final ThreadLocal<SessionTransaction> sessions = new ThreadLocal<>();
private Properties globalGraphProperties = null;
private CypherRelationshipIndex relationshipIndex = null;
private CypherNodeIndex nodeIndex = null;
private GraphDatabaseService graphDb = null;
private boolean needsIndexRebuild = false;
private String databaseUrl = null;
private String databasePath = null;
private Driver driver = null;
private int queryCacheSize = 1000;
@Override
public void initialize() {
this.databasePath = Settings.DatabasePath.getValue();
final GraphDatabaseSettings.BoltConnector bolt = GraphDatabaseSettings.boltConnector("0");
databaseUrl = Settings.ConnectionUrl.getValue();
final String username = Settings.ConnectionUser.getValue();
final String password = Settings.ConnectionPassword.getValue();
final String driverMode = Settings.DatabaseDriverMode.getValue();
final String confPath = databasePath + "/neo4j.conf";
final File confFile = new File(confPath);
boolean tryAgain = true;
// see https://github.com/neo4j/neo4j-java-driver/issues/364 for an explanation
final String databaseServerUrl;
final String databaseDriverUrl;
if (databaseUrl.length() >= 7 && databaseUrl.substring(0, 7).equalsIgnoreCase("bolt://")) {
databaseServerUrl = databaseUrl.substring(7);
databaseDriverUrl = databaseUrl;
} else {
databaseServerUrl = databaseUrl;
databaseDriverUrl = "bolt://" + databaseUrl;
}
// create db directory if it does not exist
new File(databasePath).mkdirs();
if (!"remote".equals(driverMode)) {
final GraphDatabaseBuilder builder = new GraphDatabaseFactory()
.newEmbeddedDatabaseBuilder(new File(databasePath))
.setConfig( GraphDatabaseSettings.allow_store_upgrade, "true")
.setConfig("dbms.allow_format_migration", "true")
.setConfig( bolt.type, "BOLT" )
.setConfig( bolt.enabled, "true" )
.setConfig( bolt.address, databaseServerUrl);
if (confFile.exists()) {
builder.loadPropertiesFromFile(confPath);
}
while (tryAgain) {
try {
graphDb = builder.newGraphDatabase();
tryAgain = false;
} catch (Throwable t) {
tryAgain = handleMigration(t);
}
}
}
driver = GraphDatabase.driver(databaseDriverUrl,
AuthTokens.basic(username, password),
Config.build().withEncryptionLevel(Config.EncryptionLevel.NONE).toConfig()
);
final int relCacheSize = Settings.RelationshipCacheSize.getValue();
final int nodeCacheSize = Settings.NodeCacheSize.getValue();
this.queryCacheSize = Settings.QueryCacheSize.getValue();
NodeWrapper.initialize(nodeCacheSize);
logger.info("Node cache size set to {}", nodeCacheSize);
RelationshipWrapper.initialize(relCacheSize);
logger.info("Relationship cache size set to {}", relCacheSize);
}
@Override
public void shutdown() {
RelationshipWrapper.clearCache();
NodeWrapper.clearCache();
driver.close();
graphDb.shutdown();
}
@Override
public <T> T forName(final Class<T> type, final String name) {
if (Label.class.equals(type)) {
return (T)getOrCreateLabel(name);
}
if (RelationshipType.class.equals(type)) {
return (T)getOrCreateRelationshipType(name);
}
throw new RuntimeException("Cannot create object of type " + type);
}
@Override
public Transaction beginTx() {
SessionTransaction session = sessions.get();
if (session == null || session.isClosed()) {
try {
session = new SessionTransaction(this, driver.session());
sessions.set(session);
} catch (ClientException cex) {
logger.warn("Cannot connect to Neo4j database server at {}", databaseUrl);
}
}
return session;
}
@Override
public Node createNode(final Set<String> labels, final Map<String, Object> properties) {
final StringBuilder buf = new StringBuilder("CREATE (n");
final Map<String, Object> map = new HashMap<>();
for (final String label : labels) {
buf.append(":");
buf.append(label);
}
buf.append(" {properties}) RETURN n");
// make properties available to Cypher statement
map.put("properties", properties);
return NodeWrapper.newInstance(this, getCurrentTransaction().getNode(buf.toString(), map));
}
@Override
public Node getNodeById(final long id) {
return NodeWrapper.newInstance(this, id);
}
@Override
public Relationship getRelationshipById(final long id) {
final SessionTransaction tx = getCurrentTransaction();
final Map<String, Object> map = new HashMap<>();
map.put("id", id);
final org.neo4j.driver.v1.types.Relationship rel = tx.getRelationship("CYPHER planner=rule MATCH ()-[r]-() WHERE ID(r) = {id} RETURN r", map);
return RelationshipWrapper.newInstance(this, rel);
}
@Override
public Iterable<Node> getAllNodes() {
final SessionTransaction tx = getCurrentTransaction();
final NodeNodeMapper mapper = new NodeNodeMapper(this);
return Iterables.map(mapper, tx.getNodes("MATCH (n) RETURN n", Collections.emptyMap()));
}
@Override
public Iterable<Relationship> getAllRelationships() {
final RelationshipRelationshipMapper mapper = new RelationshipRelationshipMapper(this);
final SessionTransaction tx = getCurrentTransaction();
return Iterables.map(mapper, tx.getRelationships("MATCH ()-[r]-() RETURN r", Collections.emptyMap()));
}
@Override
public GraphProperties getGlobalProperties() {
return this;
}
@Override
public Index<Node> nodeIndex() {
if (nodeIndex == null) {
nodeIndex = new CypherNodeIndex(this, queryCacheSize);
}
return nodeIndex;
}
@Override
public Index<Relationship> relationshipIndex() {
if (relationshipIndex == null) {
relationshipIndex = new CypherRelationshipIndex(this, queryCacheSize);
}
return relationshipIndex;
}
@Override
public NativeResult execute(final String nativeQuery, final Map<String, Object> parameters) {
return getCurrentTransaction().run(nativeQuery, parameters);
}
@Override
public NativeResult execute(final String nativeQuery) {
return execute(nativeQuery, Collections.EMPTY_MAP);
}
@Override
public void invalidateQueryCache() {
if (nodeIndex != null) {
nodeIndex.invalidateCache();
}
if (relationshipIndex != null) {
relationshipIndex.invalidateCache();
}
}
public SessionTransaction getCurrentTransaction() {
final SessionTransaction tx = sessions.get();
if (tx == null || tx.isClosed()) {
throw new NotInTransactionException("Not in transaction");
}
return tx;
}
public boolean logQueries() {
return Settings.CypherDebugLogging.getValue();
}
// ----- interface GraphProperties -----
@Override
public void setProperty(final String name, final Object value) {
final Properties properties = getProperties();
boolean hasChanges = false;
if (value == null) {
if (properties.containsKey(name)) {
properties.remove(name);
hasChanges = true;
}
} else {
properties.setProperty(name, value.toString());
hasChanges = true;
}
if (hasChanges) {
final File propertiesFile = new File(databasePath + "/graph.properties");
try (final Writer writer = new FileWriter(propertiesFile)) {
properties.store(writer, "Created by Structr at " + new Date());
} catch (IOException ioex) {
logger.warn("Unable to write properties file", ioex);
}
}
}
@Override
public Object getProperty(final String name) {
return getProperties().getProperty(name);
}
@Override
public boolean needsIndexRebuild() {
return needsIndexRebuild;
}
public Label getOrCreateLabel(final String name) {
Label label = labelCache.get(name);
if (label == null) {
label = new LabelImpl(name);
labelCache.put(name, label);
}
return label;
}
public RelationshipType getOrCreateRelationshipType(final String name) {
RelationshipType relType = relTypeCache.get(name);
if (relType == null) {
relType = new RelationshipTypeImpl(name);
relTypeCache.put(name, relType);
}
return relType;
}
// ----- private methods -----
private Properties getProperties() {
if (globalGraphProperties == null) {
globalGraphProperties = new Properties();
final File propertiesFile = new File(databasePath + "/graph.properties");
try (final Reader reader = new FileReader(propertiesFile)) {
globalGraphProperties.load(reader);
} catch (IOException ioex) {}
}
return globalGraphProperties;
}
private boolean handleMigration(final Throwable t) {
final List<String> messages = collectMessages(t);
if (contains(messages, "Legacy index migration failed")) {
// try to remove index directory and try again
logger.info("Legacy index migration failed, moving offending index files out of the way.");
final SimpleDateFormat df = new SimpleDateFormat("yyyy-MM-dd-HH-mm-ss");
final File indexDbFile = new File(databasePath + "/index.db");
final File indexDir = new File(databasePath + "/index");
if (indexDbFile.exists()) {
indexDbFile.renameTo(new File(databasePath + "/index.db.orig-" + df.format(System.currentTimeMillis())));
}
if (indexDir.exists()) {
indexDir.renameTo(new File(databasePath + "/index.orig-" + df.format(System.currentTimeMillis())));
}
// raise rebuild index flag
this.needsIndexRebuild = true;
// signal the service to try again
return true;
}
// cannot handle error
throw new RuntimeException(t);
}
private boolean contains(final List<String> src, final String toFind) {
for (final String s : src) {
if (s.contains(toFind)) {
return true;
}
}
return false;
}
private List<String> collectMessages(final Throwable t) {
final List<String> messages = new LinkedList<>();
Throwable current = t;
// collect exception messages
while (current != null) {
final String message = current.getMessage();
if (message != null) {
messages.add(message);
}
current = current.getCause();
}
return messages;
}
// ----- nested classes -----
private static class LabelImpl implements Label {
private String name = null;
private LabelImpl(final String name) {
this.name = name;
}
@Override
public String name() {
return name;
}
@Override
public int hashCode() {
return name.hashCode();
}
@Override
public boolean equals(final Object other) {
if (other instanceof Label) {
return other.hashCode() == hashCode();
}
return false;
}
}
private static class RelationshipTypeImpl implements RelationshipType {
private String name = null;
private RelationshipTypeImpl(final String name) {
this.name = name;
}
@Override
public String name() {
return name;
}
@Override
public int hashCode() {
return name.hashCode();
}
@Override
public boolean equals(final Object other) {
if (other instanceof RelationshipType) {
return other.hashCode() == hashCode();
}
return false;
}
}
}