/* * Hibernate OGM, Domain model persistence for NoSQL datastores * * License: GNU Lesser General Public License (LGPL), version 2.1 or later * See the lgpl.txt file in the root directory or <http://www.gnu.org/licenses/lgpl-2.1.html>. */ package org.hibernate.ogm.datastore.neo4j.remote.http.dialect.impl; import java.util.ArrayList; import java.util.Arrays; import java.util.Collection; import java.util.Collections; import java.util.HashMap; import java.util.HashSet; import java.util.Iterator; import java.util.List; import java.util.Map; import java.util.Set; import org.hibernate.HibernateException; import org.hibernate.ogm.datastore.neo4j.dialect.impl.BaseNeo4jEntityQueries; import org.hibernate.ogm.datastore.neo4j.dialect.impl.NodeLabel; import org.hibernate.ogm.datastore.neo4j.remote.common.dialect.impl.RemoteNeo4jAssociationPropertiesRow; import org.hibernate.ogm.datastore.neo4j.remote.common.util.impl.RemoteNeo4jHelper; import org.hibernate.ogm.datastore.neo4j.remote.http.impl.HttpNeo4jClient; import org.hibernate.ogm.datastore.neo4j.remote.http.json.impl.ErrorResponse; import org.hibernate.ogm.datastore.neo4j.remote.http.json.impl.Graph; import org.hibernate.ogm.datastore.neo4j.remote.http.json.impl.Graph.Node; import org.hibernate.ogm.datastore.neo4j.remote.http.json.impl.Graph.Relationship; import org.hibernate.ogm.datastore.neo4j.remote.http.json.impl.Row; import org.hibernate.ogm.datastore.neo4j.remote.http.json.impl.Statement; import org.hibernate.ogm.datastore.neo4j.remote.http.json.impl.StatementResult; import org.hibernate.ogm.datastore.neo4j.remote.http.json.impl.Statements; import org.hibernate.ogm.datastore.neo4j.remote.http.json.impl.StatementsResponse; import org.hibernate.ogm.dialect.query.spi.ClosableIterator; import org.hibernate.ogm.dialect.spi.TupleTypeContext; import org.hibernate.ogm.model.key.spi.EntityKey; import org.hibernate.ogm.model.key.spi.EntityKeyMetadata; import org.hibernate.ogm.util.impl.ArrayHelper; /** * @author Davide D'Alto */ public class HttpNeo4jEntityQueries extends BaseNeo4jEntityQueries { private static final ClosableIteratorAdapter<RemoteNeo4jAssociationPropertiesRow> EMPTY_RELATIONSHIPS = new ClosableIteratorAdapter<>( Collections.<RemoteNeo4jAssociationPropertiesRow>emptyList().iterator() ); private static final ClosableIteratorAdapter<NodeWithEmbeddedNodes> EMPTY_NODES = new ClosableIteratorAdapter<>( Collections.<NodeWithEmbeddedNodes>emptyList().iterator() ); public HttpNeo4jEntityQueries(EntityKeyMetadata entityKeyMetadata) { this( entityKeyMetadata, null ); } public HttpNeo4jEntityQueries(EntityKeyMetadata entityKeyMetadata, TupleTypeContext tupleTypeContext) { super( entityKeyMetadata, tupleTypeContext, true ); } public NodeWithEmbeddedNodes findEntity(HttpNeo4jClient executionEngine, Long transactionId, Object[] columnValues) { Map<String, Object> params = params( columnValues ); Statements statements = new Statements(); statements.addStatement( getFindEntityQuery(), params, Statement.AS_GRAPH ); List<StatementResult> queryResult = executeQuery( executionEngine, transactionId, statements ); if ( queryResult != null ) { Node owner = findOwner( queryResult ); Map<String, Collection<Node>> embeddedNodesMap = new HashMap<>(); List<Row> rows = queryResult.get( 0 ).getData(); for ( Row row : rows ) { Graph graph = row.getGraph(); if ( graph.getNodes().size() > 0 ) { updateEmbeddedNodesMap( embeddedNodesMap, graph.getNodes(), graph.getRelationships(), owner ); } } return new NodeWithEmbeddedNodes( owner, embeddedNodesMap ); } return null; } private Node findOwner(List<StatementResult> queryResult) { Graph graph = queryResult.get( 0 ).getData().get( 0 ).getGraph(); Node owner = findEntity( graph.getNodes() ); return owner; } private Node findEntity(List<Node> nodes) { for ( Node node : nodes ) { if ( node.getLabels().contains( NodeLabel.ENTITY.name() ) ) { return node; } } return null; } private void updateEmbeddedNodesMap(Map<String, Collection<Node>> embeddedNodesMap, List<Node> allNodes, List<Relationship> embeddedRelationships, Node owner) { if ( embeddedRelationships.size() > 0 ) { Relationship currentRelationship = findRelationship( embeddedRelationships, owner.getId() ); if ( currentRelationship != null ) { StringBuilder builder = new StringBuilder(); Node currentNode = null; while ( currentRelationship != null ) { builder.append( "." ); builder.append( currentRelationship.getType() ); currentNode = findEmbeddedNode( allNodes, currentRelationship.getEndNode() ); currentRelationship = findRelationship( embeddedRelationships, currentRelationship.getEndNode() ); } String path = builder.substring( 1 ); saveEmbeddedNode( embeddedNodesMap, path, currentNode ); } } } private Relationship findRelationship(List<Relationship> relationships, Long startNodeId) { for ( Relationship relationship : relationships ) { if ( relationship.getStartNode().equals( startNodeId ) ) { return relationship; } } return null; } private void saveEmbeddedNode(Map<String, Collection<Node>> embeddedNodesMap, String path, Node embeddedNode) { if ( !embeddedNode.getProperties().isEmpty() ) { if ( embeddedNodesMap.containsKey( path ) ) { embeddedNodesMap.get( path ).add( embeddedNode ); } else { Set<Node> embeddedNodes = new HashSet<>(); embeddedNodes.add( embeddedNode ); embeddedNodesMap.put( path, embeddedNodes ); } } } private Node findEmbeddedNode(List<Node> allNodes, Long embeddedNodeId) { for ( Node node : allNodes ) { if ( node.getId().equals( embeddedNodeId ) ) { return node; } } return null; } public Node findAssociatedEntity(HttpNeo4jClient neo4jClient, Long txId, Object[] keyValues, String associationrole) { Map<String, Object> params = params( keyValues ); String query = getFindAssociatedEntityQuery( associationrole ); if ( query != null ) { Graph result = executeQueryAndReturnGraph( neo4jClient, txId, query, params ); if ( result != null ) { if ( result.getNodes().size() > 0 ) { return result.getNodes().get( 0 ); } } } return null; } public Statement getCreateEntityWithPropertiesQueryStatement(Object[] columnValues, Map<String, Object> properties) { String query = getCreateEntityWithPropertiesQuery(); Map<String, Object> params = Collections.singletonMap( "props", (Object) properties ); return new Statement( query, params ); } public Statement removeColumnStatement(Object[] columnValues, String column) { String query = getRemoveColumnQuery( column ); Map<String, Object> params = params( columnValues ); return new Statement( query, params ); } public Statement getUpdateEntityPropertiesStatement(Object[] columnvalues, Map<String, Object> properties) { String query = getUpdateEntityPropertiesQuery( properties ); Object[] paramsValues = ArrayHelper.concat( Arrays.asList( columnvalues, new Object[properties.size()] ) ); int index = columnvalues.length; for ( Map.Entry<String, Object> entry : properties.entrySet() ) { paramsValues[index++] = entry.getValue(); } return new Statement( query, params( paramsValues ) ); } public void removeEntity(HttpNeo4jClient executionEngine, Long txId, Object[] columnValues) { executeQueryAndReturnGraph( executionEngine, txId, getRemoveEntityQuery(), params( columnValues ) ); } public ClosableIterator<NodeWithEmbeddedNodes> findEntitiesWithEmbedded(HttpNeo4jClient executionEngine, Long txId) { Statements statements = new Statements(); statements.addStatement( getFindEntitiesQuery() ); List<StatementResult> result = executeQuery( executionEngine, txId, statements ); return closableIterator( result ); } /** * Find the nodes corresponding to an array of entity keys. * * @param executionEngine the {@link HttpNeo4jClient} used to run the query * @param keys an array of keys identifying the nodes to return * @return the list of nodes representing the entities */ public ClosableIterator<NodeWithEmbeddedNodes> findEntities(HttpNeo4jClient executionEngine, EntityKey[] keys, Long txId) { if ( singlePropertyKey ) { return singlePropertyIdFindEntities( executionEngine, keys, txId ); } else { return multiPropertiesIdFindEntities( executionEngine, keys, txId ); } } /* * When the id is mapped on several properties */ private ClosableIterator<NodeWithEmbeddedNodes> multiPropertiesIdFindEntities(HttpNeo4jClient executionEngine, EntityKey[] keys, Long txId) { String query = getMultiGetQueryCacheQuery( keys ); Map<String, Object> params = multiGetParams( keys ); List<StatementResult> results = executeQuery( executionEngine, txId, query, params ); return closableIterator( results ); } /* * When the id is mapped with a single property */ private ClosableIterator<NodeWithEmbeddedNodes> singlePropertyIdFindEntities(HttpNeo4jClient executionEngine, EntityKey[] keys, Long txId) { Object[] paramsValues = new Object[keys.length]; for ( int i = 0; i < keys.length; i++ ) { paramsValues[i] = keys[i].getColumnValues()[0]; } Map<String, Object> params = Collections.singletonMap( "0", (Object) paramsValues ); Statements statements = new Statements(); statements.addStatement( multiGetQuery, params, Statement.AS_GRAPH ); List<StatementResult> results = executeQuery( executionEngine, txId, statements ); return closableIterator( results, keys ); } private ClosableIterator<NodeWithEmbeddedNodes> closableIterator(List<StatementResult> results) { return closableIterator( results, null ); } private ClosableIterator<NodeWithEmbeddedNodes> closableIterator(List<StatementResult> results, EntityKey[] keys) { if ( results != null ) { List<Row> data = results.get( 0 ).getData(); if ( data.size() > 0 ) { List<Node> owners = new ArrayList<>(); Map<Long, Map<String, Collection<Node>>> nodes = new HashMap<>(); for ( Row row : data ) { if ( row.getGraph().getNodes().size() > 0 ) { Node owner = findEntity( row.getGraph().getNodes() ); Map<String, Collection<Node>> embeddedNodesMap = nodes.get( owner.getId() ); if ( embeddedNodesMap == null ) { embeddedNodesMap = new HashMap<>(); nodes.put( owner.getId(), embeddedNodesMap ); owners.add( owner ); } updateEmbeddedNodesMap( embeddedNodesMap, row.getGraph().getNodes(), row.getGraph().getRelationships(), owner ); } } if ( keys == null ) { List<NodeWithEmbeddedNodes> nodeWithEmbeddeds = new ArrayList<>(); for ( Node owner : owners ) { nodeWithEmbeddeds.add( new NodeWithEmbeddedNodes( owner, nodes.get( owner.getId() ) ) ); } return new ClosableIteratorAdapter<>( nodeWithEmbeddeds.iterator() ); } else { NodeWithEmbeddedNodes[] array = new NodeWithEmbeddedNodes[keys.length]; for ( Node owner : owners ) { int index = findKeyIndex( keys, owner ); if ( index > -1 ) { array[index] = new NodeWithEmbeddedNodes( owner, nodes.get( owner.getId() ) ); } } List<NodeWithEmbeddedNodes> nullRemoved = new ArrayList<>(); for ( NodeWithEmbeddedNodes node : array ) { if ( node != null ) { nullRemoved.add( node ); } } return new ClosableIteratorAdapter<>( nullRemoved.iterator() ); } } } return EMPTY_NODES; } private int findKeyIndex(EntityKey[] keys, Node owner) { for ( int i = 0; i < keys.length; i++ ) { if ( RemoteNeo4jHelper.matches( owner.getProperties(), keys[i].getColumnNames(), keys[i].getColumnValues() ) ) { return i; } } return -1; } public Statement getUpdateOneToOneAssociationStatement(String associationRole, Object[] ownerKeyValues, Object[] targetKeyValues) { String query = getUpdateToOneQuery( associationRole ); Map<String, Object> params = params( ownerKeyValues ); params.putAll( params( targetKeyValues, ownerKeyValues.length ) ); return new Statement( query, params ); } private Graph executeQueryAndReturnGraph(HttpNeo4jClient executionEngine, Long txId, String query, Map<String, Object> properties, String... dataContents) { List<StatementResult> results = executeQuery( executionEngine, txId, query, properties, dataContents ); if ( results == null ) { return null; } return row( results ).getGraph(); } private List<StatementResult> executeQuery(HttpNeo4jClient executionEngine, Long txId, String query, Map<String, Object> properties, String... dataContents) { Statements statements = new Statements(); statements.addStatement( query, properties, dataContents ); return executeQuery( executionEngine, txId, statements ); } private List<StatementResult> executeQuery(HttpNeo4jClient executionEngine, Long txId, Statements statements) { StatementsResponse statementsResponse = txId == null ? executionEngine.executeQueriesInNewTransaction( statements ) : executionEngine.executeQueriesInOpenTransaction( txId, statements ); validate( statementsResponse ); List<StatementResult> results = statementsResponse.getResults(); if ( results == null || results.isEmpty() ) { return null; } if ( results.get( 0 ).getData().isEmpty() ) { return null; } return results; } /** * When we execute a single statement we only need the corresponding Row with the result. * * @param results a list of {@link StatementResult} * @return the result of a single query */ private static Row row(List<StatementResult> results) { Row row = results.get( 0 ).getData().get( 0 ); return row; } private void validate(StatementsResponse statementsResponse) { if ( !statementsResponse.getErrors().isEmpty() ) { ErrorResponse errorResponse = statementsResponse.getErrors().get( 0 ); throw new HibernateException( String.valueOf( errorResponse ) ); } } public Statement updateEmbeddedColumnStatement(Object[] keyValues, String column, Object value) { String query = getUpdateEmbeddedColumnQuery( keyValues, column ); Map<String, Object> params = params( ArrayHelper.concat( keyValues, value, value ) ); return new Statement( query, params ); } public Statement removeEmbeddedColumnStatement(Object[] keyValues, String embeddedColumn) { String query = getRemoveEmbeddedPropertyQuery().get( embeddedColumn ); Map<String, Object> params = params( keyValues ); return new Statement( query, params ); } public Statement removeEmptyEmbeddedNodesStatement(Object[] keyValues, String embeddedColumn) { String query = getRemoveEmbeddedPropertyQuery().get( embeddedColumn ); Map<String, Object> params = params( keyValues ); return new Statement( query, params ); } @SuppressWarnings("unchecked") public ClosableIterator<RemoteNeo4jAssociationPropertiesRow> findAssociation(HttpNeo4jClient executionEngine, Long txId, Object[] columnValues, String role) { // Find the target node String queryForAssociation = getFindAssociationQuery( role ); // Find the embedded properties of the target node String queryForEmbedded = getFindAssociationTargetEmbeddedValues( role ); // Execute the queries Map<String, Object> params = params( columnValues ); Statements statements = new Statements(); statements.addStatement( queryForAssociation, params, Statement.AS_ROW ); statements.addStatement( queryForEmbedded, params, Statement.AS_ROW ); List<StatementResult> response = executeQuery( executionEngine, txId, statements ); if ( response != null ) { List<Row> data = response.get( 0 ).getData(); List<Row> embeddedNodes = response.get( 1 ).getData(); int embeddedNodesIndex = 0; List<RemoteNeo4jAssociationPropertiesRow> responseRows = new ArrayList<>( data.size() ); for ( int i = 0; i < data.size(); i++ ) { String idTarget = String.valueOf( data.get( i ).getRow().get( 0 ) ); // Read the properties of the owner, the target and the relationship that joins them Map<String, Object> rel = (Map<String, Object>) data.get( i ).getRow().get( 1 ); Map<String, Object> ownerNode = (Map<String, Object>) data.get( i ).getRow().get( 2 ); Map<String, Object> targetNode = (Map<String, Object>) data.get( i ).getRow().get( 3 ); // Read the embedded column and add them to the target node while ( embeddedNodesIndex < embeddedNodes.size() ) { Row row = embeddedNodes.get( embeddedNodesIndex ); String embeddedOwnerId = row.getRow().get( 0 ).toString(); if ( embeddedOwnerId.equals( idTarget ) ) { addTargetEmbeddedProperties( targetNode, row ); embeddedNodesIndex++; } else { break; } } RemoteNeo4jAssociationPropertiesRow associationPropertiesRow = new RemoteNeo4jAssociationPropertiesRow( rel, ownerNode, targetNode ); responseRows.add( associationPropertiesRow ); } if ( responseRows.isEmpty() ) { return EMPTY_RELATIONSHIPS; } return new ClosableIteratorAdapter<>( responseRows.iterator() ); } return EMPTY_RELATIONSHIPS; } @SuppressWarnings("unchecked") private void addTargetEmbeddedProperties(Map<String, Object> targetNode, Row row) { List<String> pathToNode = (List<String>) row.getRow().get( 1 ); if ( pathToNode != null ) { Map<String, Object> embeddedNodeProperties = (Map<String, Object>) row.getRow().get( 3 ); String path = concat( pathToNode ); for ( Map.Entry<String, Object> entry : embeddedNodeProperties.entrySet() ) { targetNode.put( path + "." + entry.getKey(), entry.getValue() ); } } } private String concat(List<String> pathToNode) { StringBuilder path = new StringBuilder(); for ( String entry : pathToNode ) { path.append( "." ); path.append( entry ); } return path.substring( 1 ); } public Node findEmbeddedNode(HttpNeo4jClient neo4jClient, Long txId, Object[] keyValues, String embeddedPath) { Graph result = executeQueryAndReturnGraph( neo4jClient, txId, getFindEmbeddedNodeQueries().get( embeddedPath ), params( keyValues ) ); if ( result == null ) { return null; } return result.getNodes().get( 0 ); } public void removeToOneAssociation(HttpNeo4jClient executionEngine, Long txId, Object[] columnValues, String associationRole) { Map<String, Object> params = params( ArrayHelper.concat( columnValues, associationRole ) ); executeQueryAndReturnGraph( executionEngine, txId, getRemoveToOneAssociation(), params ); } private static class ClosableIteratorAdapter<T> implements ClosableIterator<T> { private final Iterator<T> iterator; public ClosableIteratorAdapter(Iterator<T> iterator) { this.iterator = iterator; } @Override public boolean hasNext() { return iterator.hasNext(); } @Override public T next() { return iterator.next(); } @Override public void close() { } @Override public void remove() { } } }