From c2f1230ed4b2e82fde6de7ee003f7925f7da2319 Mon Sep 17 00:00:00 2001 From: Michael Hunger Date: Mon, 22 Sep 2014 18:14:17 +0200 Subject: [PATCH] DATAGRAPH-253 Better abstraction layer for interfacing with Remote Neo4j --- spring-data-neo4j-rest/pom.xml | 21 - .../rest/graphdb/AbstractRemoteDatabase.java | 73 ++ .../neo4j/rest/graphdb/CypherRestShell.java | 55 ++ .../rest/graphdb/ExecutingRestRequest.java | 184 +++++ .../org/neo4j/rest/graphdb/PropertiesMap.java | 159 ++++ .../org/neo4j/rest/graphdb/RequestResult.java | 148 ++++ .../java/org/neo4j/rest/graphdb/RestAPI.java | 92 +++ .../neo4j/rest/graphdb/RestAPICypherImpl.java | 553 ++++++++++++++ .../org/neo4j/rest/graphdb/RestAPIImpl.java | 717 ++++++++++++++++++ .../org/neo4j/rest/graphdb/RestAPIIndex.java | 65 ++ .../neo4j/rest/graphdb/RestAPIInternal.java | 30 + .../neo4j/rest/graphdb/RestGraphDatabase.java | 163 ++++ .../org/neo4j/rest/graphdb/RestRequest.java | 42 + .../rest/graphdb/RestResultException.java | 80 ++ .../rest/graphdb/UpdatableRestResult.java | 28 + .../org/neo4j/rest/graphdb/UserAgent.java | 72 ++ .../rest/graphdb/batch/BatchRestAPI.java | 174 +++++ .../graphdb/batch/RecordingRestRequest.java | 124 +++ .../rest/graphdb/batch/RestOperations.java | 139 ++++ .../graphdb/converter/ConversionInfo.java | 44 ++ .../RelationshipIterableConverter.java | 52 ++ .../converter/RestEntityExtractor.java | 84 ++ .../RestEntityPropertyRefresher.java | 38 + .../converter/RestIndexHitsConverter.java | 49 ++ .../converter/RestResultConverter.java | 27 + .../converter/RestTableResultExtractor.java | 75 ++ .../converter/ResultTypeConverter.java | 155 ++++ .../graphdb/converter/TypeInformation.java | 125 +++ .../neo4j/rest/graphdb/entity/RestEntity.java | 254 +++++++ .../rest/graphdb/entity/RestEntityCache.java | 42 + .../neo4j/rest/graphdb/entity/RestNode.java | 237 ++++++ .../rest/graphdb/entity/RestRelationship.java | 111 +++ .../neo4j/rest/graphdb/index/IndexInfo.java | 36 + .../rest/graphdb/index/RestAutoIndexer.java | 73 ++ .../neo4j/rest/graphdb/index/RestIndex.java | 108 +++ .../rest/graphdb/index/RestIndexManager.java | 160 ++++ .../rest/graphdb/index/RestNodeIndex.java | 45 ++ .../index/RestRelationshipAutoIndexer.java | 38 + .../graphdb/index/RestRelationshipIndex.java | 58 ++ .../graphdb/index/RetrievedIndexInfo.java | 75 ++ .../rest/graphdb/index/SimpleIndexHits.java | 102 +++ .../rest/graphdb/query/CypherRestResult.java | 53 ++ .../rest/graphdb/query/CypherResult.java | 17 + .../rest/graphdb/query/CypherTransaction.java | 213 ++++++ .../rest/graphdb/query/CypherTxResult.java | 54 ++ .../neo4j/rest/graphdb/query/QueryEngine.java | 29 + .../graphdb/query/RestCypherQueryEngine.java | 52 ++ .../rest/graphdb/query/RestQueryResult.java | 72 ++ .../graphdb/transaction/NullTransaction.java | 51 ++ .../transaction/NullTransactionManager.java | 107 +++ .../transaction/RemoteCypherTransaction.java | 81 ++ .../rest/graphdb/traversal/NodePath.java | 82 ++ .../graphdb/traversal/RelationshipPath.java | 81 ++ .../rest/graphdb/traversal/RestDirection.java | 45 ++ .../graphdb/traversal/RestPathParser.java | 148 ++++ .../rest/graphdb/traversal/RestTraversal.java | 213 ++++++ .../traversal/RestTraversalDescription.java | 52 ++ .../rest/graphdb/traversal/RestTraverser.java | 79 ++ .../rest/graphdb/traversal/SimplePath.java | 121 +++ .../rest/graphdb/util/ArrayConverter.java | 81 ++ .../org/neo4j/rest/graphdb/util/Config.java | 58 ++ .../rest/graphdb/util/ConvertedResult.java | 27 + .../rest/graphdb/util/DefaultConverter.java | 93 +++ .../org/neo4j/rest/graphdb/util/Handler.java | 24 + .../neo4j/rest/graphdb/util/JsonHelper.java | 97 +++ .../neo4j/rest/graphdb/util/QueryResult.java | 27 + .../rest/graphdb/util/QueryResultBuilder.java | 132 ++++ .../graphdb/util/ResourceIterableWrapper.java | 45 ++ .../rest/graphdb/util/ResultConverter.java | 32 + .../rest/graphdb/util/StreamJsonHelper.java | 92 +++ .../neo4j/rest/graphdb/util/TestHelper.java | 34 + .../util/WrappingResourceIterator.java | 79 ++ .../neo4j/rest/SpringRestGraphDatabase.java | 19 +- .../org/neo4j/rest/graphdb/CypherPlugin.java | 44 ++ .../neo4j/rest/graphdb/EmptyGraphTest.java | 40 + .../neo4j/rest/graphdb/HelloWorldService.java | 46 ++ .../graphdb/IsRelationshipToNodeMatcher.java | 49 ++ .../neo4j/rest/graphdb/LocalTestServer.java | 230 ++++++ .../neo4j/rest/graphdb/MatrixDataGraph.java | 260 +++++++ .../rest/graphdb/MatrixDatabaseRestTest.java | 237 ++++++ .../rest/graphdb/MatrixDatabaseTest.java | 263 +++++++ .../rest/graphdb/Neo4jDatabaseCleaner.java | 102 +++ .../rest/graphdb/RelationshipHasMatcher.java | 90 +++ .../org/neo4j/rest/graphdb/RestAPITest.java | 396 ++++++++++ .../neo4j/rest/graphdb/RestAutoIndexTest.java | 148 ++++ .../graphdb/RestCypherQueryEngineTest.java | 170 +++++ .../neo4j/rest/graphdb/RestEntityTest.java | 90 +++ .../neo4j/rest/graphdb/RestGraphDbTest.java | 122 +++ .../org/neo4j/rest/graphdb/RestIndexTest.java | 246 ++++++ .../org/neo4j/rest/graphdb/RestNodeTest.java | 197 +++++ .../org/neo4j/rest/graphdb/RestTestBase.java | 139 ++++ .../graphdb/RestTraversalExecutionTest.java | 41 + .../neo4j/rest/graphdb/RestTraversalTest.java | 131 ++++ .../rest/graphdb/ResultTypeConverterTest.java | 235 ++++++ .../rest/graphdb/SimpleTransactionTest.java | 53 ++ .../java/org/neo4j/rest/graphdb/Type.java | 30 + .../rest/graphdb/TypeInformationTest.java | 147 ++++ .../graphdb/TypeInformationTestInterface.java | 42 + .../graphdb/extension/HelloWorldResource.java | 60 ++ .../graphdb/query/CypherTransactionTest.java | 81 ++ .../graphdb/traversal/RestPathParserTest.java | 81 ++ .../data/neo4j/core/UpdateableState.java | 16 + .../mapping/Neo4jEntityConverterImpl.java | 12 +- .../mapping/SourceStateTransmitter.java | 14 +- ...elBasedNodeTypeRepresentationStrategy.java | 20 +- 105 files changed, 11079 insertions(+), 50 deletions(-) create mode 100644 spring-data-neo4j-rest/src/main/java/org/neo4j/rest/graphdb/AbstractRemoteDatabase.java create mode 100644 spring-data-neo4j-rest/src/main/java/org/neo4j/rest/graphdb/CypherRestShell.java create mode 100644 spring-data-neo4j-rest/src/main/java/org/neo4j/rest/graphdb/ExecutingRestRequest.java create mode 100644 spring-data-neo4j-rest/src/main/java/org/neo4j/rest/graphdb/PropertiesMap.java create mode 100644 spring-data-neo4j-rest/src/main/java/org/neo4j/rest/graphdb/RequestResult.java create mode 100644 spring-data-neo4j-rest/src/main/java/org/neo4j/rest/graphdb/RestAPI.java create mode 100644 spring-data-neo4j-rest/src/main/java/org/neo4j/rest/graphdb/RestAPICypherImpl.java create mode 100644 spring-data-neo4j-rest/src/main/java/org/neo4j/rest/graphdb/RestAPIImpl.java create mode 100644 spring-data-neo4j-rest/src/main/java/org/neo4j/rest/graphdb/RestAPIIndex.java create mode 100644 spring-data-neo4j-rest/src/main/java/org/neo4j/rest/graphdb/RestAPIInternal.java create mode 100644 spring-data-neo4j-rest/src/main/java/org/neo4j/rest/graphdb/RestGraphDatabase.java create mode 100644 spring-data-neo4j-rest/src/main/java/org/neo4j/rest/graphdb/RestRequest.java create mode 100644 spring-data-neo4j-rest/src/main/java/org/neo4j/rest/graphdb/RestResultException.java create mode 100644 spring-data-neo4j-rest/src/main/java/org/neo4j/rest/graphdb/UpdatableRestResult.java create mode 100644 spring-data-neo4j-rest/src/main/java/org/neo4j/rest/graphdb/UserAgent.java create mode 100644 spring-data-neo4j-rest/src/main/java/org/neo4j/rest/graphdb/batch/BatchRestAPI.java create mode 100644 spring-data-neo4j-rest/src/main/java/org/neo4j/rest/graphdb/batch/RecordingRestRequest.java create mode 100644 spring-data-neo4j-rest/src/main/java/org/neo4j/rest/graphdb/batch/RestOperations.java create mode 100644 spring-data-neo4j-rest/src/main/java/org/neo4j/rest/graphdb/converter/ConversionInfo.java create mode 100644 spring-data-neo4j-rest/src/main/java/org/neo4j/rest/graphdb/converter/RelationshipIterableConverter.java create mode 100644 spring-data-neo4j-rest/src/main/java/org/neo4j/rest/graphdb/converter/RestEntityExtractor.java create mode 100644 spring-data-neo4j-rest/src/main/java/org/neo4j/rest/graphdb/converter/RestEntityPropertyRefresher.java create mode 100644 spring-data-neo4j-rest/src/main/java/org/neo4j/rest/graphdb/converter/RestIndexHitsConverter.java create mode 100644 spring-data-neo4j-rest/src/main/java/org/neo4j/rest/graphdb/converter/RestResultConverter.java create mode 100644 spring-data-neo4j-rest/src/main/java/org/neo4j/rest/graphdb/converter/RestTableResultExtractor.java create mode 100644 spring-data-neo4j-rest/src/main/java/org/neo4j/rest/graphdb/converter/ResultTypeConverter.java create mode 100644 spring-data-neo4j-rest/src/main/java/org/neo4j/rest/graphdb/converter/TypeInformation.java create mode 100644 spring-data-neo4j-rest/src/main/java/org/neo4j/rest/graphdb/entity/RestEntity.java create mode 100644 spring-data-neo4j-rest/src/main/java/org/neo4j/rest/graphdb/entity/RestEntityCache.java create mode 100644 spring-data-neo4j-rest/src/main/java/org/neo4j/rest/graphdb/entity/RestNode.java create mode 100644 spring-data-neo4j-rest/src/main/java/org/neo4j/rest/graphdb/entity/RestRelationship.java create mode 100644 spring-data-neo4j-rest/src/main/java/org/neo4j/rest/graphdb/index/IndexInfo.java create mode 100644 spring-data-neo4j-rest/src/main/java/org/neo4j/rest/graphdb/index/RestAutoIndexer.java create mode 100644 spring-data-neo4j-rest/src/main/java/org/neo4j/rest/graphdb/index/RestIndex.java create mode 100644 spring-data-neo4j-rest/src/main/java/org/neo4j/rest/graphdb/index/RestIndexManager.java create mode 100644 spring-data-neo4j-rest/src/main/java/org/neo4j/rest/graphdb/index/RestNodeIndex.java create mode 100644 spring-data-neo4j-rest/src/main/java/org/neo4j/rest/graphdb/index/RestRelationshipAutoIndexer.java create mode 100644 spring-data-neo4j-rest/src/main/java/org/neo4j/rest/graphdb/index/RestRelationshipIndex.java create mode 100644 spring-data-neo4j-rest/src/main/java/org/neo4j/rest/graphdb/index/RetrievedIndexInfo.java create mode 100644 spring-data-neo4j-rest/src/main/java/org/neo4j/rest/graphdb/index/SimpleIndexHits.java create mode 100644 spring-data-neo4j-rest/src/main/java/org/neo4j/rest/graphdb/query/CypherRestResult.java create mode 100644 spring-data-neo4j-rest/src/main/java/org/neo4j/rest/graphdb/query/CypherResult.java create mode 100644 spring-data-neo4j-rest/src/main/java/org/neo4j/rest/graphdb/query/CypherTransaction.java create mode 100644 spring-data-neo4j-rest/src/main/java/org/neo4j/rest/graphdb/query/CypherTxResult.java create mode 100644 spring-data-neo4j-rest/src/main/java/org/neo4j/rest/graphdb/query/QueryEngine.java create mode 100644 spring-data-neo4j-rest/src/main/java/org/neo4j/rest/graphdb/query/RestCypherQueryEngine.java create mode 100644 spring-data-neo4j-rest/src/main/java/org/neo4j/rest/graphdb/query/RestQueryResult.java create mode 100644 spring-data-neo4j-rest/src/main/java/org/neo4j/rest/graphdb/transaction/NullTransaction.java create mode 100644 spring-data-neo4j-rest/src/main/java/org/neo4j/rest/graphdb/transaction/NullTransactionManager.java create mode 100644 spring-data-neo4j-rest/src/main/java/org/neo4j/rest/graphdb/transaction/RemoteCypherTransaction.java create mode 100644 spring-data-neo4j-rest/src/main/java/org/neo4j/rest/graphdb/traversal/NodePath.java create mode 100644 spring-data-neo4j-rest/src/main/java/org/neo4j/rest/graphdb/traversal/RelationshipPath.java create mode 100644 spring-data-neo4j-rest/src/main/java/org/neo4j/rest/graphdb/traversal/RestDirection.java create mode 100644 spring-data-neo4j-rest/src/main/java/org/neo4j/rest/graphdb/traversal/RestPathParser.java create mode 100644 spring-data-neo4j-rest/src/main/java/org/neo4j/rest/graphdb/traversal/RestTraversal.java create mode 100644 spring-data-neo4j-rest/src/main/java/org/neo4j/rest/graphdb/traversal/RestTraversalDescription.java create mode 100644 spring-data-neo4j-rest/src/main/java/org/neo4j/rest/graphdb/traversal/RestTraverser.java create mode 100644 spring-data-neo4j-rest/src/main/java/org/neo4j/rest/graphdb/traversal/SimplePath.java create mode 100644 spring-data-neo4j-rest/src/main/java/org/neo4j/rest/graphdb/util/ArrayConverter.java create mode 100644 spring-data-neo4j-rest/src/main/java/org/neo4j/rest/graphdb/util/Config.java create mode 100644 spring-data-neo4j-rest/src/main/java/org/neo4j/rest/graphdb/util/ConvertedResult.java create mode 100644 spring-data-neo4j-rest/src/main/java/org/neo4j/rest/graphdb/util/DefaultConverter.java create mode 100644 spring-data-neo4j-rest/src/main/java/org/neo4j/rest/graphdb/util/Handler.java create mode 100644 spring-data-neo4j-rest/src/main/java/org/neo4j/rest/graphdb/util/JsonHelper.java create mode 100644 spring-data-neo4j-rest/src/main/java/org/neo4j/rest/graphdb/util/QueryResult.java create mode 100644 spring-data-neo4j-rest/src/main/java/org/neo4j/rest/graphdb/util/QueryResultBuilder.java create mode 100644 spring-data-neo4j-rest/src/main/java/org/neo4j/rest/graphdb/util/ResourceIterableWrapper.java create mode 100644 spring-data-neo4j-rest/src/main/java/org/neo4j/rest/graphdb/util/ResultConverter.java create mode 100644 spring-data-neo4j-rest/src/main/java/org/neo4j/rest/graphdb/util/StreamJsonHelper.java create mode 100644 spring-data-neo4j-rest/src/main/java/org/neo4j/rest/graphdb/util/TestHelper.java create mode 100644 spring-data-neo4j-rest/src/main/java/org/neo4j/rest/graphdb/util/WrappingResourceIterator.java create mode 100644 spring-data-neo4j-rest/src/test/java/org/neo4j/rest/graphdb/CypherPlugin.java create mode 100644 spring-data-neo4j-rest/src/test/java/org/neo4j/rest/graphdb/EmptyGraphTest.java create mode 100644 spring-data-neo4j-rest/src/test/java/org/neo4j/rest/graphdb/HelloWorldService.java create mode 100644 spring-data-neo4j-rest/src/test/java/org/neo4j/rest/graphdb/IsRelationshipToNodeMatcher.java create mode 100644 spring-data-neo4j-rest/src/test/java/org/neo4j/rest/graphdb/LocalTestServer.java create mode 100644 spring-data-neo4j-rest/src/test/java/org/neo4j/rest/graphdb/MatrixDataGraph.java create mode 100644 spring-data-neo4j-rest/src/test/java/org/neo4j/rest/graphdb/MatrixDatabaseRestTest.java create mode 100644 spring-data-neo4j-rest/src/test/java/org/neo4j/rest/graphdb/MatrixDatabaseTest.java create mode 100644 spring-data-neo4j-rest/src/test/java/org/neo4j/rest/graphdb/Neo4jDatabaseCleaner.java create mode 100644 spring-data-neo4j-rest/src/test/java/org/neo4j/rest/graphdb/RelationshipHasMatcher.java create mode 100644 spring-data-neo4j-rest/src/test/java/org/neo4j/rest/graphdb/RestAPITest.java create mode 100644 spring-data-neo4j-rest/src/test/java/org/neo4j/rest/graphdb/RestAutoIndexTest.java create mode 100644 spring-data-neo4j-rest/src/test/java/org/neo4j/rest/graphdb/RestCypherQueryEngineTest.java create mode 100644 spring-data-neo4j-rest/src/test/java/org/neo4j/rest/graphdb/RestEntityTest.java create mode 100644 spring-data-neo4j-rest/src/test/java/org/neo4j/rest/graphdb/RestGraphDbTest.java create mode 100644 spring-data-neo4j-rest/src/test/java/org/neo4j/rest/graphdb/RestIndexTest.java create mode 100644 spring-data-neo4j-rest/src/test/java/org/neo4j/rest/graphdb/RestNodeTest.java create mode 100644 spring-data-neo4j-rest/src/test/java/org/neo4j/rest/graphdb/RestTestBase.java create mode 100644 spring-data-neo4j-rest/src/test/java/org/neo4j/rest/graphdb/RestTraversalExecutionTest.java create mode 100644 spring-data-neo4j-rest/src/test/java/org/neo4j/rest/graphdb/RestTraversalTest.java create mode 100644 spring-data-neo4j-rest/src/test/java/org/neo4j/rest/graphdb/ResultTypeConverterTest.java create mode 100644 spring-data-neo4j-rest/src/test/java/org/neo4j/rest/graphdb/SimpleTransactionTest.java create mode 100644 spring-data-neo4j-rest/src/test/java/org/neo4j/rest/graphdb/Type.java create mode 100644 spring-data-neo4j-rest/src/test/java/org/neo4j/rest/graphdb/TypeInformationTest.java create mode 100644 spring-data-neo4j-rest/src/test/java/org/neo4j/rest/graphdb/TypeInformationTestInterface.java create mode 100644 spring-data-neo4j-rest/src/test/java/org/neo4j/rest/graphdb/extension/HelloWorldResource.java create mode 100644 spring-data-neo4j-rest/src/test/java/org/neo4j/rest/graphdb/query/CypherTransactionTest.java create mode 100644 spring-data-neo4j-rest/src/test/java/org/neo4j/rest/graphdb/traversal/RestPathParserTest.java create mode 100644 spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/UpdateableState.java diff --git a/spring-data-neo4j-rest/pom.xml b/spring-data-neo4j-rest/pom.xml index 38951b14f..8dda616ee 100644 --- a/spring-data-neo4j-rest/pom.xml +++ b/spring-data-neo4j-rest/pom.xml @@ -65,7 +65,6 @@ org.neo4j neo4j-lucene-index ${neo4j.version} - test @@ -117,26 +116,6 @@ test - - org.neo4j - neo4j-rest-graphdb - ${neo4j-rest-graphdb.version} - - - org.neo4j - neo4j-kernel - - - org.neo4j - neo4j-lucene-index - - - org.neo4j - server-api - - - - org.neo4j.app neo4j-server diff --git a/spring-data-neo4j-rest/src/main/java/org/neo4j/rest/graphdb/AbstractRemoteDatabase.java b/spring-data-neo4j-rest/src/main/java/org/neo4j/rest/graphdb/AbstractRemoteDatabase.java new file mode 100644 index 000000000..9db4c9bc8 --- /dev/null +++ b/spring-data-neo4j-rest/src/main/java/org/neo4j/rest/graphdb/AbstractRemoteDatabase.java @@ -0,0 +1,73 @@ +/** + * Copyright (c) 2002-2013 "Neo Technology," + * Network Engine for Objects in Lund AB [http://neotechnology.com] + * + * This file is part of Neo4j. + * + * Neo4j 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. + * + * This program 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 this program. If not, see . + */ +package org.neo4j.rest.graphdb; + +import org.neo4j.graphdb.*; +import org.neo4j.graphdb.event.KernelEventHandler; +import org.neo4j.graphdb.event.TransactionEventHandler; +import org.neo4j.kernel.GraphDatabaseAPI; +import org.neo4j.kernel.IdGeneratorFactory; +import org.neo4j.kernel.KernelData; +import org.neo4j.kernel.TransactionBuilder; +import org.neo4j.kernel.guard.Guard; +import org.neo4j.kernel.impl.core.KernelPanicEventGenerator; +import org.neo4j.kernel.impl.core.NodeManager; +import org.neo4j.kernel.impl.nioneo.store.StoreId; +import org.neo4j.kernel.impl.persistence.PersistenceSource; +import org.neo4j.kernel.impl.transaction.LockManager; +import org.neo4j.kernel.impl.transaction.XaDataSourceManager; +import org.neo4j.kernel.impl.transaction.xaframework.TxIdGenerator; +import org.neo4j.kernel.impl.util.StringLogger; +import org.neo4j.kernel.info.DiagnosticsManager; +import org.neo4j.rest.graphdb.transaction.NullTransaction; + +import javax.transaction.TransactionManager; +import java.util.Collection; + +abstract class AbstractRemoteDatabase implements GraphDatabaseAPI { + public Transaction beginTx() { + return new NullTransaction(); + } + + public TransactionEventHandler registerTransactionEventHandler( TransactionEventHandler tTransactionEventHandler ) { + throw new UnsupportedOperationException(); + } + + public TransactionEventHandler unregisterTransactionEventHandler( TransactionEventHandler tTransactionEventHandler ) { + throw new UnsupportedOperationException(); + } + + public KernelEventHandler registerKernelEventHandler( KernelEventHandler kernelEventHandler ) { + throw new UnsupportedOperationException(); + } + + public KernelEventHandler unregisterKernelEventHandler( KernelEventHandler kernelEventHandler ) { + throw new UnsupportedOperationException(); + } + + @Override + public TransactionBuilder tx() { + throw new UnsupportedOperationException(); + } + + @Override + public void shutdown() { + } +} diff --git a/spring-data-neo4j-rest/src/main/java/org/neo4j/rest/graphdb/CypherRestShell.java b/spring-data-neo4j-rest/src/main/java/org/neo4j/rest/graphdb/CypherRestShell.java new file mode 100644 index 000000000..fecd8a64c --- /dev/null +++ b/spring-data-neo4j-rest/src/main/java/org/neo4j/rest/graphdb/CypherRestShell.java @@ -0,0 +1,55 @@ +/** + * Copyright (c) 2002-2013 "Neo Technology," + * Network Engine for Objects in Lund AB [http://neotechnology.com] + * + * This file is part of Neo4j. + * + * Neo4j 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. + * + * This program 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 this program. If not, see . + */ +package org.neo4j.rest.graphdb; + +import org.neo4j.rest.graphdb.query.CypherRestResult; +import org.neo4j.rest.graphdb.query.CypherResult; + +import java.io.BufferedReader; +import java.io.IOException; +import java.io.InputStreamReader; +import java.util.List; + +public class CypherRestShell { + public static void main(String[] args) throws IOException { + String uri = (args.length>0) ? args[0] : "http://localhost:7474/db/data"; + RestAPIImpl restAPIFacade = args.length>1 ? new RestAPIImpl(uri,args[1],args[2]) : new RestAPIImpl(uri); + System.out.println("Connected to "+uri); + try { + BufferedReader reader = new BufferedReader(new InputStreamReader(System.in)); + String query; + System.out.print("Query: "); + while ((query=reader.readLine())!=null && !query.isEmpty()) { + long time=System.currentTimeMillis(); + CypherResult result = restAPIFacade.query(query, null); + time=System.currentTimeMillis()-time; + System.out.println(result.getColumns()); + List> rows = (List>) result.getData(); + for (List row : rows) { + System.out.println(row); + } + System.out.println(rows.size()+" row(s), roundtrip time "+time+" ms."); + System.out.print("Query: "); + } + } finally { + restAPIFacade.close(); + } + } +} diff --git a/spring-data-neo4j-rest/src/main/java/org/neo4j/rest/graphdb/ExecutingRestRequest.java b/spring-data-neo4j-rest/src/main/java/org/neo4j/rest/graphdb/ExecutingRestRequest.java new file mode 100644 index 000000000..1bfd8e4e7 --- /dev/null +++ b/spring-data-neo4j-rest/src/main/java/org/neo4j/rest/graphdb/ExecutingRestRequest.java @@ -0,0 +1,184 @@ +/** + * Copyright (c) 2002-2013 "Neo Technology," + * Network Engine for Objects in Lund AB [http://neotechnology.com] + * + * This file is part of Neo4j. + * + * Neo4j 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. + * + * This program 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 this program. If not, see . + */ +package org.neo4j.rest.graphdb; + +import com.sun.jersey.api.client.Client; +import com.sun.jersey.api.client.ClientResponse; +import com.sun.jersey.api.client.WebResource; +import com.sun.jersey.api.client.WebResource.Builder; +import com.sun.jersey.api.client.filter.HTTPBasicAuthFilter; +import com.sun.jersey.api.client.filter.LoggingFilter; +import org.neo4j.helpers.collection.MapUtil; +import org.neo4j.rest.graphdb.util.Config; +import org.neo4j.rest.graphdb.util.JsonHelper; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import javax.ws.rs.core.MediaType; +import java.io.InputStream; +import java.io.UnsupportedEncodingException; +import java.net.URI; +import java.net.URISyntaxException; +import java.net.URLEncoder; +import java.util.Map; + +import static javax.ws.rs.core.MediaType.APPLICATION_JSON_TYPE; + +public class ExecutingRestRequest implements RestRequest { + + private static final Logger log = LoggerFactory.getLogger(RestRequest.class); + + public static final MediaType STREAMING_JSON_TYPE = new MediaType(APPLICATION_JSON_TYPE.getType(),APPLICATION_JSON_TYPE.getSubtype(), MapUtil.stringMap("stream","true")); + private final String baseUri; + private final UserAgent userAgent = new UserAgent(); + private final Client client; + + public ExecutingRestRequest( String baseUri ) { + this( baseUri, null, null ); + } + + public ExecutingRestRequest( String baseUri, String username, String password ) { + this.baseUri = uriWithoutSlash( baseUri ); + client = createClient(); + addAuthFilter(username, password); + + } + + protected void addAuthFilter(String username, String password) { + if (username == null) return; + client.addFilter( new HTTPBasicAuthFilter( username, password ) ); + } + + protected Client createClient() { + Client client = Client.create(); + client.setConnectTimeout(Config.getConnectTimeout()); + client.setReadTimeout(Config.getReadTimeout()); + client.setChunkedEncodingSize(8*1024); + userAgent.install(client); + if (Config.useLoggingFilter()) { + client.addFilter(new LoggingFilter()); + } + return client; + } + + private ExecutingRestRequest( String uri, Client client ) { + this.baseUri = uriWithoutSlash( uri ); + this.client = client; + } + + protected String uriWithoutSlash( String uri ) { + return (uri.endsWith("/") ? uri.substring(0, uri.length() - 1) : uri); + } + + public static String encode( Object value ) { + if ( value == null ) return ""; + try { + return URLEncoder.encode( value.toString(), "utf-8" ).replaceAll( "\\+", "%20" ); + } catch ( UnsupportedEncodingException e ) { + throw new RuntimeException( e ); + } + } + + + private Builder builder( String path ) { + WebResource resource = client.resource( uri( pathOrAbsolute( path ) ) ); + if (Config.streamingIsEnabled()) return resource.accept(STREAMING_JSON_TYPE).header("X-Stream","true"); + return resource.accept(APPLICATION_JSON_TYPE); + } + + private String pathOrAbsolute( String path ) { + if ( path.startsWith( "http://" ) ) return path; + return baseUri + "/" + path; + } + + + @Override + public RequestResult get( String path ) { + if (log.isDebugEnabled()) log.debug("GET "+path); + return RequestResult.extractFrom(builder(path).get(ClientResponse.class)); + } + + + @Override + public RequestResult get( String path, Object data ) { + Builder builder = builder(path); + if ( data != null ) { + builder = builder.entity( JsonHelper.createJsonFrom( data ), APPLICATION_JSON_TYPE ); + } + if (log.isDebugEnabled()) log.debug("GET "+path+" "+data); + return RequestResult.extractFrom(builder.get(ClientResponse.class)); + } + + + @Override + public RequestResult delete(String path) { + if (log.isDebugEnabled()) log.debug("DELETE "+path); + return RequestResult.extractFrom(builder(path).delete(ClientResponse.class)); + } + + + @Override + public RequestResult post( String path, Object data ) { + Builder builder = builder( path ); + if ( data != null ) { + Object payload = data instanceof InputStream ? data : JsonHelper.createJsonFrom(data); + builder = builder.entity( payload , APPLICATION_JSON_TYPE ); + } + if (log.isDebugEnabled()) log.debug("POST "+path+" "+data); + return RequestResult.extractFrom(builder.post(ClientResponse.class)); + } + + @Override + public RequestResult put( String path, Object data ) { + Builder builder = builder( path ); + if ( data != null ) { + builder = builder.entity( JsonHelper.createJsonFrom( data ), APPLICATION_JSON_TYPE ); + } + if (log.isDebugEnabled()) log.debug("PUT "+path+" "+data); + return RequestResult.extractFrom(builder.put(ClientResponse.class)); + } + + @Override + public RestRequest with( String uri ) { + return new ExecutingRestRequest(uri, client); + } + + private URI uri( String uri ) { + try { + return new URI( uri ); + } catch ( URISyntaxException e ) { + throw new RuntimeException( e ); + } + } + + + @Override + public String getUri() { + return baseUri; + } + + @Override + public Map toMap(RequestResult requestResult) { + return requestResult.toMap(); + } + + public static void shutdown() { + } +} diff --git a/spring-data-neo4j-rest/src/main/java/org/neo4j/rest/graphdb/PropertiesMap.java b/spring-data-neo4j-rest/src/main/java/org/neo4j/rest/graphdb/PropertiesMap.java new file mode 100644 index 000000000..d5945f3c4 --- /dev/null +++ b/spring-data-neo4j-rest/src/main/java/org/neo4j/rest/graphdb/PropertiesMap.java @@ -0,0 +1,159 @@ +/** + * Copyright (c) 2002-2013 "Neo Technology," + * Network Engine for Objects in Lund AB [http://neotechnology.com] + * + * This file is part of Neo4j. + * + * Neo4j 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. + * + * This program 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 this program. If not, see . + */ +package org.neo4j.rest.graphdb; + +/** + * @author mh + * @since 13.12.10 + */ + +import org.neo4j.graphdb.PropertyContainer; + +import java.lang.reflect.Array; +import java.util.*; + +public class PropertiesMap { + + private final Map values = new HashMap(); + + public PropertiesMap( PropertyContainer container ) { + for ( String key : container.getPropertyKeys() ) { + values.put( key, container.getProperty( key ) ); + } + } + + public PropertiesMap( Map map ) { + for ( Map.Entry entry : map.entrySet() ) { + values.put( entry.getKey(), toInternalType( entry.getValue() ) ); + } + } + + public Object getValue( String key ) { + return values.get( key ); + } + + public Map serialize() { + // TODO Nice with sorted, but TreeMap the best? + Map result = new TreeMap(); + for ( Map.Entry entry : values.entrySet() ) { + result.put( entry.getKey(), toSerializedType( entry.getValue() ) ); + } + return result; + } + + void storeTo( PropertyContainer container ) { + for ( Map.Entry entry : values.entrySet() ) { + container.setProperty( entry.getKey(), entry.getValue() ); + } + } + + @SuppressWarnings("unchecked") + private static Object toInternalType( Object value ) { + if ( value instanceof List ) { + List list = (List) value; + if ( list.isEmpty() ) { + return new byte[0]; + } else { + Object first = list.get( 0 ); + if ( first instanceof String ) { + return stringArray( list ); + } else if ( first instanceof Number ) { + return numberArray( list ); + } else if ( first instanceof Boolean ) { + return booleanArray( list ); + } else { + throw new RuntimeException( "Unsupported array type " + first.getClass() + + ". Supported array types are arrays of all java primitives (" + + "byte[], char[], short[], int[], long[], float[], double[]) " + + "and String[]" ); + } + } + } else { + return assertSupportedPropertyValue( value ); + } + } + + public static Object assertSupportedPropertyValue( Object value ) { + if ( value == null ) { + throw new RuntimeException( "null value not supported" ); + } + final Class type = value.getClass(); + if (isSupportedType(type) || type.isArray() && isSupportedType(type.getComponentType())) { + return value; + } + throw new RuntimeException( "Unsupported value type " + type + "." + + " Supported value types are all java primitives (byte, char, short, int, " + + "long, float, double) and String, as well as arrays of all those types" ); + } + + private static boolean isSupportedType(Class type) { + return type.isPrimitive() || String.class.isAssignableFrom(type) || Number.class.isAssignableFrom(type) || Boolean.class.isAssignableFrom(type); + } + + private static Boolean[] booleanArray( List list ) { + return list.toArray( new Boolean[list.size()] ); + } + + private static Number[] numberArray( List numbers ) { + Number[] internal = new Number[numbers.size()]; + for ( int i = 0; i < internal.length; i++ ) { + Number number = numbers.get( i ); + if ( number instanceof Float || number instanceof Double ) { + number = number.doubleValue(); + } else { + number = number.longValue(); + } + internal[i] = number; + } + final Number[] result; + if ( internal[0] instanceof Double ) { + result = new Double[internal.length]; + } else { + result = new Long[internal.length]; + } + System.arraycopy( internal, 0, result, 0, internal.length ); + return result; + } + + private static String[] stringArray( List strings ) { + return strings.toArray( new String[strings.size()] ); + } + + private Object toSerializedType( Object value ) { + if ( value.getClass().isArray() ) { + if ( value.getClass().getComponentType().isPrimitive() ) { + int size = Array.getLength( value ); + List result = new ArrayList(); + for ( int i = 0; i < size; i++ ) { + result.add( Array.get( value, i ) ); + } + return result; + } else { + return Arrays.asList( (Object[]) value ); + } + } else { + return value; + } + } + + public boolean isEmpty() { + return values.isEmpty(); + } +} \ No newline at end of file diff --git a/spring-data-neo4j-rest/src/main/java/org/neo4j/rest/graphdb/RequestResult.java b/spring-data-neo4j-rest/src/main/java/org/neo4j/rest/graphdb/RequestResult.java new file mode 100644 index 000000000..0fad9b886 --- /dev/null +++ b/spring-data-neo4j-rest/src/main/java/org/neo4j/rest/graphdb/RequestResult.java @@ -0,0 +1,148 @@ +/** + * Copyright (c) 2002-2013 "Neo Technology," + * Network Engine for Objects in Lund AB [http://neotechnology.com] + * + * This file is part of Neo4j. + * + * Neo4j 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. + * + * This program 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 this program. If not, see . + */ +package org.neo4j.rest.graphdb; + +import java.io.IOException; +import java.io.InputStream; +import java.net.URI; +import java.util.Map; + +import javax.ws.rs.core.Response; +import javax.ws.rs.core.Response.StatusType; + +import org.neo4j.rest.graphdb.util.JsonHelper; + +import com.sun.jersey.api.client.ClientResponse; +import org.neo4j.rest.graphdb.util.StreamJsonHelper; + + +/** +* @author Klemens Burchardi +* @since 03.08.11 +*/ +public class RequestResult { + private final int status; + private final String location; + private ClientResponse response; + private String string; + private Object entity; + private InputStream stream; + + RequestResult(int status, String location, String string) { + this.status = status; + this.location = location; + this.string = string; + this.stream = null; + } + + RequestResult(int status, String location, InputStream stream, ClientResponse response) { + this.status = status; + this.location = location; + this.response = response; + this.entity = null; + this.stream = stream; + } + + public static RequestResult extractFrom(ClientResponse clientResponse) { + final int status = clientResponse.getStatus(); + final URI location = clientResponse.getLocation(); + // final InputStream data; + if (status == Response.Status.NO_CONTENT.getStatusCode()) { + // data = null; + clientResponse.close(); + return new RequestResult(status, uriString(location), null,clientResponse); + } else { + // data = clientResponse.getEntityInputStream(); + RequestResult result = new RequestResult(status, uriString(location), clientResponse.getEntity(String.class)); + clientResponse.close(); + return result; + } + //return new RequestResult(status, uriString(location), data,clientResponse); + } + + public static RequestResult extractFrom(Map batchResult) { + return new RequestResult(200, (String) batchResult.get("location"),JsonHelper.createJsonFrom(batchResult.get("body"))); + } + + private static String uriString(URI location) { + return location==null ? null : location.toString(); + } + + + public int getStatus() { + return status; + } + + public String getLocation() { + return location; + } + + public Object toEntity() { + if (entity!=null) return entity; + if (stream != null) { + entity = StreamJsonHelper.jsonToSingleValue(stream); + closeStream(); + } + else { + entity = JsonHelper.jsonToSingleValue(string); + } + return entity; + } + + public boolean isMap() { + return toEntity() instanceof Map; + } + public Map toMap() { + return (Map) toEntity(); + } + + public boolean statusIs( StatusType status ) { + return getStatus() == status.getStatusCode(); + } + + public boolean statusOtherThan( StatusType status ) { + return !statusIs(status ); + } + + public String getText() { + if (string==null && stream!=null) { + string = JsonHelper.readString(stream); + closeStream(); + } + return string; + } + + private void closeStream() { + if (stream!=null) readFully(stream); + stream = null; + if (response!=null) { + response.close(); + response = null; + } + } + + private void readFully(InputStream stream) { + try { + while (stream.read()!=-1); + } catch (IOException e) { + // ignore + } + } +} diff --git a/spring-data-neo4j-rest/src/main/java/org/neo4j/rest/graphdb/RestAPI.java b/spring-data-neo4j-rest/src/main/java/org/neo4j/rest/graphdb/RestAPI.java new file mode 100644 index 000000000..120192a0d --- /dev/null +++ b/spring-data-neo4j-rest/src/main/java/org/neo4j/rest/graphdb/RestAPI.java @@ -0,0 +1,92 @@ +/** + * Copyright (c) 2002-2013 "Neo Technology," + * Network Engine for Objects in Lund AB [http://neotechnology.com] + * + * This file is part of Neo4j. + * + * Neo4j 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. + * + * This program 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 this program. If not, see . + */ +package org.neo4j.rest.graphdb; + +import org.neo4j.graphdb.*; +import org.neo4j.graphdb.traversal.TraversalDescription; +import org.neo4j.rest.graphdb.query.CypherRestResult; +import org.neo4j.rest.graphdb.entity.RestEntity; +import org.neo4j.rest.graphdb.entity.RestNode; +import org.neo4j.rest.graphdb.entity.RestRelationship; +import org.neo4j.rest.graphdb.traversal.RestTraverser; +import org.neo4j.rest.graphdb.util.QueryResult; +import org.neo4j.rest.graphdb.util.ResultConverter; + +import java.util.Collection; +import java.util.Map; + +/** + * @author mh + * @since 02.05.12 + */ +public interface RestAPI extends RestAPIIndex, RestAPIInternal { + + void deleteEntity(RestEntity entity); + + void setPropertyOnEntity(RestEntity entity, String key, Object value); + void setPropertiesOnEntity(RestEntity restEntity, Map propertyData); +// Map getData(RestEntity uri); + void removeProperty(RestEntity entity, String key); + + RestNode getNodeById(long id); + + RestNode createNode(Map props); + RestNode createNode(Map props,Collection labels); + + RestRelationship getRelationshipById(long id); + RestRelationship createRelationship(Node startNode, Node endNode, RelationshipType type, Map props); + + Iterable getRelationshipTypes(RestNode node); + int getDegree(RestNode restNode, RelationshipType type, Direction direction); + + void addLabels(RestNode node, Collection labels); + void removeLabel(RestNode node, String label); + + Iterable getNodesByLabel(String label); + Iterable getNodesByLabelAndProperty(String label, String property, Object value); + + org.neo4j.rest.graphdb.query.CypherResult query(String statement, Map params); + QueryResult> query(String statement, Map params, ResultConverter resultConverter); + + Transaction beginTx(); + + Collection getAllLabelNames(); + + Iterable getRelationshipTypes(); + + TraversalDescription createTraversalDescription(); + + Iterable getRelationships(RestNode restNode, Direction direction, RelationshipType... types); + + RestTraverser traverse(RestNode restNode, Map description); + + RestNode merge(String labelName, String key, Object value, Map properties, Collection labels); + + RequestResult batch(Collection> batchRequestData); + + // internal + + RestRequest getRestRequest(); + + RestNode addToCache(RestNode restNode); + RestNode getFromCache(long id); + + void close(); +} diff --git a/spring-data-neo4j-rest/src/main/java/org/neo4j/rest/graphdb/RestAPICypherImpl.java b/spring-data-neo4j-rest/src/main/java/org/neo4j/rest/graphdb/RestAPICypherImpl.java new file mode 100644 index 000000000..b5f16f5c5 --- /dev/null +++ b/spring-data-neo4j-rest/src/main/java/org/neo4j/rest/graphdb/RestAPICypherImpl.java @@ -0,0 +1,553 @@ +/** + * Copyright (c) 2002-2013 "Neo Technology," + * Network Engine for Objects in Lund AB [http://neotechnology.com] + * + * This file is part of Neo4j. + * + * Neo4j 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. + * + * This program 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 this program. If not, see . + */ +package org.neo4j.rest.graphdb; + +import org.neo4j.graphdb.*; +import org.neo4j.graphdb.index.IndexHits; +import org.neo4j.graphdb.traversal.TraversalDescription; +import org.neo4j.helpers.collection.IterableWrapper; +import org.neo4j.helpers.collection.IteratorUtil; +import org.neo4j.helpers.collection.MapUtil; +import org.neo4j.index.impl.lucene.AbstractIndexHits; +import org.neo4j.rest.graphdb.converter.RestEntityExtractor; +import org.neo4j.rest.graphdb.entity.RestEntity; +import org.neo4j.rest.graphdb.entity.RestNode; +import org.neo4j.rest.graphdb.entity.RestRelationship; +import org.neo4j.rest.graphdb.index.IndexInfo; +import org.neo4j.rest.graphdb.index.RestIndex; +import org.neo4j.rest.graphdb.index.RestIndexManager; +import org.neo4j.rest.graphdb.query.CypherResult; +import org.neo4j.rest.graphdb.query.CypherTransaction; +import org.neo4j.rest.graphdb.query.CypherTxResult; +import org.neo4j.rest.graphdb.query.RestQueryResult; +import org.neo4j.rest.graphdb.transaction.RemoteCypherTransaction; +import org.neo4j.rest.graphdb.traversal.RestTraverser; +import org.neo4j.rest.graphdb.util.QueryResult; +import org.neo4j.rest.graphdb.util.ResultConverter; + +import javax.ws.rs.core.Response.Status; +import java.util.*; + +import static org.neo4j.helpers.collection.MapUtil.map; + + +public class RestAPICypherImpl implements RestAPI { + + public static final String _QUERY_RETURN_NODE = " RETURN id(n) as id, labels(n) as labels, n as data"; + public static final String _QUERY_RETURN_REL = " RETURN id(r) as id, type(r) as type, r as data, id(startNode(r)) as start, id(endNode(r)) as end"; + public static String MATCH_NODE_QUERY(String name) { return " MATCH ("+name+") WHERE id("+name+") = {id_"+name+"} "; } + public static final String _MATCH_NODE_QUERY = " MATCH (n) WHERE id(n) = {id} "; + public static final String GET_NODE_QUERY = _MATCH_NODE_QUERY + _QUERY_RETURN_NODE; + public static final String _MATCH_REL_QUERY = " START r=rel({id}) "; + public static final String GET_REL_QUERY = _MATCH_REL_QUERY + _QUERY_RETURN_REL; + + public static final String GET_REL_TYPES_QUERY = _MATCH_NODE_QUERY + " MATCH (n)-[r]-() RETURN distinct type(r) as relType"; + + private String createNodeQuery(Collection labels) { + String labelString = toLabelString(labels); + return "CREATE (n" + labelString + " {props}) " + _QUERY_RETURN_NODE; + } + + private String mergeQuery(String labelName, String key, Collection labels) { + StringBuilder setLabels = new StringBuilder(); + if (labels!=null) { + for (String label : labels) { + if (label.equals(labelName)) continue; + setLabels.append("SET n:").append(label).append(" "); + } + } + return "MERGE (n:`"+labelName+"` {`"+key+"`: {value}}) ON CREATE SET n={props} "+setLabels+ _QUERY_RETURN_NODE; + } + + private String toLabelString(Collection labels) { + if (labels==null || labels.size() == 0) return ""; + StringBuilder sb = new StringBuilder(); + for (String label : labels) { + sb.append(":").append(label); + } + return sb.toString(); + } + + private final RestAPI restAPI; + private RestRequest restRequest; + + private static final ThreadLocal cypherTransaction = new ThreadLocal<>(); + + protected RestAPICypherImpl(RestAPI restAPI) { + this.restAPI = restAPI; + this.restRequest = restAPI.getRestRequest(); + } + + @Override + public RestNode getNodeById(long id, Load force) { + if (force != Load.ForceFromServer) { + RestNode restNode = getFromCache(id); + if (restNode != null) return restNode; + } + if (force == Load.FromCache) return new RestNode(RestNode.nodeUri(this, id),this); + Iterator> result = query(GET_NODE_QUERY, map("id", id)).getData().iterator(); + if (!result.hasNext()) { + throw new NotFoundException("Node not found " + id); + } + List row = result.next(); + return addToCache(toNode(row)); + } + + public RestNode getFromCache(long id) { + return restAPI.getFromCache(id); + } + + @Override + public RestNode getNodeById(long id) { + return getNodeById(id, Load.FromServer); + } + + private RestNode toNode(List row) { + long id = ((Number) row.get(0)).longValue(); + List labels = (List) row.get(1); + Map props = (Map) row.get(2); + return RestNode.fromCypher(id, labels, props, this); + } + + private RestRelationship toRel(List row) { + long id = ((Number) row.get(0)).longValue(); + String type = (String)row.get(1); + Map props = (Map) row.get(2); + long start = ((Number) row.get(3)).longValue(); + long end = ((Number) row.get(4)).longValue(); + return RestRelationship.fromCypher(id, type, props, start,end,this); + } + + @Override + public RestRelationship getRelationshipById(long id) { + Iterator> result = query(GET_REL_QUERY, map("id", id)).getData().iterator(); + if (!result.hasNext()) { + throw new NotFoundException("Relationship not found " + id); + } + List row = result.next(); + return toRel(row); + } + + + @Override + public RestNode createNode(Map props) { + return createNode(props,Collections.emptyList()); + } + @Override + public RestNode createNode(Map props, Collection labels) { + Map data = props == null ? Collections.emptyMap() : props; + Iterator> result = query(createNodeQuery(labels), map("props", data)).getData().iterator(); + if (result.hasNext()) { + return addToCache(toNode(result.next())); + } + throw new RuntimeException("Error creating node with labels: " + labels + " and props: " + props + " no data returned"); + } + + @Override + public RestNode merge(String labelName, String key, Object value, final Map nodeProperties, Collection labels) { + if (labelName ==null || key == null || value==null) throw new IllegalArgumentException("Label "+ labelName +" key "+key+" and value must not be null"); + Map props = nodeProperties.containsKey(key) ? nodeProperties : MapUtil.copyAndPut(nodeProperties, key, value); + Map params = map("props", props, "value", value); + Iterator> result = query(mergeQuery(labelName, key, labels), params).getData().iterator(); + if (!result.hasNext()) + throw new RuntimeException("Error merging node with labels: " + labelName + " key " + key + " value " + value + " labels " + labels+ " and props: " + props + " no data returned"); + + return addToCache(toNode(result.next())); + } + + public RestNode addToCache(RestNode restNode) { + return restAPI.addToCache(restNode); + } + + @Override + public RestRelationship createRelationship(Node startNode, Node endNode, RelationshipType type, Map props) { + String statement = MATCH_NODE_QUERY("n") + MATCH_NODE_QUERY("m") + " CREATE (n)-[r:`"+type.name()+"`]->(m) SET r={props} " + _QUERY_RETURN_REL; + Map params = map("id_n", startNode.getId(), "id_m", endNode.getId(), "props", props); + CypherTransaction.Result result = runQuery(statement, params); + if (!result.hasData()) throw new RuntimeException("Error creating relationship from "+startNode+" to "+endNode+" type "+type.name()); + Iterator> it = result.getRows().iterator(); + return toRel(it.next()); + } + + + @Override + public void removeLabel(RestNode node, String label) { + CypherTransaction.Result result = runQuery(_MATCH_NODE_QUERY + (" REMOVE n:`" + label + "` ") + _QUERY_RETURN_NODE, map("id", node.getId())); + if (!result.hasData()) { + throw new RuntimeException("Error removing label "+label+" from node "+node); + } + } + + @Override + public Iterable getNodesByLabel(String label) { + String statement = "MATCH (n:`" + label + "`) " + _QUERY_RETURN_NODE; + return queryForNodes(statement, null); + } + + private Iterable queryForNodes(String statement, Map params) { + Iterable> result = runQuery(statement, params).getRows(); + return new IterableWrapper>(result) { + protected RestNode underlyingObjectToObject(List row) { + return addToCache(toNode(row)); + } + }; + } + + @Override + public Iterable getNodesByLabelAndProperty(String label, String property, Object value) { + String statement = "MATCH (n:`" + label + "`) WHERE n.`"+property+"` = {value} " + _QUERY_RETURN_NODE; + return queryForNodes(statement, map("value", value)); + } + + @Override + public Iterable getRelationshipTypes(RestNode node) { + Iterable> result = runQuery(GET_REL_TYPES_QUERY, map("id", node.getId())).getRows(); + return new IterableWrapper>(result) { + protected RelationshipType underlyingObjectToObject(List row) { + return DynamicRelationshipType.withName(row.get(0).toString()); + } + }; + } + + @Override + public int getDegree(RestNode restNode, RelationshipType type, Direction direction) { + String nodeDegreeQuery = "MATCH (n)" + relPattern(direction,type) + "() WHERE id(n) = {id} RETURN count(*) as degree"; + Iterator> degree = runQuery(nodeDegreeQuery, map("id", restNode.getId())).getRows().iterator(); + if (!degree.hasNext()) return 0; + return ((Number)degree.next().get(0)).intValue(); + } + + private String relPattern(Direction direction, RelationshipType... types) { + String typeString = toTypeString(types); + String relPattern = "--"; + if (!typeString.isEmpty()) relPattern = "-[r "+typeString+"]-"; + if (direction == Direction.OUTGOING) { + relPattern += ">"; + } else if (direction == Direction.INCOMING) { + relPattern = "<" + relPattern; + } + return relPattern; + } + + private String toTypeString(RelationshipType... types) { + if (types==null || types.length == 0) return ""; + StringBuilder typeString = new StringBuilder(); + for (RelationshipType type : types) { + if (typeString.length() > 0 ) typeString.append("|"); + typeString.append(':').append('`').append(type.name()).append("`"); + } + return typeString.toString(); + } + + @Override + public Iterable getRelationships(RestNode restNode, Direction direction, RelationshipType... types) { + String statement = _MATCH_NODE_QUERY + " MATCH (n)"+relPattern(direction,types)+"() "+_QUERY_RETURN_REL; + CypherTransaction.Result result = runQuery(statement, map("id", restNode.getId())); + return new IterableWrapper>(result.getRows()) { + protected Relationship underlyingObjectToObject(List row) { + return toRel(row); + } + }; + } + + @Override + public void addLabels(RestNode node, Collection labels) { + String statement = _MATCH_NODE_QUERY + " SET n"+toLabelString(labels) + _QUERY_RETURN_NODE; + runQuery(statement,map("id",node.getId())); + RequestResult response = getRestRequest().with(node.getUri()).post("labels", labels); + + if (response.statusOtherThan(Status.NO_CONTENT)) { + throw new IllegalStateException("error adding labels, received " + response); + } + } + + public RestRequest getRestRequest() { + return restRequest; + } + + @Override + public Transaction beginTx() { + CypherTransaction tx = cypherTransaction.get(); + if (tx != null ) { + throw new IllegalStateException("Transaction already running "+tx); + } else { + cypherTransaction.set(newCypherTransaction()); + return new RemoteCypherTransaction(cypherTransaction); + } + } + + @Override + public IndexHits getIndex(Class entityType, String indexName, String key, Object value) { + String index = key == null ? ":`" + indexName + "`({query})" : ":`" + indexName + "`(`" + key + "`={query})"; + if (Node.class.isAssignableFrom(entityType)) { + String statement = "start n=node"+index+ _QUERY_RETURN_NODE; + CypherTransaction.Result result = runQuery(statement, map("query", value)); + return toIndexHits(result,true); + } + if (Relationship.class.isAssignableFrom(entityType)) { + String statement = "start r=rel"+index+ _QUERY_RETURN_REL; + CypherTransaction.Result result = runQuery(statement, map("query", value)); + return toIndexHits(result,false); + } + throw new IllegalStateException("Unknown index entity type "+entityType); + } + + @Override + public IndexHits queryIndex(Class entityType, String indexName, String key, Object value) { + String index = ":`" + indexName + "`({query})"; + if (Node.class.isAssignableFrom(entityType)) { + String statement = "start n=node"+index+ _QUERY_RETURN_NODE; + CypherTransaction.Result result = runQuery(statement, map("query", value)); + return toIndexHits(result,true); + } + if (Relationship.class.isAssignableFrom(entityType)) { + String statement = "start r=rel"+index+ _QUERY_RETURN_REL; + CypherTransaction.Result result = runQuery(statement, map("query", value)); + return toIndexHits(result,false); + } + throw new IllegalStateException("Unknown index entity type "+entityType); + } + + private IndexHits toIndexHits(CypherTransaction.Result result, final boolean isNode) { + final int size = IteratorUtil.count(result.getRows()); + final Iterator> it = result.getRows().iterator(); + return new AbstractIndexHits() { + @Override + public int size() { + return size; + } + + @Override + public float currentScore() { + return 0; + } + + @Override + protected S fetchNextOrNull() { + if (!it.hasNext()) return null; + return (S)(isNode ? addToCache(toNode(it.next())) : toRel(it.next())); + } + }; + } + + @Override + public RestIndexManager index() { + return restAPI.index(); + } + + + @Override + public void deleteEntity(RestEntity entity) { + if (entity instanceof Node) { + runQuery(_MATCH_NODE_QUERY + " DELETE n", map("id", entity.getId())); + } else if (entity instanceof Relationship) { + runQuery(_MATCH_REL_QUERY + " DELETE r", map("id", entity.getId())); + } + } + + @Override + public void setPropertyOnEntity(RestEntity entity, String key, Object value) { + if (entity instanceof Node) { + runQuery(_MATCH_NODE_QUERY + " SET n.`"+key+"` = {value} ", map("id", entity.getId(), "value", value)); + } else if (entity instanceof Relationship) { + runQuery(_MATCH_REL_QUERY + " SET r.`"+key+"` = {value} ", map("id", entity.getId(), "value", value)); + } + } + + // TODO return entity ??? + @Override + public void setPropertiesOnEntity(RestEntity entity, Map properties) { + if (entity instanceof Node) { + runQuery(_MATCH_NODE_QUERY + " SET n = {props} ", map("id", entity.getId(), "props", properties)); + } else if (entity instanceof Relationship) { + runQuery(_MATCH_REL_QUERY + " SET r = {props} ", map("id", entity.getId(), "props", properties)); + } + } + + @Override + public void removeProperty(RestEntity entity, String key) { + if (entity instanceof Node) { + runQuery(_MATCH_NODE_QUERY + " REMOVE n.`"+key+"`", map("id", entity.getId())); + } else if (entity instanceof Relationship) { + runQuery(_MATCH_REL_QUERY + " REMOVE r.`"+key+"`", map("id", entity.getId())); + } + } + + // todo handle within cypher tx + @Override + public RestNode getOrCreateNode(RestIndex index, String key, Object value, final Map properties, Collection labels) { + return restAPI.getOrCreateNode(index,key,value,properties,labels); + } + + // todo handle within cypher tx + @Override + public RestRelationship getOrCreateRelationship(RestIndex index, String key, Object value, final RestNode start, final RestNode end, final String type, final Map properties) { + return restAPI.getOrCreateRelationship(index,key,value,start,end,type,properties); + } + + public CypherResult query(String statement, Map params) { + return new CypherTxResult(runQuery(statement, params)); + } + + private CypherTransaction.Result runQuery(String statement, Map params) { + if (cypherTransaction.get() == null) { + return newCypherTransaction().commit(statement,params); + } + return cypherTransaction.get().send(statement,params); + } + + private CypherTransaction newCypherTransaction() { + return new CypherTransaction(this, CypherTransaction.ResultType.row); + } + + public QueryResult> query(String statement, Map params, ResultConverter resultConverter) { + final CypherResult result = query(statement, params); + if (RestResultException.isExceptionResult(result.asMap())) throw new RestResultException(result.asMap()); + return RestQueryResult.toQueryResult(result, this, resultConverter); + } + + @Override + public RestTraverser traverse(RestNode restNode, Map description) { + return restAPI.traverse(restNode, description); + } + + public RequestResult batch(Collection> batchRequestData) { + return restAPI.batch(batchRequestData); + } + + @Override + @SuppressWarnings("unchecked") + public RestIndex getIndex(String indexName) { + return restAPI.getIndex(indexName); + } + + @Override + @SuppressWarnings("unchecked") + public void createIndex(String type, String indexName, Map config) { + restAPI.createIndex(type, indexName, config); + } + + @Override + @SuppressWarnings("unchecked") + public RestIndex createIndex(Class type, String indexName, Map config) { + return restAPI.createIndex(type,indexName,config); + } + + @Override + public void close() { + restAPI.close(); + } + + @Override + public boolean isAutoIndexingEnabled(Class clazz) { + return restAPI.isAutoIndexingEnabled(clazz); + } + + @Override + public void setAutoIndexingEnabled(Class clazz, boolean enabled) { + restAPI.setAutoIndexingEnabled(clazz, enabled); + } + + @Override + public Set getAutoIndexedProperties(Class forClass) { + return restAPI.getAutoIndexedProperties(forClass); + } + + @Override + public void startAutoIndexingProperty(Class forClass, String s) { + restAPI.startAutoIndexingProperty(forClass,s); + } + + @Override + public void stopAutoIndexingProperty(Class forClass, String s) { + restAPI.stopAutoIndexingProperty(forClass, s); + } + + @Override + public void delete(RestIndex index) { + restAPI.delete(index); + } + + @Override + public void removeFromIndex(RestIndex index, T entity, String key, Object value) { + restAPI.removeFromIndex(index, entity, key, value); + } + + @Override + public void removeFromIndex(RestIndex index, T entity, String key) { + restAPI.removeFromIndex(index,entity,key); + } + + @Override + public void removeFromIndex(RestIndex index, T entity) { + restAPI.removeFromIndex(index,entity); + } + + + @Override + public void addToIndex(T entity, RestIndex index, String key, Object value) { + restAPI.addToIndex(entity,index,key,value); + } + + @Override + @SuppressWarnings("unchecked") + public T putIfAbsent(T entity, RestIndex index, String key, Object value) { + return restAPI.putIfAbsent(entity, index, key, value); + } + + @Override + public boolean hasToUpdate(long lastUpdate) { + return restAPI.hasToUpdate(lastUpdate); + } + @Override + public IndexInfo indexInfo(final String indexType) { + return restAPI.indexInfo(indexType); + } + + + @Override + public Collection getAllLabelNames() { + return restAPI.getAllLabelNames(); + } + + @Override + public Iterable getRelationshipTypes() { + return restAPI.getRelationshipTypes(); + } + + @Override + public TraversalDescription createTraversalDescription() { + return restAPI.createTraversalDescription(); + } + + public String getBaseUri() { + return restRequest.getUri(); + } + + @Override + public RestEntityExtractor getEntityExtractor() { + return restAPI.getEntityExtractor(); + } + + @Override + public RestEntity createRestEntity(Map data) { + return restAPI.createRestEntity(data); + } +} diff --git a/spring-data-neo4j-rest/src/main/java/org/neo4j/rest/graphdb/RestAPIImpl.java b/spring-data-neo4j-rest/src/main/java/org/neo4j/rest/graphdb/RestAPIImpl.java new file mode 100644 index 000000000..4f8738379 --- /dev/null +++ b/spring-data-neo4j-rest/src/main/java/org/neo4j/rest/graphdb/RestAPIImpl.java @@ -0,0 +1,717 @@ +/** + * Copyright (c) 2002-2013 "Neo Technology," + * Network Engine for Objects in Lund AB [http://neotechnology.com] + * + * This file is part of Neo4j. + * + * Neo4j 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. + * + * This program 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 this program. If not, see . + */ +package org.neo4j.rest.graphdb; + +import org.neo4j.graphdb.*; +import org.neo4j.graphdb.index.IndexHits; +import org.neo4j.graphdb.traversal.TraversalDescription; +import org.neo4j.helpers.collection.IterableWrapper; +import org.neo4j.helpers.collection.MapUtil; +import org.neo4j.index.lucene.ValueContext; +import org.neo4j.rest.graphdb.batch.BatchRestAPI; +import org.neo4j.rest.graphdb.entity.RestEntityCache; +import org.neo4j.rest.graphdb.query.CypherRestResult; +import org.neo4j.rest.graphdb.converter.RelationshipIterableConverter; +import org.neo4j.rest.graphdb.converter.RestEntityExtractor; +import org.neo4j.rest.graphdb.converter.RestIndexHitsConverter; +import org.neo4j.rest.graphdb.entity.RestEntity; +import org.neo4j.rest.graphdb.entity.RestNode; +import org.neo4j.rest.graphdb.entity.RestRelationship; +import org.neo4j.rest.graphdb.index.IndexInfo; +import org.neo4j.rest.graphdb.index.RestIndex; +import org.neo4j.rest.graphdb.index.RestIndexManager; +import org.neo4j.rest.graphdb.index.RetrievedIndexInfo; +import org.neo4j.rest.graphdb.index.SimpleIndexHits; +import org.neo4j.rest.graphdb.query.CypherResult; +import org.neo4j.rest.graphdb.query.RestQueryResult; +import org.neo4j.rest.graphdb.transaction.NullTransaction; +import org.neo4j.rest.graphdb.traversal.RestDirection; +import org.neo4j.rest.graphdb.traversal.RestTraversal; +import org.neo4j.rest.graphdb.traversal.RestTraverser; +import org.neo4j.rest.graphdb.util.JsonHelper; +import org.neo4j.rest.graphdb.util.QueryResult; +import org.neo4j.rest.graphdb.util.ResultConverter; + +import javax.ws.rs.core.Response; +import javax.ws.rs.core.Response.Status; +import java.io.ByteArrayInputStream; +import java.io.InputStream; +import java.io.UnsupportedEncodingException; +import java.util.*; +import java.util.concurrent.TimeUnit; + +import static javax.ws.rs.core.Response.Status.CREATED; +import static org.neo4j.helpers.collection.MapUtil.map; +import static org.neo4j.rest.graphdb.ExecutingRestRequest.encode; + + +public class RestAPIImpl implements RestAPI { + + public static final String _QUERY_RETURN_NODE = " RETURN id(n) as id, labels(n) as labels, n as data"; + public static final String GET_REL_TYPES_QUERY = "MATCH (n)-[r]-() WHERE id(n) = {id} RETURN distinct type(r) as relType"; + private static final String[] NO_LABELS = new String[0]; + protected RestRequest restRequest; + + private long entityRefetchTimeInMillis = TimeUnit.SECONDS.toMillis(1000); //TODO move to cache + private final RestEntityCache entityCache = new RestEntityCache(this); + private RestEntityExtractor restEntityExtractor = new RestEntityExtractor(this); + + public RestAPIImpl(String uri) { + this.restRequest = createRestRequest(uri, null, null); + } + + public RestAPIImpl(String uri, String user, String password) { + this.restRequest = createRestRequest(uri, user, password); + } + + protected RestRequest createRestRequest(String uri, String user, String password) { + return new ExecutingRestRequest(uri, user, password); + } + + @Override + public RestIndexManager index() { + return new RestIndexManager(this); + } + + @Override + public RestNode getNodeById(long id, Load force) { + if (force != Load.ForceFromServer) { + RestNode restNode = entityCache.getNode(id); + if (restNode != null) return restNode; + } + if (force == Load.FromCache) return new RestNode(RestNode.nodeUri(this, id),this); + + BatchRestAPI batchRestAPI = new BatchRestAPI(this); + RestNode node = batchRestAPI.getNodeById(id); +// RequestResult response = restRequest.get("node/" + id); +// if (response.statusIs(Status.NOT_FOUND)) { +// throw new NotFoundException("" + id); +// } +// Collection labels = getNodeLabels(id); +// RestNode node = new RestNode(id, labels, (Map) response.toMap(), this); + return entityCache.addToCache(node); + } + + @Override + public RestNode addToCache(RestNode restNode) { + return entityCache.addToCache(restNode); + } + + @Override + public RestNode getFromCache(long id) { + return entityCache.getNode(id); + } + + @Override + public RestNode getNodeById(long id) { + return getNodeById(id, Load.FromServer); + } + + @Override + public RestRelationship getRelationshipById(long id) { + RequestResult requestResult = restRequest.get("relationship/" + id); + if (requestResult.statusIs(Status.NOT_FOUND)) { + throw new NotFoundException("" + id); + } + return new RestRelationship(requestResult.toMap(), this); + } + + + @Override + public RestNode createNode(Map props) { + return createNode(props,Collections.emptyList()); + } + + @Override + public RestNode createNode(Map props, Collection labels) { + RequestResult result = restRequest.post("node", props); + RestNode node = createRestNode(result); + if (node==null) { + throw RestResultException.create(result); + } + addLabels(node,labels); + node.setLabels(labels); + return entityCache.addToCache(node); + } + + @Override + public RestNode getOrCreateNode(RestIndex index, String key, Object value, final Map properties, Collection labels) { + if (index==null || key == null || value==null) throw new IllegalArgumentException("Unique index "+index+" key "+key+" value must not be null"); + final Map data = map("key", key, "value", value, "properties", properties); + final RequestResult result = getRestRequest().post(uniqueIndexPath(index), data); + if (result.statusIs(Response.Status.CREATED) || result.statusIs(Response.Status.OK)) { + RestNode node = (RestNode) getEntityExtractor().convertFromRepresentation(result); + addLabels(node, labels); + node.setLabels(labels); + return entityCache.addToCache(node); + } + throw new RuntimeException(String.format("Error retrieving or creating node for key %s and value %s with index %s", key, value, index.getIndexName())); + } + + private String toLabelString(String[] labels) { + if (labels==null || labels.length == 0) return ""; + StringBuilder sb = new StringBuilder(); + for (String label : labels) { + sb.append(":").append(label); + } + return sb.toString(); + } + + + private RestNode createRestNode(RequestResult result) { + if (result.statusIs(Status.NOT_FOUND)) { + throw new NotFoundException("Node not found"); + } + RestNode node = null; + if (result.statusIs(CREATED)) { + node = result.isMap() ? new RestNode(result.toMap(), this) : new RestNode(result.getLocation(), this); + } + if (node == null && result.statusIs(Status.OK)) { + node = new RestNode(result.toMap(), this); + } + return entityCache.addToCache(node); + } + + @Override + public RestRelationship createRelationship(Node startNode, Node endNode, RelationshipType type, Map props) { + // final RestRequest restRequest = ((RestNode) startNode).getRestRequest(); + final RestNode end = (RestNode) endNode; + Map data = map("to", end.getUri(), "type", type.name()); + if (props != null && props.size() > 0) { + data.put("data", props); + } + final RestNode start = (RestNode) startNode; + RequestResult requestResult = getRestRequest().with(start.getUri()).post("relationships", data); + return createRestRelationship(requestResult, startNode); + } + + private RestRelationship createRestRelationship(RequestResult requestResult, PropertyContainer element) { + if (requestResult.statusOtherThan(CREATED)) { + final int status = requestResult.getStatus(); + throw new RuntimeException("Error creating relationship " + status+" "+requestResult.getText()); + } + final String location = requestResult.getLocation(); + if (requestResult.isMap()) return new RestRelationship(requestResult.toMap(), this); + return new RestRelationship(location, this); + } + + @Override + @SuppressWarnings("unchecked") + public RestIndex getIndex(String indexName) { + final RestIndexManager index = this.index(); + if (index.existsForNodes(indexName)) return (RestIndex) index.forNodes(indexName); + if (index.existsForRelationships(indexName)) return (RestIndex) index.forRelationships(indexName); + throw new IllegalArgumentException("Index " + indexName + " does not yet exist"); + } + + @Override + @SuppressWarnings("unchecked") + public void createIndex(String type, String indexName, Map config) { + Map data=new HashMap(); + data.put("name",indexName); + data.put("config",config); + restRequest.post("index/" + type, data); + } + + @Override + @SuppressWarnings("unchecked") + public RestIndex createIndex(Class type, String indexName, Map config) { + if (Node.class.isAssignableFrom(type)) { + return (RestIndex) index().forNodes( indexName, config); + } + if (Relationship.class.isAssignableFrom(type)) { + return (RestIndex) index().forRelationships(indexName, config); + } + throw new IllegalArgumentException("Required Node or Relationship types to create index, got " + type); + } + + @Override + public void close() { + ExecutingRestRequest.shutdown(); + } + + @Override + public RestNode merge(String labelName, String key, Object value, final Map nodeProperties, Collection labels) { + if (labelName ==null || key == null || value==null) throw new IllegalArgumentException("Label "+ labelName +" key "+key+" and value must not be null"); + Map props = nodeProperties.containsKey(key) ? nodeProperties : MapUtil.copyAndPut(nodeProperties, key, value); + Map params = map("props", props, "value", value); + Iterator> result = query(mergeQuery(labelName, key, labels), params).getData().iterator(); + if (!result.hasNext()) + throw new RuntimeException("Error merging node with labels: " + labelName + " key " + key + " value " + value + " labels " + labels+ " and props: " + props + " no data returned"); + + return entityCache.addToCache(toNode(result.next())); + } + + private String mergeQuery(String labelName, String key, Collection labels) { + StringBuilder setLabels = new StringBuilder(); + if (labels!=null) { + for (String label : labels) { + if (label.equals(labelName)) continue; + setLabels.append("SET n:").append(label).append(" "); + } + } + return "MERGE (n:`"+labelName+"` {`"+key+"`: {value}}) ON CREATE SET n={props} "+setLabels+ _QUERY_RETURN_NODE; + } + + @Override + public boolean isAutoIndexingEnabled(Class clazz) { + RequestResult response = getRestRequest().get(buildPathAutoIndexerStatus(clazz)); + if (response.statusIs(Response.Status.OK)) { + return Boolean.parseBoolean(response.getText()); + } else { + throw new IllegalStateException("received " + response); + } + } + + @Override + public void setAutoIndexingEnabled(Class clazz, boolean enabled) { + RequestResult response = getRestRequest().put(buildPathAutoIndexerStatus(clazz), enabled); + if (response.statusOtherThan(Status.NO_CONTENT)) { + throw new IllegalStateException("received " + response); + } + } + + @Override + public Set getAutoIndexedProperties(Class forClass) { + RequestResult response = getRestRequest().get(buildPathAutoIndexerProperties(forClass).toString()); + Collection autoIndexedProperties = (Collection) JsonHelper.readJson(response.getText()); + return new HashSet(autoIndexedProperties); + } + + @Override + public void startAutoIndexingProperty(Class forClass, String s) { + try { + // we need to use a inputstream instead of the string directly. Otherwise "post" implicitly uses + // StreamJsonHelper.writeJsonTo which quotes a given string + InputStream stream = new ByteArrayInputStream(s.getBytes("UTF-8")); + RequestResult response = getRestRequest().post(buildPathAutoIndexerProperties(forClass).toString(), stream); + if (response.statusOtherThan(Status.NO_CONTENT)) { + throw new IllegalStateException("received " + response); + } + } catch (UnsupportedEncodingException e) { + throw new IllegalStateException(e); + } + + } + + @Override + public void stopAutoIndexingProperty(Class forClass, String s) { + RequestResult response = getRestRequest().delete(buildPathAutoIndexerProperties(forClass).append("/").append(s).toString()); + if (response.statusOtherThan(Status.NO_CONTENT)) { + throw new IllegalStateException("received " + response); + } + } + + + @Override + public void removeLabel(RestNode node, String label) { + RequestResult response = getRestRequest().with(node.getUri()).delete("labels/" + encode(label)); + if (response.statusOtherThan(Status.NO_CONTENT)) { + throw new IllegalStateException("received " + response); + } + } + + public Collection getNodeLabels(long id) { + RequestResult response = restRequest.get(RestNode.nodeUri(this,id)+"/labels"); + if (response.statusOtherThan(Status.OK)) { + throw new IllegalStateException("received " + response); + } + return (Collection) response.toEntity(); + } + + @Override + public Collection getAllLabelNames() { + RequestResult response = restRequest.get("labels"); + if (response.statusOtherThan(Status.OK)) { + throw new IllegalStateException("received " + response); + } + return (Collection) response.toEntity(); + } + + @Override + public Iterable getNodesByLabel(String label) { + RequestResult response = getRestRequest().get("label/" + encode(label) + "/nodes"); + if (response.statusOtherThan(Status.OK)) { + throw new IllegalStateException("received " + response); + } + return (Iterable) getEntityExtractor().convertFromRepresentation(response); + } + + private RestNode toNode(List row) { + long id = ((Number) row.get(0)).longValue(); + List labels = (List) row.get(1); + Map restData = (Map) row.get(2); + return new RestNode(id, labels, restData, this); + } + + private Iterable queryForNodes(String statement, Map params) { + Iterable> result = query(statement, params).getData(); + return new IterableWrapper>(result) { + protected RestNode underlyingObjectToObject(List row) { + return entityCache.addToCache(toNode(row)); + } + }; + } + + @Override + public Iterable getNodesByLabelAndProperty(String label, String property, Object value) { + String statement = "MATCH (n:`" + label + "`) WHERE n.`"+property+"` = {value} " + _QUERY_RETURN_NODE; + return queryForNodes(statement, map("value", value)); + } + + @Override + public Iterable getRelationshipTypes(RestNode node) { + Iterable> result = query(GET_REL_TYPES_QUERY, map("id", node.getId())).getData(); + return new IterableWrapper>(result) { + protected RelationshipType underlyingObjectToObject(List row) { + return DynamicRelationshipType.withName(row.get(0).toString()); + } + }; + } + + @Override + public int getDegree(RestNode restNode, RelationshipType type, Direction direction) { + String relPattern = "--"; + if (type != null) relPattern = "-[:`"+type+"`]-"; + if (direction == Direction.OUTGOING) { + relPattern += ">"; + } else if (direction == Direction.INCOMING) { + relPattern = "<" + relPattern; + } + String nodeDegreeQuery = "MATCH (n)" + relPattern + "() WHERE id(n) = {id} RETURN count(*) as degree"; + Iterator> degree = query(nodeDegreeQuery, map("id", restNode.getId())).getData().iterator(); + if (!degree.hasNext()) return 0; + return ((Number)degree.next().get(0)).intValue(); + } + + @Override + public Iterable getRelationshipTypes() { + Object result = restRequest.get("relationship/types").toEntity(); + if (!(result instanceof Iterable)) throw new RuntimeException("Error loading relationship types"); + + return new IterableWrapper((Iterable) result) { + protected RelationshipType underlyingObjectToObject(Object type) { + return DynamicRelationshipType.withName(type.toString()); + } + }; + } + + @Override + public void addLabels(RestNode node, Collection labels) { + if (labels == null || labels.isEmpty()) return; + RequestResult response = getRestRequest().with(node.getUri()).post("labels", labels); + + if (response.statusOtherThan(Status.NO_CONTENT)) { + throw new IllegalStateException("error adding labels, received " + response); + } + } + + private String buildPathAutoIndexerStatus(Class clazz) { + return buildPathAutoIndexerBase(clazz).append("/status").toString(); + } + + private StringBuilder buildPathAutoIndexerProperties(Class clazz) { + return buildPathAutoIndexerBase(clazz).append("/properties"); + } + + private StringBuilder buildPathAutoIndexerBase(Class clazz) { + return new StringBuilder().append("index/auto/").append(indexTypeName(clazz)); + } + + + public RestRequest getRestRequest() { + return restRequest; + } + + + @Override + public TraversalDescription createTraversalDescription() { + return new RestTraversal(); + } + + @Override + public Transaction beginTx() { + return new NullTransaction(); + } + + public long getEntityRefetchTimeInMillis() { + return entityRefetchTimeInMillis; + } + + public String getBaseUri() { + return restRequest.getUri(); + } + + + public void setEntityRefetchTimeInMillis(long entityRefetchTimeInMillis) { + this.entityRefetchTimeInMillis = entityRefetchTimeInMillis; + } + + @SuppressWarnings("unchecked") + public Iterable wrapRelationships(RequestResult requestResult) { + return (Iterable) new RelationshipIterableConverter(this).convertFromRepresentation(requestResult); + } + + public RestEntityExtractor getEntityExtractor() { + return restEntityExtractor; + } + + + public String indexPath( Class entityType, String indexName, String key, Object value ) { + String typeName = indexTypeName(entityType); + return "index/" + typeName + "/" + encode(indexName) + (key!=null? "/" + encode(key) :"") + (value!=null ? "/" + encode(value):""); + } + + private String indexTypeName(Class entityType) { + return entityType.getSimpleName().toLowerCase(); + } + + public String indexPath( Class entityType, String indexName ) { + return "index/" + indexTypeName(entityType) + "/" + encode(indexName); + } + + private String queryPath( Class entityType, String indexName, String key, Object value ) { + return indexPath( entityType, indexName, key,null) + "?query="+ encode(value); + } + + @Override + @SuppressWarnings("unchecked") + public IndexHits getIndex(Class entityType, String indexName, String key, Object value) { + String indexPath = indexPath(entityType, indexName, key, value); + RequestResult response = restRequest.get(indexPath); + if (response.statusIs(Response.Status.OK)) { + return new RestIndexHitsConverter(this, entityType).convertFromRepresentation(response); + } else { + return new SimpleIndexHits(Collections.emptyList(), 0, entityType, this); + } + } + @Override + @SuppressWarnings("unchecked") + public IndexHits queryIndex(Class entityType, String indexName, String key, Object value) { + String indexPath = queryPath(entityType, indexName, key, value); + RequestResult response = restRequest.get(indexPath); + if (response.statusIs(Response.Status.OK)) { + return new RestIndexHitsConverter(this, entityType).convertFromRepresentation(response); + } else { + return new SimpleIndexHits(Collections.emptyList(), 0, entityType, this); + } + } + + @Override + public void deleteEntity(RestEntity entity) { + getRestRequest().with(entity.getUri()).delete( "" ); + entityCache.remove(entity.getId()); + } + @Override + public IndexInfo indexInfo(final String indexType) { + RequestResult response = restRequest.get("index/" + encode(indexType)); + return new RetrievedIndexInfo(response); + } + + @Override + public void setPropertyOnEntity(RestEntity entity, String key, Object value) { + RequestResult result = getRestRequest().with(entity.getUri()).put("properties/" + encode(key), value); + if (result.statusOtherThan(Status.NO_CONTENT)) + throw new RuntimeException("Error setting properties on entity "+entity+" properties "+key+" : "+value); + } + + @Override + public void setPropertiesOnEntity(RestEntity entity, Map properties) { + RequestResult result = getRestRequest().with(entity.getUri()).put("properties", properties); + if (result.statusOtherThan(Status.NO_CONTENT)) + throw new RuntimeException("Error setting properties on entity "+entity+" properties "+properties); + } + + @SuppressWarnings("unchecked") + private Map getPropertiesFromEntity(RestEntity entity){ + RequestResult response = getRestRequest().with(entity.getUri()).get("properties"); + Map properties; + boolean ok = response.statusIs( Status.OK ); + if ( ok ) { + properties = (Map) response.toMap( ); + } else { + properties = Collections.emptyMap(); + } + + return properties; + } + + private void deleteIndex(String indexPath) { + getRestRequest().delete(indexPath); + } + + @Override + public void delete(RestIndex index) { + deleteIndex(indexPath(index, null, null)); + } + + @Override + public void removeFromIndex(RestIndex index, T entity, String key, Object value) { + String indexPath = indexPath(index, key, value); + deleteIndex(indexPath(indexPath, entity)); + } + + protected String indexPath(String indexPath, T restEntity) { + return indexPath + "/" + ((RestEntity)restEntity).getId(); + } + + @Override + public void removeFromIndex(RestIndex index, T entity, String key) { + String indexPath = indexPath(index, key, null); + deleteIndex(indexPath(indexPath, entity)); + } + + private String indexPath(RestIndex index, String key, Object value) { + return indexPath(index.getEntityType(), index.getIndexName(), key, value); + } + + @Override + public void removeFromIndex(RestIndex index, T entity) { + deleteIndex(indexPath(indexPath(index, null, null), entity)); + } + + public String uniqueIndexPath(RestIndex index) { + return indexPath(index,null,null) + "?uniqueness=get_or_create"; + } + + @Override + public void addToIndex(T entity, RestIndex index, String key, Object value) { + final RestEntity restEntity = (RestEntity) entity; + String uri = restEntity.getUri(); + if (value instanceof ValueContext) { + value = ((ValueContext)value).getCorrectValue(); + } + final Map data = map("key", key, "value", value, "uri", uri); + final RequestResult result = getRestRequest().post(indexPath(index, null, null), data); + if (result.statusOtherThan(Status.CREATED)) throw new RuntimeException(String.format("Error adding element %d %s %s to index %s", restEntity.getId(), key, value, index.getIndexName())); + } + + @Override + @SuppressWarnings("unchecked") + public T putIfAbsent(T entity, RestIndex index, String key, Object value) { + final RestEntity restEntity = (RestEntity) entity; + restEntity.flush(); + String uri = restEntity.getUri(); + if (value instanceof ValueContext) { + value = ((ValueContext)value).getCorrectValue(); + } + final Map data = map("key", key, "value", value, "uri", uri); + final RequestResult result = getRestRequest().post(uniqueIndexPath(index), data); + if (result.statusIs(Response.Status.CREATED)) { + if (index.getEntityType().equals(Node.class)) return (T)createRestNode(result); + if (index.getEntityType().equals(Relationship.class)) return (T)createRestRelationship(result,restEntity); + } + if (result.statusIs(Response.Status.OK)) { + return (T) getEntityExtractor().convertFromRepresentation(result); + } + throw new RuntimeException(String.format("Error adding element %d %s %s to index %s", restEntity.getId(), key, value, index.getIndexName())); + } + + @Override + public boolean hasToUpdate(long lastUpdate) { + return timeElapsed(lastUpdate, getEntityRefetchTimeInMillis()); + } + + @Override + public void removeProperty(RestEntity entity, String key) { + restRequest.with(entity.getUri()).delete("properties/" + encode(key)); + } + + private boolean timeElapsed( long since, long isItGreaterThanThis ) { + return System.currentTimeMillis() - since > isItGreaterThanThis; + } + + @Override + public RestRelationship getOrCreateRelationship(RestIndex index, String key, Object value, final RestNode start, final RestNode end, final String type, final Map properties) { + if (index==null || key == null || value==null) throw new IllegalArgumentException("Unique index "+index+" key "+key+" value must not be null"); + if (start == null || end == null || type == null) throw new IllegalArgumentException("Neither start, end nore type must be null"); + final Map data = map("key", key, "value", value, "properties", properties, "start", start.getUri(), "end", end.getUri(), "type", type); + final RequestResult result = getRestRequest().post(uniqueIndexPath(index), data); + if (result.statusIs(Response.Status.CREATED) || result.statusIs(Response.Status.OK)) { + return (RestRelationship) getEntityExtractor().convertFromRepresentation(result); + } + throw new RuntimeException(String.format("Error retrieving or creating relationship for key %s and value %s with index %s", key, value, index.getIndexName())); + } + + public org.neo4j.rest.graphdb.query.CypherResult query(String statement, Map params) { + params = (params==null) ? Collections.emptyMap() : params; + final RequestResult requestResult = getRestRequest().post("cypher", map("query", statement, "params", params)); + return new CypherRestResult(requestResult); + } + + @Override + public Iterable getRelationships(RestNode restNode, Direction direction, RelationshipType... types) { + String path = (types.length > 1) ? relPath(types) : relPath(direction,types.length == 0 ? null : types[0]); + return wrapRelationships(getRestRequest().with(restNode.getUri()).get(path)); + } + + + private String relPath(RelationshipType... types) { + String path = "relationships/all/"; + int counter = 0; + for ( RelationshipType type : types ) { + if ( counter++ > 0 ) { + path += "&"; + } + path += encode(type.name()); + } + return path; + } + + private String relPath(Direction direction,RelationshipType type) { + String path = "relationships/" + RestDirection.from(direction).shortName; + return type == null ? path : path + "/" + encode(type.name()); + } + + + private static final String FULLPATH = "fullpath"; + + @Override + public RestTraverser traverse(RestNode restNode, Map description) { + final RequestResult result = getRestRequest().with(restNode.getUri()).post("traverse/" + FULLPATH, description); + if (result.statusOtherThan(Response.Status.OK)) throw new RuntimeException(String.format("Error executing traversal: %d %s",result.getStatus(), description)); + final Object col = result.toEntity(); + if (!(col instanceof Collection)) throw new RuntimeException(String.format("Unexpected traversal result, %s instead of collection", col != null ? col.getClass() : null)); + return new RestTraverser((Collection) col,restNode.getRestApi()); + } + + public QueryResult> query(String statement, Map params, ResultConverter resultConverter) { + final CypherResult result = query(statement, params); + if (RestResultException.isExceptionResult(result.asMap())) throw new RestResultException(result.asMap()); + return RestQueryResult.toQueryResult(result, this, resultConverter); + } + + @Override + public RestEntity createRestEntity(Map data) { + final String uri = (String) data.get("self"); + if (uri == null || uri.isEmpty()) return null; + if (uri.contains("/node/")) { + return entityCache.addToCache(new RestNode(data, this)); + } + if (uri.contains("/relationship/")) { + return new RestRelationship(data, this); + } + return null; + } + + public RequestResult batch(Collection> batchRequestData) { + return restRequest.post("batch",batchRequestData); + } +} diff --git a/spring-data-neo4j-rest/src/main/java/org/neo4j/rest/graphdb/RestAPIIndex.java b/spring-data-neo4j-rest/src/main/java/org/neo4j/rest/graphdb/RestAPIIndex.java new file mode 100644 index 000000000..7b8a98ce7 --- /dev/null +++ b/spring-data-neo4j-rest/src/main/java/org/neo4j/rest/graphdb/RestAPIIndex.java @@ -0,0 +1,65 @@ +package org.neo4j.rest.graphdb; + +import org.neo4j.graphdb.Node; +import org.neo4j.graphdb.PropertyContainer; +import org.neo4j.graphdb.Relationship; +import org.neo4j.graphdb.index.IndexHits; +import org.neo4j.rest.graphdb.entity.RestNode; +import org.neo4j.rest.graphdb.entity.RestRelationship; +import org.neo4j.rest.graphdb.index.IndexInfo; +import org.neo4j.rest.graphdb.index.RestIndex; +import org.neo4j.rest.graphdb.index.RestIndexManager; + +import java.util.Collection; +import java.util.Map; +import java.util.Set; + +/** + * @author mh + * @since 21.09.14 + */ +public interface RestAPIIndex { + RestIndexManager index(); + + @SuppressWarnings("unchecked") + RestIndex getIndex(String indexName); + + @SuppressWarnings("unchecked") + void createIndex(String type, String indexName, Map config); + + @SuppressWarnings("unchecked") + IndexHits getIndex(Class entityType, String indexName, String key, Object value); + IndexHits queryIndex(Class entityType, String indexName, String key, Object value); + + IndexInfo indexInfo(String indexType); + + void removeFromIndex(RestIndex index, T entity, String key, Object value); + + void removeFromIndex(RestIndex index, T entity, String key); + + void removeFromIndex(RestIndex index, T entity); + + void addToIndex(T entity, RestIndex index, String key, Object value); + + @SuppressWarnings("unchecked") + T putIfAbsent(T entity, RestIndex index, String key, Object value); + + RestNode getOrCreateNode(RestIndex index, String key, Object value, Map properties, Collection labels); + + RestRelationship getOrCreateRelationship(RestIndex index, String key, Object value, RestNode start, RestNode end, String type, Map properties); + + @SuppressWarnings("unchecked") + RestIndex createIndex(Class type, String indexName, Map config); + + boolean isAutoIndexingEnabled(Class clazz); + + void setAutoIndexingEnabled(Class clazz, boolean enabled); + + Set getAutoIndexedProperties(Class forClass); + + void startAutoIndexingProperty(Class forClass, String s); + + void stopAutoIndexingProperty(Class forClass, String s); + + void delete(RestIndex index); +} diff --git a/spring-data-neo4j-rest/src/main/java/org/neo4j/rest/graphdb/RestAPIInternal.java b/spring-data-neo4j-rest/src/main/java/org/neo4j/rest/graphdb/RestAPIInternal.java new file mode 100644 index 000000000..0169e8d91 --- /dev/null +++ b/spring-data-neo4j-rest/src/main/java/org/neo4j/rest/graphdb/RestAPIInternal.java @@ -0,0 +1,30 @@ +package org.neo4j.rest.graphdb; + +import org.neo4j.rest.graphdb.converter.RestEntityExtractor; +import org.neo4j.rest.graphdb.entity.RestEntity; +import org.neo4j.rest.graphdb.entity.RestNode; + +import java.util.Map; + +/** + * @author mh + * @since 21.09.14 + */ +public interface RestAPIInternal { + RestNode getNodeById(long id, RestAPI.Load force); + + boolean hasToUpdate(long lastUpdate); + + String getBaseUri(); + + RestEntityExtractor getEntityExtractor(); + + // todo add to cache or update data in cache + RestEntity createRestEntity(Map data); + + public enum Load { + FromCache, + FromServer, + ForceFromServer + } +} diff --git a/spring-data-neo4j-rest/src/main/java/org/neo4j/rest/graphdb/RestGraphDatabase.java b/spring-data-neo4j-rest/src/main/java/org/neo4j/rest/graphdb/RestGraphDatabase.java new file mode 100644 index 000000000..cb81a4db2 --- /dev/null +++ b/spring-data-neo4j-rest/src/main/java/org/neo4j/rest/graphdb/RestGraphDatabase.java @@ -0,0 +1,163 @@ +/** + * Copyright (c) 2002-2013 "Neo Technology," + * Network Engine for Objects in Lund AB [http://neotechnology.com] + * + * This file is part of Neo4j. + * + * Neo4j 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. + * + * This program 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 this program. If not, see . + */ +package org.neo4j.rest.graphdb; + + +import org.neo4j.graphdb.*; +import org.neo4j.graphdb.schema.Schema; +import org.neo4j.graphdb.traversal.BidirectionalTraversalDescription; +import org.neo4j.kernel.impl.nioneo.store.StoreId; +import org.neo4j.rest.graphdb.entity.RestNode; +import org.neo4j.rest.graphdb.index.RestIndexManager; +import org.neo4j.rest.graphdb.query.RestCypherQueryEngine; +import org.neo4j.rest.graphdb.transaction.NullTransactionManager; +import org.neo4j.rest.graphdb.traversal.RestTraversal; +import org.neo4j.rest.graphdb.traversal.RestTraversalDescription; +import org.neo4j.rest.graphdb.util.ResourceIterableWrapper; + +import javax.transaction.TransactionManager; +import java.util.Collection; +import java.util.LinkedHashSet; + + +public class RestGraphDatabase extends AbstractRemoteDatabase { + private RestAPI restAPI; + private final RestCypherQueryEngine cypherQueryEngine; + + + public RestGraphDatabase( RestAPI api){ + this.restAPI = api; + cypherQueryEngine = new RestCypherQueryEngine(restAPI); + } + + public RestGraphDatabase( String uri ) { + this( new RestAPIImpl( uri )); + } + + public RestGraphDatabase( String uri, String user, String password ) { + this(new RestAPIImpl( uri, user, password )); + } + + public RestAPI getRestAPI(){ + return this.restAPI; + } + + public RestIndexManager index() { + return this.restAPI.index(); + } + + public Node createNode() { + return this.restAPI.createNode(null); + } + + public Node getNodeById( long id ) { + return this.restAPI.getNodeById(id); + } + + @Override + public Iterable getAllNodes() { + return cypherQueryEngine.query("match (n) return n", null).to(Node.class); + } + + @Override + public Iterable getRelationshipTypes() { + return this.restAPI.getRelationshipTypes(); + } + + public Relationship getRelationshipById( long id ) { + return this.restAPI.getRelationshipById(id); + } + @Override + public String getStoreDir() { + return restAPI.getBaseUri(); + } + + @Override + public StoreId storeId() { + return null; + } + + @Override + public boolean isAvailable(long timeout) { + return restAPI!=null; + } + + public TransactionManager getTxManager() { + return new NullTransactionManager(); + } + + @Override + public DependencyResolver getDependencyResolver() { + return new DependencyResolver.Adapter() { + @Override + public T resolveDependency(Class type, SelectionStrategy selector) throws IllegalArgumentException { + if (TransactionManager.class.isAssignableFrom(type)) return (T)getTxManager(); + return null; + } + }; + } + + @Override + public Transaction beginTx() { + return restAPI.beginTx(); + } + + @Override + public void shutdown() { + restAPI.close(); + } + + @Override + public Node createNode(Label... labels) { + LinkedHashSet labelNames = new LinkedHashSet<>(labels.length); + for (int i = 0; i < labels.length; i++) labelNames.add(labels[i].name()); + return restAPI.createNode(null,labelNames); + } + + @Override + public ResourceIterable findNodesByLabelAndProperty(Label label, String property, Object value) { + Iterable nodes = restAPI.getNodesByLabelAndProperty(label.name(), property, value); + return new ResourceIterableWrapper(nodes) { + protected Node underlyingObjectToObject(RestNode node) { + return node; + } + }; + } + + @Override + public Schema schema() { + throw new UnsupportedOperationException(); + } + + @Override + public RestTraversalDescription traversalDescription() { + return RestTraversal.description(); + } + + @Override + public BidirectionalTraversalDescription bidirectionalTraversalDescription() { + throw new UnsupportedOperationException(); + } + + public Collection getAllLabelNames() { + return restAPI.getAllLabelNames(); + } +} + diff --git a/spring-data-neo4j-rest/src/main/java/org/neo4j/rest/graphdb/RestRequest.java b/spring-data-neo4j-rest/src/main/java/org/neo4j/rest/graphdb/RestRequest.java new file mode 100644 index 000000000..a478f63db --- /dev/null +++ b/spring-data-neo4j-rest/src/main/java/org/neo4j/rest/graphdb/RestRequest.java @@ -0,0 +1,42 @@ +/** + * Copyright (c) 2002-2013 "Neo Technology," + * Network Engine for Objects in Lund AB [http://neotechnology.com] + * + * This file is part of Neo4j. + * + * Neo4j 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. + * + * This program 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 this program. If not, see . + */ +package org.neo4j.rest.graphdb; + +import java.util.Map; + +public interface RestRequest { + + RequestResult get(String path); + + RequestResult get(String path, Object data); + + RequestResult delete(String path); + + RequestResult post(String path, Object data); + + RequestResult put(String path, Object data); + + RestRequest with(String uri); + + String getUri(); + + Map toMap( RequestResult requestResult); + +} \ No newline at end of file diff --git a/spring-data-neo4j-rest/src/main/java/org/neo4j/rest/graphdb/RestResultException.java b/spring-data-neo4j-rest/src/main/java/org/neo4j/rest/graphdb/RestResultException.java new file mode 100644 index 000000000..1738fb23c --- /dev/null +++ b/spring-data-neo4j-rest/src/main/java/org/neo4j/rest/graphdb/RestResultException.java @@ -0,0 +1,80 @@ +/** + * Copyright (c) 2002-2013 "Neo Technology," + * Network Engine for Objects in Lund AB [http://neotechnology.com] + * + * This file is part of Neo4j. + * + * Neo4j 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. + * + * This program 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 this program. If not, see . + */ +package org.neo4j.rest.graphdb; + +import java.util.List; +import java.util.Map; + +public class RestResultException extends RuntimeException { + public RestResultException(Object result, String...messages) { + super(format(toMap(result),messages)); + } + + public RestResultException(String...messages) { + super(format(messages)); + } + + private static String format(String[] messages) { + StringBuilder sb = new StringBuilder(); + if (messages==null || messages.length==0) return ""; + for (String s : messages) { + sb.append(s).append("\n"); + } + return sb.toString(); + } + + private static String format(Map result, String...messages) { + if (result==null && (messages==null || messages.length==0)) return "Unknown Exception"; + StringBuilder sb = new StringBuilder(); + sb.append(format(messages)); + sb.append(result.get("message")).append(" at\n"); + sb.append(result.get("exception")).append("\n"); + List stacktrace = (List) result.get("stacktrace"); + if (stacktrace != null) { + for (String line : stacktrace) { + sb.append(" ").append(line).append("\n"); + } + } + if (result.containsKey("body")) sb.append(format(toMap(result.get("body")))); + return sb.toString(); + } + + public static boolean isExceptionResult(Object result) { + final Map map = toMap(result); + return map!=null && (hasErrorStatus(map) || map.containsKey("exception") || map.containsKey("message") || isExceptionResult(map.get("body"))); + } + + private static boolean hasErrorStatus(Map map) { + Object status = map.get("status"); + return status != null && !status.toString().startsWith("2"); + } + + private static Map toMap(Object result) { + if (!(result instanceof Map)) return null; + return (Map) result; + + } + + static RestResultException create(RequestResult result, String...messages) { + if (result.isMap()) return new RestResultException(result.toMap(),messages); + if (messages.length==0) return new RestResultException("Error executing request,status " + result.getStatus()+" message "+result.getText()); + return new RestResultException(messages); + } +} diff --git a/spring-data-neo4j-rest/src/main/java/org/neo4j/rest/graphdb/UpdatableRestResult.java b/spring-data-neo4j-rest/src/main/java/org/neo4j/rest/graphdb/UpdatableRestResult.java new file mode 100644 index 000000000..6bd83370e --- /dev/null +++ b/spring-data-neo4j-rest/src/main/java/org/neo4j/rest/graphdb/UpdatableRestResult.java @@ -0,0 +1,28 @@ +/** + * Copyright (c) 2002-2013 "Neo Technology," + * Network Engine for Objects in Lund AB [http://neotechnology.com] + * + * This file is part of Neo4j. + * + * Neo4j 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. + * + * This program 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 this program. If not, see . + */ +package org.neo4j.rest.graphdb; + +/** + * @author mh + * @since 21.09.11 + */ +public interface UpdatableRestResult { + void updateFrom(T newValue, RestAPI restApi); +} diff --git a/spring-data-neo4j-rest/src/main/java/org/neo4j/rest/graphdb/UserAgent.java b/spring-data-neo4j-rest/src/main/java/org/neo4j/rest/graphdb/UserAgent.java new file mode 100644 index 000000000..c292b27e5 --- /dev/null +++ b/spring-data-neo4j-rest/src/main/java/org/neo4j/rest/graphdb/UserAgent.java @@ -0,0 +1,72 @@ +/** + * Copyright (c) 2002-2013 "Neo Technology," + * Network Engine for Objects in Lund AB [http://neotechnology.com] + * + * This file is part of Neo4j. + * + * Neo4j 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. + * + * This program 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 this program. If not, see . + */ +package org.neo4j.rest.graphdb; + +import java.io.IOException; +import java.io.InputStream; +import java.util.Properties; +import com.sun.jersey.api.client.Client; +import com.sun.jersey.api.client.ClientHandlerException; +import com.sun.jersey.api.client.ClientRequest; +import com.sun.jersey.api.client.ClientResponse; +import com.sun.jersey.api.client.filter.ClientFilter; + +/** + * @author mh + * @since 10.08.12 + */ +public class UserAgent { + public static final String NEO4J_DRIVER_PROPERTY = "org.neo4j.rest.driver"; + + private final String userAgent = determineUserAgent(); + + private String determineUserAgent() { + String property = System.getProperty(NEO4J_DRIVER_PROPERTY); + if (property == null || property.trim().isEmpty()) { + final Properties props = loadPomProperties(); + return String.format("%s/%s", props.getProperty("artifactId", "neo4j-rest-graphdb"), props.getProperty("version", "0")); + } + return property; + } + + private Properties loadPomProperties() { + final Properties props = new Properties(); + try { + final InputStream is = getClass().getClassLoader().getResourceAsStream("/META-INF/maven/org.neo4j/neo4j-rest-graphdb/pom.properties"); + if (is!=null) { + props.load(is); + is.close(); + } + } catch (Exception e) { + // ignore + } + return props; + } + + public void install(Client client) { + client.addFilter(new ClientFilter() { + @Override + public ClientResponse handle(ClientRequest cr) throws ClientHandlerException { + cr.getHeaders().add("User-Agent", userAgent); + return getNext().handle(cr); + } + }); + } +} diff --git a/spring-data-neo4j-rest/src/main/java/org/neo4j/rest/graphdb/batch/BatchRestAPI.java b/spring-data-neo4j-rest/src/main/java/org/neo4j/rest/graphdb/batch/BatchRestAPI.java new file mode 100644 index 000000000..8191c44ce --- /dev/null +++ b/spring-data-neo4j-rest/src/main/java/org/neo4j/rest/graphdb/batch/BatchRestAPI.java @@ -0,0 +1,174 @@ +/** + * Copyright (c) 2002-2013 "Neo Technology," + * Network Engine for Objects in Lund AB [http://neotechnology.com] + * + * This file is part of Neo4j. + * + * Neo4j 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. + * + * This program 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 this program. If not, see . + */ +package org.neo4j.rest.graphdb.batch; + + +import org.neo4j.graphdb.NotFoundException; +import org.neo4j.graphdb.PropertyContainer; +import org.neo4j.helpers.collection.MapUtil; +import org.neo4j.index.lucene.ValueContext; +import org.neo4j.rest.graphdb.RequestResult; +import org.neo4j.rest.graphdb.RestAPI; +import org.neo4j.rest.graphdb.RestAPIImpl; +import org.neo4j.rest.graphdb.RestResultException; +import org.neo4j.rest.graphdb.converter.RestEntityExtractor; +import org.neo4j.rest.graphdb.converter.RestEntityPropertyRefresher; +import org.neo4j.rest.graphdb.entity.RestEntity; +import org.neo4j.rest.graphdb.entity.RestNode; +import org.neo4j.rest.graphdb.index.IndexInfo; +import org.neo4j.rest.graphdb.index.RestIndex; +import org.neo4j.rest.graphdb.util.DefaultConverter; + +import javax.ws.rs.core.Response; +import java.util.*; + +import static java.util.Arrays.asList; + +public class BatchRestAPI { + + private final RecordingRestRequest restRequest; + private final String baseUri; + private final RestAPI restApi; + + public BatchRestAPI(RestAPI restApi) { + this.baseUri = restApi.getBaseUri(); + this.restApi = restApi; + RestEntityExtractor converter = new RestEntityExtractor(restApi); + this.restRequest = new RecordingRestRequest(new RestOperations(converter), restApi.getBaseUri()); + } + + public void stop() { + restRequest.stop(); + } + + + public RestNode getNodeById(long id) { + RestOperations.RestOperation nodeRequest = restRequest.get("node/" + id); + RestOperations.RestOperation labelsRequest = restRequest.get("node/" + id+"/labels"); + try { + Map results = executeBatchRequest(); + RestNode node = (RestNode) results.get(nodeRequest.getBatchId()); + List labels = (List) results.get(labelsRequest.getBatchId()); + node.setLabels(labels); + return node; + } catch(RestResultException rre) { + if (rre.getMessage().contains("NodeNotFoundException")) throw new NotFoundException("Node not found: "+id); + else throw rre; + } + } + + +// public void addToIndex( T entity, RestIndex index, String key, Object value ) { +// final RestEntity restEntity = (RestEntity) entity; +// String uri = restEntity.getUri(); +// if (value instanceof ValueContext) { +// value = ((ValueContext)value).getCorrectValue(); +// } +// final Map data = MapUtil.map("key", key, "value", value, "uri", uri); +// restRequest.post(index.indexPath(), data); +// } + + public Map executeBatchRequest() { + stop(); + RestOperations operations = restRequest.getOperations(); + RequestResult response = restApi.batch(createBatchRequestData(operations)); + return convertRequestResultToEntities(operations, response); + } + + + @SuppressWarnings("unchecked") + protected Map convertRequestResultToEntities(RestOperations operations, RequestResult response) { + Object result = response.toEntity(); + if (RestResultException.isExceptionResult(result)) { + throw new RestResultException(result); + } + Collection> responseCollection = (Collection>) result; + Map mappedObjects = new HashMap(responseCollection.size()); + for (Map entry : responseCollection) { + if (RestResultException.isExceptionResult(entry)) { + throw new RestResultException(entry); + } + final Long batchId = getBatchId(entry); + final RequestResult subResult = RequestResult.extractFrom(entry); + RestOperations.RestOperation restOperation = operations.getOperation(batchId); + Object entity = restOperation.getResultConverter().convertFromRepresentation(subResult); + mappedObjects.put(batchId, entity); + + } + return mappedObjects; + } + + + private Long getBatchId(Map entry) { + return ((Number) entry.get("id")).longValue(); + } + + protected Collection> createBatchRequestData(RestOperations operations) { + Collection> batch = new ArrayList>(); + for (RestOperations.RestOperation operation : operations.getRecordedRequests().values()) { + Map params = new HashMap(); + params.put("method", operation.getMethod()); + if (operation.isSameUri(baseUri)) { + params.put("to", operation.getUri()); + } else { + params.put("to",createOperationUri(operation)); + } + if (operation.getData() != null) { + params.put("body", operation.getData()); + } + params.put("id", operation.getBatchId()); + batch.add(params); + } + return batch; + } + + private String createOperationUri(RestOperations.RestOperation operation){ + String uri = operation.getBaseUri(); + String suffix = operation.getUri(); + if (suffix.startsWith("/")){ + return uri + suffix; + } + return uri + "/" + suffix; + } + + + private static class BatchIndexInfo implements IndexInfo { + + @Override + public boolean checkConfig(String indexName, Map config) { + return true; + } + + @Override + public String[] indexNames() { + return new String[0]; + } + + @Override + public boolean exists(String indexName) { + return true; + } + + @Override + public Map getConfig(String name) { + return null; + } + } +} diff --git a/spring-data-neo4j-rest/src/main/java/org/neo4j/rest/graphdb/batch/RecordingRestRequest.java b/spring-data-neo4j-rest/src/main/java/org/neo4j/rest/graphdb/batch/RecordingRestRequest.java new file mode 100644 index 000000000..35588b781 --- /dev/null +++ b/spring-data-neo4j-rest/src/main/java/org/neo4j/rest/graphdb/batch/RecordingRestRequest.java @@ -0,0 +1,124 @@ +/** + * Copyright (c) 2002-2013 "Neo Technology," + * Network Engine for Objects in Lund AB [http://neotechnology.com] + * + * This file is part of Neo4j. + * + * Neo4j 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. + * + * This program 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 this program. If not, see . + */ +package org.neo4j.rest.graphdb.batch; + + +import java.io.UnsupportedEncodingException; +import java.net.URLEncoder; +import java.util.Map; + +import javax.ws.rs.core.MediaType; + +import org.neo4j.rest.graphdb.RequestResult; +import org.neo4j.rest.graphdb.RestRequest; +import org.neo4j.rest.graphdb.batch.RestOperations.RestOperation; +import org.neo4j.rest.graphdb.batch.RestOperations.RestOperation.Methods; + + + +public class RecordingRestRequest { + + private final String baseUri; + private MediaType contentType; + private MediaType acceptHeader; + private RestOperations operations; + private boolean stop; + + + public RestOperations getOperations() { + return operations; + } + + public RecordingRestRequest(final String baseUri) { + this(baseUri, MediaType.APPLICATION_JSON_TYPE, MediaType.APPLICATION_JSON_TYPE ); + } + + public RecordingRestRequest(RestOperations operations, final String baseUri) { + this(baseUri); + this.operations = operations; + } + + + public RecordingRestRequest(String baseUri, MediaType contentType, MediaType acceptHeader) { + this.baseUri = uriWithoutSlash( baseUri ); + this.contentType = contentType; + this.acceptHeader = acceptHeader; + } + + public RestOperation get(String path, Object data) { + return this.record(Methods.GET, path, data, getBaseUri()); + } + + public RestOperation delete(String path) { + return this.record(Methods.DELETE, path, null, getBaseUri()); + } + + public RestOperation post(String path, Object data) { + return this.record(Methods.POST, path, data, getBaseUri()); + } + + public RestOperation put(String path, Object data) { + return this.record(Methods.PUT, path, data, getBaseUri()); + + } + + public RecordingRestRequest with(String uri) { + return new RecordingRestRequest(this.operations, uri); + } + + public String getUri() { + return getBaseUri(); + } + + public RestOperation get(String path) { + return this.record(Methods.GET, path, null, getBaseUri()); + } + + public RestOperation record(Methods method, String path, Object data, String baseUri){ + if (stop) throw new IllegalStateException("BatchRequest already executed"); + return this.operations.record(method, path, data,baseUri); + } + + private String uriWithoutSlash( String uri ) { + return uri.endsWith("/") ? uri.substring(0, uri.length() - 1) : uri; + } + + public static String encode( Object value ) { + if ( value == null ) return ""; + try { + return URLEncoder.encode( value.toString(), "utf-8" ).replaceAll( "\\+", "%20" ); + } catch ( UnsupportedEncodingException e ) { + throw new RuntimeException( e ); + } + } + + public Map getRecordedRequests(){ + return this.operations.getRecordedRequests(); + } + + public void stop() { + this.stop = true; + } + + public String getBaseUri() { + return baseUri; + } +} + diff --git a/spring-data-neo4j-rest/src/main/java/org/neo4j/rest/graphdb/batch/RestOperations.java b/spring-data-neo4j-rest/src/main/java/org/neo4j/rest/graphdb/batch/RestOperations.java new file mode 100644 index 000000000..88cb9ccfc --- /dev/null +++ b/spring-data-neo4j-rest/src/main/java/org/neo4j/rest/graphdb/batch/RestOperations.java @@ -0,0 +1,139 @@ +/** + * Copyright (c) 2002-2013 "Neo Technology," + * Network Engine for Objects in Lund AB [http://neotechnology.com] + * + * This file is part of Neo4j. + * + * Neo4j 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. + * + * This program 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 this program. If not, see . + */ +package org.neo4j.rest.graphdb.batch; + +import java.util.LinkedHashMap; +import java.util.Map; +import java.util.concurrent.atomic.AtomicLong; + +import javax.ws.rs.core.MediaType; + + +import org.neo4j.rest.graphdb.RequestResult; +import org.neo4j.rest.graphdb.RestAPI; +import org.neo4j.rest.graphdb.UpdatableRestResult; +import org.neo4j.rest.graphdb.batch.RestOperations.RestOperation.Methods; +import org.neo4j.rest.graphdb.converter.RestEntityExtractor; +import org.neo4j.rest.graphdb.converter.RestResultConverter; +import org.neo4j.rest.graphdb.util.DefaultConverter; +import org.neo4j.rest.graphdb.util.ResultConverter; + +public class RestOperations { + private RestResultConverter resultConverter; + private AtomicLong currentBatchId = new AtomicLong(0); + private Map operations = new LinkedHashMap(); + private MediaType contentType; + private MediaType acceptHeader; + + public RestOperations(){ + this.contentType = MediaType.APPLICATION_JSON_TYPE; + this.acceptHeader = MediaType.APPLICATION_JSON_TYPE; + } + + public RestOperations(RestResultConverter converter) { + this.resultConverter = converter; + } + + public RestOperation getOperation(Long batchId) { + return operations.get(batchId); + } + + public static class RestOperation { + + public void setConverter(RestResultConverter converter) { + this.resultConverter = converter; + } + + public enum Methods{ + POST, + PUT, + GET, + DELETE + } + + private Methods method; + private Object data; + private final String baseUri; + private long batchId; + private String uri; + private MediaType contentType; + private MediaType acceptHeader; + private RestResultConverter resultConverter; + + + + public RestOperation(long batchId, Methods method, String uri, MediaType contentType, MediaType acceptHeader, Object data, String baseUri){ + this.batchId = batchId; + this.method = method; + this.uri = uri; + this.contentType = contentType; + this.acceptHeader = acceptHeader; + this.data = data; + this.baseUri = baseUri; + } + + public RestResultConverter getResultConverter() { + return resultConverter; + } + + public Methods getMethod() { + return method; + } + + public Object getData() { + return data; + } + + public long getBatchId() { + return batchId; + } + + public String getUri() { + return uri; + } + + public MediaType getContentType() { + return contentType; + } + + public MediaType getAcceptHeader() { + return acceptHeader; + } + + public String getBaseUri() { + return baseUri; + } + public boolean isSameUri(String baseUri) { + return this.baseUri.equals(baseUri); + } + } + + public Map getRecordedRequests(){ + return this.operations; + } + + public RestOperation record(Methods method, String path, Object data, String baseUri){ + long batchId = this.currentBatchId.incrementAndGet(); + RestOperation r = new RestOperation(batchId,method,path,this.contentType,this.acceptHeader,data,baseUri); + r.setConverter(resultConverter); + operations.put(batchId,r); + return r; + } +} diff --git a/spring-data-neo4j-rest/src/main/java/org/neo4j/rest/graphdb/converter/ConversionInfo.java b/spring-data-neo4j-rest/src/main/java/org/neo4j/rest/graphdb/converter/ConversionInfo.java new file mode 100644 index 000000000..42c9ab908 --- /dev/null +++ b/spring-data-neo4j-rest/src/main/java/org/neo4j/rest/graphdb/converter/ConversionInfo.java @@ -0,0 +1,44 @@ +/** + * Copyright (c) 2002-2013 "Neo Technology," + * Network Engine for Objects in Lund AB [http://neotechnology.com] + * + * This file is part of Neo4j. + * + * Neo4j 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. + * + * This program 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 this program. If not, see . + */ +package org.neo4j.rest.graphdb.converter; + +/** + * User: KBurchardi + * Date: 19.10.11 + * Time: 10:37 + */ +public class ConversionInfo { + + private Object conversionData; + private boolean successfulConversion; + + public ConversionInfo(Object conversionData, boolean successfulConversion) { + this.conversionData = conversionData; + this.successfulConversion = successfulConversion; + } + + public boolean isSuccessfulConversion() { + return successfulConversion; + } + + public Object getConversionData() { + return conversionData; + } +} diff --git a/spring-data-neo4j-rest/src/main/java/org/neo4j/rest/graphdb/converter/RelationshipIterableConverter.java b/spring-data-neo4j-rest/src/main/java/org/neo4j/rest/graphdb/converter/RelationshipIterableConverter.java new file mode 100644 index 000000000..a2f65bc0c --- /dev/null +++ b/spring-data-neo4j-rest/src/main/java/org/neo4j/rest/graphdb/converter/RelationshipIterableConverter.java @@ -0,0 +1,52 @@ +/** + * Copyright (c) 2002-2013 "Neo Technology," + * Network Engine for Objects in Lund AB [http://neotechnology.com] + * + * This file is part of Neo4j. + * + * Neo4j 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. + * + * This program 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 this program. If not, see . + */ +package org.neo4j.rest.graphdb.converter; + +import java.util.Collection; +import java.util.Map; + +import org.neo4j.graphdb.Relationship; +import org.neo4j.helpers.collection.IterableWrapper; + +import org.neo4j.rest.graphdb.RequestResult; +import org.neo4j.rest.graphdb.RestAPI; +import org.neo4j.rest.graphdb.entity.RestRelationship; + +/** +* @author mh +* @since 22.09.11 +*/ +public class RelationshipIterableConverter implements RestResultConverter { + private final RestAPI restAPI; + + public RelationshipIterableConverter(RestAPI restAPI) { + this.restAPI = restAPI; + } + + @Override + public Object convertFromRepresentation(RequestResult requestResult) { + return new IterableWrapper((Collection) requestResult.toEntity()) { + @Override + protected Relationship underlyingObjectToObject(Object data) { + return new RestRelationship((Map) data, restAPI); + } + }; + } +} diff --git a/spring-data-neo4j-rest/src/main/java/org/neo4j/rest/graphdb/converter/RestEntityExtractor.java b/spring-data-neo4j-rest/src/main/java/org/neo4j/rest/graphdb/converter/RestEntityExtractor.java new file mode 100644 index 000000000..c94bdaa41 --- /dev/null +++ b/spring-data-neo4j-rest/src/main/java/org/neo4j/rest/graphdb/converter/RestEntityExtractor.java @@ -0,0 +1,84 @@ +/** + * Copyright (c) 2002-2013 "Neo Technology," + * Network Engine for Objects in Lund AB [http://neotechnology.com] + * + * This file is part of Neo4j. + * + * Neo4j 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. + * + * This program 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 this program. If not, see . + */ +package org.neo4j.rest.graphdb.converter; + +import java.util.*; + +import org.neo4j.helpers.collection.IterableWrapper; +import org.neo4j.rest.graphdb.RequestResult; +import org.neo4j.rest.graphdb.RestAPI; +import org.neo4j.rest.graphdb.entity.RestEntity; +import org.neo4j.rest.graphdb.entity.RestNode; +import org.neo4j.rest.graphdb.entity.RestRelationship; + + +public class RestEntityExtractor implements RestResultConverter { + private final RestAPI restApi; + + public RestEntityExtractor(RestAPI restApi) { + this.restApi = restApi; + } + + public Object convertFromRepresentation(RequestResult requestResult) { + return convertFromRepresentation(requestResult.toEntity()); + } + + @SuppressWarnings("unchecked") + public Object convertFromRepresentation(Object value) { + if (value instanceof Map) { + if (canHandle(value)) { + value = convertToEntityIfPossible(value); + } else { + final Map source = (Map) value; + Map result=new HashMap<>(source.size()); + for (Map.Entry entry : source.entrySet()) { + result.put(entry.getKey(),convertFromRepresentation(entry.getValue())); + } + return result; + } + } + if (value instanceof Iterable) { + Collection result = value instanceof Set ? new HashSet<>() : new ArrayList<>(); + for (Object o : (Iterable) value) { + result.add(convertFromRepresentation(o)); + } + return result; + } + return value; + } + + private Object convertToEntityIfPossible(Object value) { + if (value instanceof Map) { + RestEntity restEntity = restApi.createRestEntity((Map) value); + if (restEntity != null) return restEntity; + } + return value; + } + + public boolean canHandle(Object value) { + if (value instanceof Map) { + final String uri = (String) ((Map)value).get("self"); + if (uri != null && (uri.contains("/node/") || uri.contains("/relationship/"))){ + return true; + } + } + return false; + } +} diff --git a/spring-data-neo4j-rest/src/main/java/org/neo4j/rest/graphdb/converter/RestEntityPropertyRefresher.java b/spring-data-neo4j-rest/src/main/java/org/neo4j/rest/graphdb/converter/RestEntityPropertyRefresher.java new file mode 100644 index 000000000..c7f0e6475 --- /dev/null +++ b/spring-data-neo4j-rest/src/main/java/org/neo4j/rest/graphdb/converter/RestEntityPropertyRefresher.java @@ -0,0 +1,38 @@ +/** + * Copyright (c) 2002-2013 "Neo Technology," + * Network Engine for Objects in Lund AB [http://neotechnology.com] + * + * This file is part of Neo4j. + * + * Neo4j 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. + * + * This program 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 this program. If not, see . + */ +package org.neo4j.rest.graphdb.converter; + +import org.neo4j.rest.graphdb.RequestResult; +import org.neo4j.rest.graphdb.entity.RestEntity; + +public class RestEntityPropertyRefresher implements RestResultConverter { + + private final RestEntity entity; + + public RestEntityPropertyRefresher(RestEntity entity){ + this.entity = entity; + } + + @Override + public Object convertFromRepresentation(RequestResult value) { + return this.entity; + } + +} diff --git a/spring-data-neo4j-rest/src/main/java/org/neo4j/rest/graphdb/converter/RestIndexHitsConverter.java b/spring-data-neo4j-rest/src/main/java/org/neo4j/rest/graphdb/converter/RestIndexHitsConverter.java new file mode 100644 index 000000000..1b6f78a93 --- /dev/null +++ b/spring-data-neo4j-rest/src/main/java/org/neo4j/rest/graphdb/converter/RestIndexHitsConverter.java @@ -0,0 +1,49 @@ +/** + * Copyright (c) 2002-2013 "Neo Technology," + * Network Engine for Objects in Lund AB [http://neotechnology.com] + * + * This file is part of Neo4j. + * + * Neo4j 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. + * + * This program 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 this program. If not, see . + */ +package org.neo4j.rest.graphdb.converter; + +import java.util.Collection; + +import org.neo4j.graphdb.PropertyContainer; +import org.neo4j.graphdb.index.IndexHits; + +import org.neo4j.rest.graphdb.RequestResult; +import org.neo4j.rest.graphdb.RestAPI; +import org.neo4j.rest.graphdb.index.SimpleIndexHits; + +/** + * @author mh + * @since 22.09.11 + */ +public class RestIndexHitsConverter implements RestResultConverter { + private final RestAPI restAPI; + private final Class entityType; + + public RestIndexHitsConverter(RestAPI restAPI,Class entityType) { + this.restAPI = restAPI; + this.entityType = entityType; + } + + public IndexHits convertFromRepresentation(RequestResult response) { + Collection hits = (Collection) response.toEntity(); + return new SimpleIndexHits(hits, hits.size(), entityType, restAPI); + } + +} diff --git a/spring-data-neo4j-rest/src/main/java/org/neo4j/rest/graphdb/converter/RestResultConverter.java b/spring-data-neo4j-rest/src/main/java/org/neo4j/rest/graphdb/converter/RestResultConverter.java new file mode 100644 index 000000000..98f37c137 --- /dev/null +++ b/spring-data-neo4j-rest/src/main/java/org/neo4j/rest/graphdb/converter/RestResultConverter.java @@ -0,0 +1,27 @@ +/** + * Copyright (c) 2002-2013 "Neo Technology," + * Network Engine for Objects in Lund AB [http://neotechnology.com] + * + * This file is part of Neo4j. + * + * Neo4j 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. + * + * This program 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 this program. If not, see . + */ +package org.neo4j.rest.graphdb.converter; + +import org.neo4j.rest.graphdb.RequestResult; + +public interface RestResultConverter { + public Object convertFromRepresentation(RequestResult value); + +} diff --git a/spring-data-neo4j-rest/src/main/java/org/neo4j/rest/graphdb/converter/RestTableResultExtractor.java b/spring-data-neo4j-rest/src/main/java/org/neo4j/rest/graphdb/converter/RestTableResultExtractor.java new file mode 100644 index 000000000..256d94fc1 --- /dev/null +++ b/spring-data-neo4j-rest/src/main/java/org/neo4j/rest/graphdb/converter/RestTableResultExtractor.java @@ -0,0 +1,75 @@ +/** + * Copyright (c) 2002-2013 "Neo Technology," + * Network Engine for Objects in Lund AB [http://neotechnology.com] + * + * This file is part of Neo4j. + * + * Neo4j 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. + * + * This program 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 this program. If not, see . + */ +package org.neo4j.rest.graphdb.converter; + +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +import org.neo4j.rest.graphdb.RequestResult; +import org.neo4j.rest.graphdb.util.JsonHelper; + +public class RestTableResultExtractor implements RestResultConverter{ + + private final RestEntityExtractor restEntityExtractor; + + public RestTableResultExtractor(RestEntityExtractor restEntityExtractor) { + this.restEntityExtractor = restEntityExtractor; + } + + + public List> extract(Map restResult) { + List columns = (List) restResult.get("columns"); + return extractData(restResult, columns); + } + + private List> extractData(Map restResult, List columns) { + List> rows = (List>) restResult.get("data"); + List> result = new ArrayList>(rows.size()); + for (List row : rows) { + result.add(mapRow(columns, row)); + } + return result; + } + + private Map mapRow(List columns, List row) { + int columnCount = columns.size(); + Map newRow = new HashMap(columnCount); + for (int i = 0; i < columnCount; i++) { + final Object value = row.get(i); + newRow.put(columns.get(i), restEntityExtractor.convertFromRepresentation(value)); + } + return newRow; + } + + @Override + public Object convertFromRepresentation(RequestResult value) { + return extract(toMap(value)); + } + + public boolean canHandle(Object restResult){ + return restResult instanceof Map && ((Map)restResult).containsKey("columns") && ((Map)restResult).containsKey("data"); + } + + public Map toMap(RequestResult requestResult) { + return requestResult.toMap(); + } +} \ No newline at end of file diff --git a/spring-data-neo4j-rest/src/main/java/org/neo4j/rest/graphdb/converter/ResultTypeConverter.java b/spring-data-neo4j-rest/src/main/java/org/neo4j/rest/graphdb/converter/ResultTypeConverter.java new file mode 100644 index 000000000..c44dc3c3c --- /dev/null +++ b/spring-data-neo4j-rest/src/main/java/org/neo4j/rest/graphdb/converter/ResultTypeConverter.java @@ -0,0 +1,155 @@ +/** + * Copyright (c) 2002-2013 "Neo Technology," + * Network Engine for Objects in Lund AB [http://neotechnology.com] + * + * This file is part of Neo4j. + * + * Neo4j 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. + * + * This program 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 this program. If not, see . + */ +package org.neo4j.rest.graphdb.converter; + +import java.util.ArrayList; +import java.util.HashMap; +import java.util.Iterator; +import java.util.List; +import java.util.Map; + +import org.neo4j.rest.graphdb.RestAPI; +import org.neo4j.rest.graphdb.RestResultException; +import org.neo4j.rest.graphdb.traversal.RestPathParser; + +/** + * User: KBurchardi + * Date: 18.10.11 + * Time: 17:55 + */ +public class ResultTypeConverter { + + private RestAPI restAPI; + private RestEntityExtractor restEntityExtractor; + + public ResultTypeConverter(RestAPI restAPI) { + this.restAPI = restAPI; + this.restEntityExtractor = new RestEntityExtractor(this.restAPI); + } + + public Object convertToResultType(Object resultObject, TypeInformation typeInformation){ + if (typeInformation.isSingleType() || typeInformation.isPath(typeInformation.type)) { + return convertSingleTypeToResultType(resultObject, typeInformation, typeInformation.type); + } + if (typeInformation.isCollection()){ + ArrayList result = new ArrayList(); + for (Object innerObject : toIterable(resultObject)) { + result.add(convertSingleTypeToResultType(innerObject, typeInformation, typeInformation.genericArguments[0])); + } + return result; + }if (typeInformation.isMap()){ + Map originalMap = toMap(resultObject); + HashMap result = new HashMap(((Map)resultObject).size()); + for (Map.Entry entry : originalMap.entrySet()) { + Object resultKey = convertSingleTypeToResultType(entry.getKey(), typeInformation, typeInformation.genericArguments[0]); + Object resultValue = convertSingleTypeToResultType(entry.getValue(), typeInformation, typeInformation.genericArguments[1]); + result.put(resultKey, resultValue); + } + return result; + } + throw new RestResultException("could not convert Type "+ resultObject.getClass().getName()+" to Type "+typeInformation.type.getName()); + + } + + private Iterable toIterable(Object resultObject){ + if (Iterable.class.isAssignableFrom(resultObject.getClass())){ + return (Iterable)resultObject; + }else{ + final RestTableResultExtractor extractor = new RestTableResultExtractor(new RestEntityExtractor(this.restAPI)); + if (extractor.canHandle(resultObject)){ + final List> data = extractor.extract((Map)resultObject); + return (Iterable)data; + } + } + throw new RestResultException("could not convert Type "+ resultObject.getClass().getName()+" to Iterable"); + } + + private Map toMap (Object resultObject){ + if (Map.class.isAssignableFrom(resultObject.getClass())){ + return (Map)resultObject; + } + throw new RestResultException("could not convert Type "+ resultObject.getClass().getName()+" to Map"); + } + + + + private Object convertSingleTypeToResultType(Object resultObject, TypeInformation typeInformation, Class singleObjectType){ + if (typeInformation.isInstance(resultObject, singleObjectType)){ + return resultObject; + }else{ + if (typeInformation.isGraphEntity(singleObjectType)){ + if (restEntityExtractor.canHandle(resultObject)){ + return restEntityExtractor.convertFromRepresentation(resultObject); + } + } + + if (typeInformation.isPath(singleObjectType) && resultObject instanceof Map){ + return RestPathParser.parse((Map)resultObject, restAPI); + } + + ConversionInfo resultInfo = convertFromCollectionType(resultObject, singleObjectType); + if (resultInfo.isSuccessfulConversion()){ + return resultInfo.getConversionData(); + } + } + throw new RestResultException("could not convert Type "+ resultObject.getClass().getName()+" to Type "+typeInformation.type.getName()); + } + + private ConversionInfo convertFromCollectionType(Object resultObject, Class singleObjectType){ + TypeInformation resultTypeInfo = new TypeInformation(resultObject); + + if (resultTypeInfo.isCollectionType()){ + if (resultTypeInfo.isCollection()){ + Iterable resultIterable = (Iterable) resultObject; + if(!resultIterable.iterator().hasNext()){ + return new ConversionInfo(null,true); + }else{ + if (iterableHasSingleElement(resultIterable) && resultTypeInfo.getGenericArguments()[0].equals(singleObjectType)){ + return new ConversionInfo(resultIterable.iterator().next(), true); + } + } + }else{ + Map resultMap = (Map)resultObject; + if (resultMap.isEmpty()){ + return new ConversionInfo(null,true); + }else{ + if (resultMap.size() == 1 && resultTypeInfo.getGenericArguments()[1].equals(singleObjectType) ){ + return new ConversionInfo(resultMap.values().iterator().next(), true); + } + } + } + } + return new ConversionInfo(null,false); + } + + public boolean iterableHasSingleElement(Iterable object){ + Iterator it = object.iterator(); + if (!it.hasNext()){ + return false; + } + + it.next(); + if (it.hasNext()){ + return false; + } + return true; + + } +} diff --git a/spring-data-neo4j-rest/src/main/java/org/neo4j/rest/graphdb/converter/TypeInformation.java b/spring-data-neo4j-rest/src/main/java/org/neo4j/rest/graphdb/converter/TypeInformation.java new file mode 100644 index 000000000..7a15a1314 --- /dev/null +++ b/spring-data-neo4j-rest/src/main/java/org/neo4j/rest/graphdb/converter/TypeInformation.java @@ -0,0 +1,125 @@ +/** + * Copyright (c) 2002-2013 "Neo Technology," + * Network Engine for Objects in Lund AB [http://neotechnology.com] + * + * This file is part of Neo4j. + * + * Neo4j 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. + * + * This program 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 this program. If not, see . + */ +package org.neo4j.rest.graphdb.converter; + +import java.lang.reflect.ParameterizedType; +import java.lang.reflect.Type; +import java.util.Map; + +import org.neo4j.graphdb.Node; +import org.neo4j.graphdb.Path; +import org.neo4j.graphdb.Relationship; + +/** +* User: KBurchardi +* Date: 18.10.11 +* Time: 17:16 +*/ +public class TypeInformation { + + Class type; + Class[] genericArguments; + + public TypeInformation(Type type) { + this.type = convertToClass(type); + this.genericArguments = extractGenericArguments(type); + } + + public TypeInformation(Object object){ + this.type = object.getClass(); + this.genericArguments = extractGenericArgumentsFromObject(object); + } + + private Class convertToClass(Type type) { + if (type instanceof Class) { + return (Class) type; + }else{ + return(Class)((ParameterizedType)type).getRawType(); + } + } + + public boolean isSingleType(){ + return !isCollectionType(); + } + + public boolean isCollectionType() { + return (isCollection() || isMap()); + } + + public boolean isMap() { + return Map.class.isAssignableFrom(this.type); + } + + public boolean isCollection() { + return Iterable.class.isAssignableFrom(this.type); + } + + private Class[] extractGenericArgumentsFromObject(Object object){ + if (isCollectionType()){ + if(isCollection()){ + if(((Iterable)object).iterator().hasNext()){ + return new Class[]{((Iterable)object).iterator().next().getClass()}; + } + }else{ + if (!((Map)object).isEmpty()){ + return new Class[]{((Map)object).keySet().iterator().next().getClass(), ((Map)object).values().iterator().next().getClass()}; + } + } + } + return null; + } + + + + + private Class[] extractGenericArguments(Type type) { + + if (!(type instanceof ParameterizedType)) { + return null; + } + ParameterizedType parameterizedType = (ParameterizedType) type; + Type[] actualTypeArguments = parameterizedType.getActualTypeArguments(); + Class[]result = new Class[actualTypeArguments.length]; + for (int i = 0; i < actualTypeArguments.length; i++) { + result[i] = convertToClass(actualTypeArguments[i]); + } + return result; + } + + public boolean isInstance(Object resultObject, Class type) { + return type.isInstance(resultObject); + } + + public boolean isGraphEntity(Class classType) { + return Node.class.isAssignableFrom(classType)|| Relationship.class.isAssignableFrom(classType); + } + + public boolean isPath(Class classType){ + return Path.class.isAssignableFrom(classType); + } + + public Class getType() { + return type; + } + + public Class[] getGenericArguments() { + return genericArguments; + } +} diff --git a/spring-data-neo4j-rest/src/main/java/org/neo4j/rest/graphdb/entity/RestEntity.java b/spring-data-neo4j-rest/src/main/java/org/neo4j/rest/graphdb/entity/RestEntity.java new file mode 100644 index 000000000..f2a2601d4 --- /dev/null +++ b/spring-data-neo4j-rest/src/main/java/org/neo4j/rest/graphdb/entity/RestEntity.java @@ -0,0 +1,254 @@ +/** + * Copyright (c) 2002-2013 "Neo Technology," + * Network Engine for Objects in Lund AB [http://neotechnology.com] + * + * This file is part of Neo4j. + * + * Neo4j 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. + * + * This program 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 this program. If not, see . + */ +package org.neo4j.rest.graphdb.entity; + + +import java.net.URI; +import java.util.Collection; +import java.util.LinkedHashMap; +import java.util.Map; + +import org.neo4j.graphdb.NotFoundException; +import org.neo4j.graphdb.PropertyContainer; +import org.neo4j.helpers.collection.IterableWrapper; +import org.neo4j.rest.graphdb.*; +import org.neo4j.rest.graphdb.util.ArrayConverter; +import org.springframework.data.neo4j.core.UpdateableState; + +import static org.neo4j.helpers.collection.MapUtil.map; + +public abstract class RestEntity implements PropertyContainer, UpdatableRestResult, UpdateableState { + private Map structuralData; + protected Map propertyData; + private long lastTimeFetchedPropertyData; + protected RestAPI restApi; + private Long id; + + private final ArrayConverter arrayConverter=new ArrayConverter(); + private String uri; + + public RestEntity( URI uri, RestAPI restApi ) { + this( uri.toString(), restApi ); + } + + public RestEntity( String uri, RestAPI restApi ) { + this.uri = uri; + this.id = getEntityId(uri); + this.restApi = restApi; + } + + public RestEntity( Map data, RestAPI restApi ) { + this.restApi = restApi; + this.structuralData = data; + this.uri = (String) data.get( "self" ); + this.id = getEntityId(uri); + setProperties((Map) data.get("data")); + } + + public RestEntity(long id, Map restData, RestAPI facade) { + this.restApi = facade; + this.structuralData = restData; + this.uri = nodeUri(facade,id); + this.id = id; + setProperties((Map) structuralData.get("data")); + } + + public static String nodeUri(RestAPI facade, long id) { + return facade.getBaseUri()+"/node/" + id; + } + + public String getUri() { + return uri; + } + + public void updateFrom(RestEntity updateEntity, RestAPI restApi){ +// if (this == updateEntity){ +// this.lastTimeFetchedPropertyData = 0; +// } + this.uri = updateEntity.getUri(); + this.id = getEntityId(uri); + this.structuralData = updateEntity.getStructuralData(); + if (updateEntity.lastTimeFetchedPropertyData > 0 && updateEntity.propertyData != null) { + setProperties(updateEntity.propertyData); + } + } + + Map getStructuralData() { +// if ( this.structuralData == null ) { +// this.structuralData = restApi.getData(this); +// } + return this.structuralData; + } + + Map getPropertyData() { + if (hasToUpdateProperties()) { + doUpdate(); + } + return this.propertyData; + } + + protected abstract void doUpdate(); + + protected void setProperties(Map properties) { + tracking = false; + this.propertyData = properties; + this.lastTimeFetchedPropertyData = System.currentTimeMillis(); + } + + protected boolean hasToUpdateProperties() { + if (tracking) return false; + if (this.propertyData == null) return true; + return restApi.hasToUpdate(this.lastTimeFetchedPropertyData); + } + + + public Object getProperty( String key ) { + Object value = getPropertyValue(key); + if ( value == null ) { + throw new NotFoundException( "'" + key + "' on " + this ); + } + return value; + } + + private Object getPropertyValue( String key ) { + Map properties = getPropertyData(); + Object value = properties.get( key ); + if ( value == null) return null; + if ( value instanceof Collection ) { + Collection col= (Collection) value; + if (col.isEmpty()) return new String[0]; // todo concrete value type ? + Object result = arrayConverter.toArray( col ); + if (result == null) throw new IllegalStateException( "Could not determine type of property "+key ); + properties.put(key,result); + return result; + + } + return PropertiesMap.assertSupportedPropertyValue( value ); + } + + public Object getProperty( String key, Object defaultValue ) { + Object value = getPropertyValue( key ); + return value != null ? value : defaultValue; + } + + @SuppressWarnings("unchecked") + public Iterable getPropertyKeys() { + return new IterableWrapper( getPropertyData().keySet() ) { + @Override + protected String underlyingObjectToObject( Object key ) { + return key.toString(); + } + }; + } + + @SuppressWarnings("unchecked") + public Iterable getPropertyValues() { + return (Iterable) getPropertyData().values(); + } + + public boolean hasProperty( String key ) { + return getPropertyData().containsKey( key ); + } + + public Object removeProperty( String key ) { + Object value = getProperty( key, null ); + if (!tracking) restApi.removeProperty(this, key); + if (this.propertyData != null ) this.propertyData.remove(key); + return value; + } + + public void setProperty( String key, Object value ) { + if (!tracking) this.restApi.setPropertyOnEntity(this, key, value); + if (this.propertyData == null) this.propertyData = new LinkedHashMap<>(); + this.propertyData.put(key,value); + } + + private boolean tracking; + + @Override + public void flush() { + if (!tracking) return; + if (propertyData != null && !propertyData.isEmpty()) { + this.restApi.setPropertiesOnEntity(this,propertyData); + } + tracking = false; + } + + @Override + public void track() { + this.tracking = true; + } + + public static long getEntityId( String uri ) { + if (uri.startsWith("{")) return -1; + return Long.parseLong(uri.substring(uri.lastIndexOf('/') + 1)); + } + + public long getId() { + return id; + } + + public void delete() { + this.restApi.deleteEntity(this); + } + + @Override + public int hashCode() { + return (int) getId(); + } + + @Override + public boolean equals( Object o ) { + if (o == null) return false; + return getClass().equals( o.getClass() ) && getId() == ( (RestEntity) o ).getId(); + } + + + public RestGraphDatabase getGraphDatabase() { + return new RestGraphDatabase(restApi); + } + + @Override + public String toString() { + return getUri(); + } + + public RestAPI getRestApi() { + return restApi; + } + + public void setLastTimeFetchedPropertyData(long lastTimeFetchedPropertyData) { + this.lastTimeFetchedPropertyData = lastTimeFetchedPropertyData; + } + + @Override + public void refresh() { + doUpdate(); + } + + @Override + public void addPropertiesBatch(Map properties) { + setProperties(properties); + restApi.setPropertiesOnEntity(this, propertyData); + } + + @Override + public void addAllLabelsBatch(Collection labels) { } +} diff --git a/spring-data-neo4j-rest/src/main/java/org/neo4j/rest/graphdb/entity/RestEntityCache.java b/spring-data-neo4j-rest/src/main/java/org/neo4j/rest/graphdb/entity/RestEntityCache.java new file mode 100644 index 000000000..7e03655f5 --- /dev/null +++ b/spring-data-neo4j-rest/src/main/java/org/neo4j/rest/graphdb/entity/RestEntityCache.java @@ -0,0 +1,42 @@ +package org.neo4j.rest.graphdb.entity; + +import org.neo4j.kernel.impl.cache.LruCache; +import org.neo4j.rest.graphdb.RestAPI; + +/** + * @author mh + * @since 21.09.14 + */ +public class RestEntityCache { + + private LruCache lruCache = new LruCache<>("RestNode",10000); + + private final RestAPI restAPI; + + public RestEntityCache(RestAPI restAPI) { + this.restAPI = restAPI; + } + + public RestNode addToCache(RestNode node) { + if (node == null) return null; + long id = node.getId(); + if (id != -1) { + RestNode existing = lruCache.get(id); + if (existing !=null) { + if (existing != node) existing.updateFrom(node, restAPI); + return existing; + } else { + lruCache.put(id, node); + } + } + return node; + } + + public RestNode getNode(long id) { + return lruCache.get(id); + } + + public void remove(long id) { + lruCache.remove(id); + } +} diff --git a/spring-data-neo4j-rest/src/main/java/org/neo4j/rest/graphdb/entity/RestNode.java b/spring-data-neo4j-rest/src/main/java/org/neo4j/rest/graphdb/entity/RestNode.java new file mode 100644 index 000000000..2b466336a --- /dev/null +++ b/spring-data-neo4j-rest/src/main/java/org/neo4j/rest/graphdb/entity/RestNode.java @@ -0,0 +1,237 @@ +/** + * Copyright (c) 2002-2013 "Neo Technology," + * Network Engine for Objects in Lund AB [http://neotechnology.com] + * + * This file is part of Neo4j. + * + * Neo4j 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. + * + * This program 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 this program. If not, see . + */ +package org.neo4j.rest.graphdb.entity; + +import static java.util.Arrays.asList; +import static org.neo4j.helpers.collection.MapUtil.map; +import static org.neo4j.rest.graphdb.ExecutingRestRequest.encode; + +import java.net.URI; +import java.util.*; + +import org.neo4j.graphdb.*; +import org.neo4j.graphdb.Traverser.Order; +import org.neo4j.helpers.collection.CombiningIterable; +import org.neo4j.helpers.collection.IterableWrapper; +import org.neo4j.helpers.collection.IteratorUtil; +import org.neo4j.rest.graphdb.RestAPI; +import org.neo4j.rest.graphdb.RestAPIInternal; +import org.neo4j.rest.graphdb.traversal.RestDirection; +import org.neo4j.rest.graphdb.util.ResourceIterableWrapper; + +public class RestNode extends RestEntity implements Node { + + public RestNode( URI uri, RestAPI restApi ) { + super( uri, restApi ); + } + + public RestNode( String uri, RestAPI restApi ) { + super( uri, restApi ); + } + + @SuppressWarnings("unchecked") + public RestNode( Map data, RestAPI restApi ) { + super(data, restApi); + if (data.containsKey("metadata")) { + setLabels((Collection)((Map)data.get("metadata")).get("labels")); + } + } + + public RestNode(long id, Collection labels, Map restData, RestAPI facade) { + super(id,restData,facade); + setLabels(labels); + } + + public static RestNode fromCypher(long id, Collection labels, Map props, RestAPI facade) { + Map restData = map("data", props, "self", RestNode.nodeUri(facade, id));//,"metadata",map("id",String.valueOf("id"),"labels",labels) + return new RestNode(id, labels, restData, facade); + } + + @Override + protected void doUpdate() { + updateFrom(restApi.getNodeById(getId(), RestAPIInternal.Load.ForceFromServer), restApi); + } + + @Override + public void updateFrom(RestEntity entity, RestAPI restApi) { + super.updateFrom(entity, restApi); + RestNode node = (RestNode) entity; + if (node.lastLabelFetchTime > 0 && node.labels != null) { + setLabels(node.labels); + } + } + + public Relationship createRelationshipTo( Node toNode, RelationshipType type ) { + return this.restApi.createRelationship(this, toNode, type, null); + } + + @Override + public Iterable getRelationshipTypes() { + return this.restApi.getRelationshipTypes(this); + } + + @Override + public int getDegree() { + return this.restApi.getDegree(this, null, Direction.BOTH); + } + + @Override + public int getDegree(RelationshipType type) { + return this.restApi.getDegree(this,type,Direction.BOTH); + } + + @Override + public int getDegree(Direction direction) { + return this.restApi.getDegree(this,null,direction); + } + + @Override + public int getDegree(RelationshipType type, Direction direction) { + return this.restApi.getDegree(this,type,direction); + } + + public Iterable getRelationships() { + return restApi.getRelationships(this, Direction.BOTH ); + } + + public Iterable getRelationships( RelationshipType... types ) { + return restApi.getRelationships(this, Direction.BOTH, types); + } + + public Iterable getRelationships( Direction direction ) { + return restApi.getRelationships(this, direction); + } + + public Iterable getRelationships( RelationshipType type, + Direction direction ) { + return restApi.getRelationships(this, direction, type); + } + + public Relationship getSingleRelationship( RelationshipType type, + Direction direction ) { + return IteratorUtil.singleOrNull( getRelationships( type, direction ) ); + } + + public boolean hasRelationship() { + return getRelationships().iterator().hasNext(); + } + + public boolean hasRelationship( RelationshipType... types ) { + return getRelationships( types ).iterator().hasNext(); + } + + public boolean hasRelationship( Direction direction ) { + return getRelationships( direction ).iterator().hasNext(); + } + + public boolean hasRelationship( RelationshipType type, Direction direction ) { + return getRelationships( type, direction ).iterator().hasNext(); + } + + public Traverser traverse( Order order, StopEvaluator stopEvaluator, + ReturnableEvaluator returnableEvaluator, Object... rels ) { + throw new UnsupportedOperationException(); + } + + public Traverser traverse( Order order, StopEvaluator stopEvaluator, + ReturnableEvaluator returnableEvaluator, RelationshipType type, Direction direction ) { + throw new UnsupportedOperationException(); + } + + public Traverser traverse( Order order, StopEvaluator stopEvaluator, + ReturnableEvaluator returnableEvaluator, RelationshipType type, Direction direction, + RelationshipType secondType, Direction secondDirection ) { + throw new UnsupportedOperationException(); + } + + @Override + public Iterable getRelationships(final Direction direction, RelationshipType... types) { + return new CombiningIterable(new IterableWrapper, RelationshipType>(asList(types)) { + @Override + protected Iterable underlyingObjectToObject(RelationshipType relationshipType) { + return getRelationships(relationshipType,direction); + } + }); + } + + @Override + public boolean hasRelationship(Direction direction, RelationshipType... types) { + for (RelationshipType relationshipType : types) { + if (hasRelationship(relationshipType,direction)) return true; + } + return false; + } + + private Set labels=null; + private long lastLabelFetchTime = 0; + + @Override + public void addLabel(Label label) { + restApi.addLabels(this, Collections.singleton(label.name())); + if (this.labels!=null) this.labels.add(label.name()); + else updateLabels(); + } + + @Override + public void removeLabel(Label label) { + restApi.removeLabel(this,label.name()); + if (this.labels!=null) this.labels.remove(label.name()); + else updateLabels(); + } + + @Override + public boolean hasLabel(Label label) { + updateLabels(); + return this.labels.contains(label.name()); + } + + private void updateLabels() { + if (hasToUpdateLabels()) { + doUpdate(); + } + } + + public void setLabels(Collection labels) { + this.labels = (labels == null) ? new LinkedHashSet() : new LinkedHashSet<>(labels); + this.lastLabelFetchTime = System.currentTimeMillis(); + } + + private boolean hasToUpdateLabels() { + return labels == null || restApi.hasToUpdate(this.lastLabelFetchTime); + } + + @Override + public ResourceIterable