/* * 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.dialect.impl; import static org.hibernate.ogm.datastore.neo4j.dialect.impl.NodeLabel.EMBEDDED; import static org.hibernate.ogm.datastore.neo4j.dialect.impl.NodeLabel.ENTITY; import static org.hibernate.ogm.datastore.neo4j.query.parsing.cypherdsl.impl.CypherDSL.escapeIdentifier; import static org.hibernate.ogm.util.impl.EmbeddedHelper.isPartOfEmbedded; import static org.hibernate.ogm.util.impl.EmbeddedHelper.split; import java.util.ArrayList; import java.util.Arrays; import java.util.HashMap; import java.util.List; import java.util.Map; import java.util.Map.Entry; import org.hibernate.ogm.model.key.spi.AssociationKey; import org.hibernate.ogm.model.key.spi.AssociationKeyMetadata; import org.hibernate.ogm.model.key.spi.AssociationKind; import org.hibernate.ogm.model.key.spi.EntityKey; import org.hibernate.ogm.model.key.spi.EntityKeyMetadata; import org.hibernate.ogm.model.key.spi.RowKey; import org.hibernate.ogm.util.impl.ArrayHelper; import org.neo4j.graphdb.GraphDatabaseService; import org.neo4j.graphdb.Relationship; import org.neo4j.graphdb.Result; /** * Container for the queries related to one association family in Neo4j. Unfortunately, we cannot use the same queries * for all associations, as Neo4j does not allow to parameterize on node labels which would be required, as the * association table is stored as a label. * * @author Davide D'Alto */ public abstract class BaseNeo4jAssociationQueries extends BaseNeo4jQueries { protected final EntityKeyMetadata ownerEntityKeyMetadata; protected final String matchOwnerEntityNode; protected final String findRelationshipQuery; protected final String createRelationshipQuery; protected final String removeAssociationQuery; protected final String removeAssociationRowQuery; public BaseNeo4jAssociationQueries(EntityKeyMetadata ownerEntityKeyMetadata, AssociationKeyMetadata associationKeyMetadata) { this.ownerEntityKeyMetadata = ownerEntityKeyMetadata; this.removeAssociationQuery = initRemoveAssociationQuery( ownerEntityKeyMetadata, associationKeyMetadata ); this.removeAssociationRowQuery = initRemoveAssociationRowQuery( ownerEntityKeyMetadata, associationKeyMetadata ); this.findRelationshipQuery = initFindRelationshipQuery( ownerEntityKeyMetadata, associationKeyMetadata ); this.createRelationshipQuery = initCreateRelationshipQuery( ownerEntityKeyMetadata, associationKeyMetadata ); this.matchOwnerEntityNode = initMatchOwnerEntityNode( ownerEntityKeyMetadata ); } /* * Example: MATCH (owner:ENTITY:table {id: {0}}) */ private static String initMatchOwnerEntityNode(EntityKeyMetadata ownerEntityKeyMetadata) { StringBuilder queryBuilder = new StringBuilder(); appendMatchOwnerEntityNode( queryBuilder, ownerEntityKeyMetadata ); return queryBuilder.toString(); } /* * Example with target node: * * MATCH (n:ENTITY:table {id: {0}}) -[r:role] - (t {id: {1}}) * RETURN r * * Example with relationship indexes: * * MATCH (n:ENTITY:table {id: {0}}) -[r:role {index: {1}}] - (t) * RETURN r */ private static String initFindRelationshipQuery(EntityKeyMetadata ownerEntityKeyMetadata, AssociationKeyMetadata associationKeyMetadata) { int offset = 0; StringBuilder queryBuilder = new StringBuilder( "MATCH " ); queryBuilder.append( "(n:" ); queryBuilder.append( ENTITY ); queryBuilder.append( ":" ); appendLabel( ownerEntityKeyMetadata, queryBuilder ); appendProperties( ownerEntityKeyMetadata, queryBuilder ); queryBuilder.append( ") - " ); queryBuilder.append( "[r" ); queryBuilder.append( ":" ); appendRelationshipType( queryBuilder, associationKeyMetadata ); offset = ownerEntityKeyMetadata.getColumnNames().length; if ( associationKeyMetadata.getRowKeyIndexColumnNames().length > 0 ) { appendProperties( queryBuilder, associationKeyMetadata.getRowKeyIndexColumnNames(), offset ); queryBuilder.append( "] - (t" ); } else { queryBuilder.append( "] - (t" ); appendProperties( queryBuilder, associationKeyMetadata.getAssociatedEntityKeyMetadata().getEntityKeyMetadata().getColumnNames(), offset ); } queryBuilder.append( ")" ); queryBuilder.append( " RETURN r" ); return queryBuilder.toString(); } /* * MATCH (o:ENTITY:table1 {id: {0}}), (t:ENTITY:table2 {id: {1}}) * CREATE UNIQUE (o) -[r:role {props}]-> (t) * RETURN r */ private static String initCreateRelationshipQuery(EntityKeyMetadata ownerEntityKeyMetadata, AssociationKeyMetadata associationKeyMetadata) { EntityKeyMetadata targetEntityKeyMetadata = associationKeyMetadata.getAssociatedEntityKeyMetadata().getEntityKeyMetadata(); int offset = 0; StringBuilder queryBuilder = new StringBuilder( "MATCH " ); appendEntityNode( "n", ownerEntityKeyMetadata, queryBuilder ); queryBuilder.append( ", " ); offset += ownerEntityKeyMetadata.getColumnNames().length; appendEntityNode( "t", targetEntityKeyMetadata, queryBuilder, offset ); queryBuilder.append( " CREATE UNIQUE (n)" ); queryBuilder.append( " -[r" ); queryBuilder.append( ":" ); appendRelationshipType( queryBuilder, associationKeyMetadata ); offset = ownerEntityKeyMetadata.getColumnNames().length; if ( associationKeyMetadata.getRowKeyIndexColumnNames().length > 0 ) { offset += targetEntityKeyMetadata.getColumnNames().length; appendProperties( queryBuilder, associationKeyMetadata.getRowKeyIndexColumnNames(), offset ); } queryBuilder.append( "]-> (t)" ); queryBuilder.append( " RETURN r" ); return queryBuilder.toString(); } /* * Example with association: * * MATCH (n:ENTITY:table {id: {0}}) -[r:role] - () * DELETE r * * Example with an embedded association: * * MATCH (n:ENTITY:table {id: {0}}) -[a:role] - (e:EMBEDDED) * WITH a, e MATCH path=(e) -[*0..]-> (:EMBEDDED) * FOREACH (r IN relationships(path) | DELETE r) * FOREACH (e IN nodes(path) | DELETE e) * DELETE a */ private static String initRemoveAssociationQuery(EntityKeyMetadata ownerEntityKeyMetadata, AssociationKeyMetadata associationKeyMetadata) { StringBuilder queryBuilder = new StringBuilder( "MATCH " ); queryBuilder.append( "(n:" ); queryBuilder.append( ENTITY ); queryBuilder.append( ":" ); appendLabel( ownerEntityKeyMetadata, queryBuilder ); appendProperties( ownerEntityKeyMetadata, queryBuilder ); queryBuilder.append( ") - " ); queryBuilder.append( "[a" ); queryBuilder.append( ":" ); appendRelationshipType( queryBuilder, associationKeyMetadata ); queryBuilder.append( "]" ); if ( associationKeyMetadata.getAssociationKind() == AssociationKind.EMBEDDED_COLLECTION ) { queryBuilder.append( " - (e:" ); queryBuilder.append( EMBEDDED ); queryBuilder.append( ")" ); queryBuilder.append( " WITH a,e" ); queryBuilder.append( " MATCH path=(e) -[*0..]-> (:EMBEDDED) " ); queryBuilder.append( " FOREACH ( r IN relationships(path) | DELETE r )" ); queryBuilder.append( " FOREACH ( e IN nodes(path) | DELETE e )" ); queryBuilder.append( " DELETE a" ); } else { queryBuilder.append( " - () DELETE a" ); } return queryBuilder.toString(); } /* * Example with association: * * MATCH (n:ENTITY:table {id: {0}}) -[r:role] - (e) * DELETE r * * Example with embedded collection: * * MATCH (n:ENTITY:table {id: {0}}) -[r:role] - (e:EMBEDDED) * DELETE r, e * * Example with indexes: * * MATCH (n:ENTITY:table {id: {0}}) -[r:role {index: {1}}] - (e) * DELETE r */ private static String initRemoveAssociationRowQuery(EntityKeyMetadata ownerEntityKeyMetadata, AssociationKeyMetadata associationKeyMetadata) { StringBuilder queryBuilder = new StringBuilder( "MATCH " ); queryBuilder.append( "(n:" ); queryBuilder.append( ENTITY ); queryBuilder.append( ":" ); appendLabel( ownerEntityKeyMetadata, queryBuilder ); appendProperties( ownerEntityKeyMetadata, queryBuilder ); queryBuilder.append( ") - " ); queryBuilder.append( "[r" ); queryBuilder.append( ":" ); appendRelationshipType( queryBuilder, associationKeyMetadata ); int offset = ownerEntityKeyMetadata.getColumnNames().length; boolean hasIndexColumns = associationKeyMetadata.getRowKeyIndexColumnNames().length > 0; if ( hasIndexColumns ) { appendProperties( queryBuilder, associationKeyMetadata.getRowKeyIndexColumnNames(), offset ); } queryBuilder.append( "] - (e" ); if ( associationKeyMetadata.getAssociationKind() == AssociationKind.EMBEDDED_COLLECTION ) { queryBuilder.append( ":" ); queryBuilder.append( EMBEDDED ); } if ( !hasIndexColumns ) { appendProperties( queryBuilder, associationKeyMetadata.getAssociatedEntityKeyMetadata().getEntityKeyMetadata().getColumnNames(), offset ); } queryBuilder.append( ")" ); queryBuilder.append( " DELETE r" ); if ( associationKeyMetadata.getAssociationKind() == AssociationKind.EMBEDDED_COLLECTION ) { queryBuilder.append( ", e" ); } return queryBuilder.toString(); } /** * Removes the relationship(s) representing the given association. If the association refers to an embedded entity * (collection), the referenced entities are removed as well. * * @param executionEngine the {@link GraphDatabaseService} used to run the query * @param associationKey represents the association */ public void removeAssociation(GraphDatabaseService executionEngine, AssociationKey associationKey) { executionEngine.execute( removeAssociationQuery, params( associationKey.getEntityKey().getColumnValues() ) ); } /** * Returns the relationship corresponding to the {@link AssociationKey} and {@link RowKey}. * * @param executionEngine the {@link GraphDatabaseService} used to run the query * @param associationKey represents the association * @param rowKey represents a row in an association * @return the corresponding relationship */ public Relationship findRelationship(GraphDatabaseService executionEngine, AssociationKey associationKey, RowKey rowKey) { Object[] relationshipValues = relationshipValues( associationKey, rowKey ); Object[] queryValues = ArrayHelper.concat( associationKey.getEntityKey().getColumnValues(), relationshipValues ); Result result = executionEngine.execute( findRelationshipQuery, params( queryValues ) ); return singleResult( result ); } protected Object[] relationshipValues(AssociationKey associationKey, RowKey rowKey) { Object[] relationshipValues; if ( associationKey.getMetadata().getRowKeyIndexColumnNames().length > 0 ) { int length = associationKey.getMetadata().getRowKeyIndexColumnNames().length; relationshipValues = new Object[length]; String[] indexColumnNames = associationKey.getMetadata().getRowKeyIndexColumnNames(); for ( int i = 0; i < indexColumnNames.length; i++ ) { for ( int j = 0; j < rowKey.getColumnNames().length; j++ ) { if ( indexColumnNames[i].equals( rowKey.getColumnNames()[j] ) ) { relationshipValues[i] = rowKey.getColumnValues()[j]; } } } return relationshipValues; } else { return getEntityKey( associationKey, rowKey ).getColumnValues(); } } /** * Remove an association row * * @param executionEngine the {@link GraphDatabaseService} used to run the query * @param associationKey represents the association * @param rowKey represents a row in an association */ public void removeAssociationRow(GraphDatabaseService executionEngine, AssociationKey associationKey, RowKey rowKey) { Object[] relationshipValues = relationshipValues( associationKey, rowKey ); Object[] queryValues = ArrayHelper.concat( associationKey.getEntityKey().getColumnValues(), relationshipValues ); executionEngine.execute( removeAssociationRowQuery, params( queryValues ) ); } /** * Returns the entity key on the other side of association row represented by the given row key. * <p> * <b>Note:</b> May only be invoked if the row key actually contains all the columns making up that entity key. * Specifically, it may <b>not</b> be invoked if the association has index columns (maps, ordered collections), as * the entity key columns will not be part of the row key in this case. */ private EntityKey getEntityKey(AssociationKey associationKey, RowKey rowKey) { String[] associationKeyColumns = associationKey.getMetadata().getAssociatedEntityKeyMetadata().getAssociationKeyColumns(); Object[] columnValues = new Object[associationKeyColumns.length]; int i = 0; for ( String associationKeyColumn : associationKeyColumns ) { columnValues[i] = rowKey.getColumnValue( associationKeyColumn ); i++; } EntityKeyMetadata entityKeyMetadata = associationKey.getMetadata().getAssociatedEntityKeyMetadata().getEntityKeyMetadata(); return new EntityKey( entityKeyMetadata, columnValues ); } private static void appendRelationshipType(StringBuilder queryBuilder, AssociationKeyMetadata associationKeyMetadata) { escapeIdentifier( queryBuilder, associationKeyMetadata.getCollectionRole() ); } /** * Give an embedded association, creates all the nodes and relationships required to represent it. It assumes that * the entity node containing the association already exists in the db. * * @param executionEngine the {@link GraphDatabaseService} to run the query * @param associationKey the {@link AssociationKey} identifying the association * @param embeddedKey the {@link EntityKey} identifying the embedded component * @return the created {@link Relationship} that represents the association */ public Relationship createRelationshipForEmbeddedAssociation(GraphDatabaseService executionEngine, AssociationKey associationKey, EntityKey embeddedKey, Object[] relValues) { String query = initCreateEmbeddedAssociationQuery( associationKey, embeddedKey ); Object[] queryValues = createRelationshipForEmbeddedQueryValues( associationKey, embeddedKey, relValues ); return executeQuery( executionEngine, query, queryValues ); } protected String initCreateEmbeddedAssociationQuery(AssociationKey associationKey, EntityKey embeddedKey) { String collectionRole = associationKey.getMetadata().getCollectionRole(); String[] embeddedColumnNames = embeddedKey.getColumnNames(); Object[] embeddedColumnValues = embeddedKey.getColumnValues(); String[] columnNames = associationKey.getEntityKey().getMetadata().getColumnNames(); StringBuilder queryBuilder = new StringBuilder(); queryBuilder.append( matchOwnerEntityNode ); if ( isCollectionOfPrimitives( collectionRole, embeddedColumnNames ) ) { createRelationshipForCollectionOfPrimitivesOrMap( associationKey, collectionRole, columnNames, queryBuilder ); } else { createRelationshipforCollectionOfComponents( associationKey, collectionRole, embeddedColumnNames, embeddedColumnValues, queryBuilder ); } return queryBuilder.toString(); } protected Object[] createRelationshipForEmbeddedQueryValues(AssociationKey associationKey, EntityKey embeddedKey, Object[] relationshipProperties) { Object[] columnValues = associationKey.getEntityKey().getColumnValues(); return ArrayHelper.concat( Arrays.asList( columnValues, relationshipProperties, embeddedKey.getColumnValues() ) ); } private boolean isCollectionOfPrimitives(String collectionRole, String[] embeddedColumnNames) { return embeddedColumnNames.length == 1 && collectionRole.equals( embeddedColumnNames[0] ); } /* * Example 1: * * MATCH (owner:ENTITY:MultiAddressAccount {login: {0}}) * CREATE UNIQUE (owner) -[r:addresses {name: {1}}]-> (target:EMBEDDED:`MultiAddressAccount_addresses` {city: {2}, country: {3}}) * RETURN r * * Example 2: * * MATCH (owner:ENTITY:StoryGame {id: {0}}) - [:goodBranch] -> (e:EMBEDDED) * CREATE (e) -[r:additionalEndings {name: {1}]-> (target:EMBEDDED:`StoryGame_goodBranch.additionalEndings` {score: {2}, text: {3}}) * RETURN r */ private void createRelationshipforCollectionOfComponents(AssociationKey associationKey, String collectionRole, String[] embeddedColumnNames, Object[] embeddedColumnValues, StringBuilder queryBuilder) { int offset = associationKey.getEntityKey().getColumnNames().length; if ( isPartOfEmbedded( collectionRole ) ) { String[] pathToEmbedded = appendEmbeddedNodes( collectionRole, queryBuilder ); queryBuilder.append( " CREATE UNIQUE (e) -[r:" ); appendRelationshipType( queryBuilder, pathToEmbedded[pathToEmbedded.length - 1] ); } else { queryBuilder.append( " CREATE UNIQUE (owner) -[r:" ); appendRelationshipType( queryBuilder, collectionRole ); } appendProperties( queryBuilder, associationKey.getMetadata().getRowKeyIndexColumnNames(), offset ); offset += associationKey.getMetadata().getRowKeyIndexColumnNames().length; queryBuilder.append( "]-> " ); queryBuilder.append( "(target:" ); queryBuilder.append( EMBEDDED ); queryBuilder.append( ":" ); escapeIdentifier( queryBuilder, associationKey.getMetadata().getAssociatedEntityKeyMetadata().getEntityKeyMetadata().getTable() ); int index = 0; int embeddedNumber = 0; EmbeddedNodesTree tree = createEmbeddedTree( collectionRole, embeddedColumnNames, embeddedColumnValues, offset ); // Append primitive properties if ( !tree.getProperties().isEmpty() ) { queryBuilder.append( " {" ); for ( EmbeddedNodeProperty property : tree.getProperties() ) { escapeIdentifier( queryBuilder, property.getColumn() ); queryBuilder.append( ": {" ); queryBuilder.append( property.getParam() ); queryBuilder.append( "}" ); if ( index++ < tree.getProperties().size() - 1 ) { queryBuilder.append( ", " ); } } queryBuilder.append( "}" ); } queryBuilder.append( ")" ); // Append relationships representing embedded properties Map<String, EmbeddedNodesTree> children = tree.getChildren(); boolean first = true; for ( Entry<String, EmbeddedNodesTree> entry : children.entrySet() ) { index = 0; String relationshipType = entry.getKey(); EmbeddedNodesTree child = entry.getValue(); if ( first ) { first = false; } else { queryBuilder.append( ", (target)" ); } queryBuilder.append( " - [:" ); appendRelationshipType( queryBuilder, relationshipType ); queryBuilder.append( "] -> " ); queryBuilder.append( "(a" ); queryBuilder.append( embeddedNumber++ ); queryBuilder.append( ":" ); queryBuilder.append( EMBEDDED ); if ( !child.getProperties().isEmpty() ) { queryBuilder.append( " {" ); for ( EmbeddedNodeProperty property : child.getProperties() ) { escapeIdentifier( queryBuilder, property.getColumn() ); queryBuilder.append( ": {" ); queryBuilder.append( property.getParam() ); queryBuilder.append( "}" ); if ( index++ < child.getProperties().size() - 1 ) { queryBuilder.append( ", " ); } } queryBuilder.append( "}" ); } queryBuilder.append( ")" ); } queryBuilder.append( " RETURN r" ); } /* * Append query part related to the creation of a relationship for a collection of primitive or a Map like in the * following examples: * * 1) @ElementCollection List<String> alternatives * 2) @MapKeyColumn(name = "addressType") Map<String, Address> addresses * * Query example for embedded collection of primitives or Map: * * MATCH (owner:ENTITY:table {id: {0}}) * * MERGE (owner) -[:relType]-> (e0:EMBEDDED) -[:relType2]-> (e:EMBEDDED) * CREATE (e) -[r:relType2]-> (new:EMBEDDED { property: {1}}) * RETURN r * * MATCH (owner:ENTITY:table {id: {0}}) * CREATE (owner) -[r:relType2]-> (target:EMBEDDED { property: {1}}) * RETURN r */ private void createRelationshipForCollectionOfPrimitivesOrMap(AssociationKey associationKey, String collectionRole, String[] columnNames, StringBuilder queryBuilder) { String relationshipType = collectionRole; if ( isPartOfEmbedded( collectionRole ) ) { queryBuilder.append( " MERGE (owner) " ); String[] pathToEmbedded = appendEmbeddedNodes( collectionRole, queryBuilder ); relationshipType = pathToEmbedded[pathToEmbedded.length - 1]; queryBuilder.append( " CREATE (e) -[r:" ); } else { queryBuilder.append( " CREATE (owner) -[r:" ); } escapeIdentifier( queryBuilder, relationshipType ); int offset = ownerEntityKeyMetadata.getColumnNames().length; appendProperties( queryBuilder, associationKey.getMetadata().getRowKeyIndexColumnNames(), offset ); queryBuilder.append( "]-> (new:" ); queryBuilder.append( EMBEDDED ); queryBuilder.append( ":" ); escapeIdentifier( queryBuilder, associationKey.getTable() ); queryBuilder.append( " {" ); // THe name of the property is the same as the relationship type escapeIdentifier( queryBuilder, relationshipType ); queryBuilder.append( ": {" ); offset += associationKey.getMetadata().getRowKeyIndexColumnNames().length; queryBuilder.append( offset ); queryBuilder.append( "}" ); queryBuilder.append( "}" ); queryBuilder.append( ")" ); queryBuilder.append( " RETURN r" ); } private Relationship executeQuery(GraphDatabaseService executionEngine, String query, Object[] queryValues) { Map<String, Object> params = params( queryValues ); Result result = executionEngine.execute( query, params ); return singleResult( result ); } /* * If the association is connected to embedded elements we also need to create the embedded relationships for this * elements. This method will create an tree containing the information about the path to the embedded in a more * managabel way. */ private EmbeddedNodesTree createEmbeddedTree(String collectionRole, String[] embeddedColumnNames, Object[] embeddedColumnValues, int offset) { EmbeddedNodesTree tree = new EmbeddedNodesTree(); for ( int i = 0; i < embeddedColumnNames.length; i++ ) { String embeddedColumnName; if ( embeddedColumnNames[i].startsWith( collectionRole ) ) { embeddedColumnName = embeddedColumnNames[i].substring( collectionRole.length() + 1 ); } else { embeddedColumnName = embeddedColumnNames[i]; } if ( embeddedColumnValues[i] != null ) { if ( embeddedColumnName.contains( "." ) ) { int firstDot = embeddedColumnName.indexOf( "." ); String relationshipType = embeddedColumnName.substring( 0, firstDot ); String currentProperty = embeddedColumnName.substring( firstDot + 1 ); appendSubTree( tree, currentProperty, relationshipType, offset + i ); } else { EmbeddedNodeProperty property = new EmbeddedNodeProperty(); property.setParam( offset + i ); property.setColumn( embeddedColumnName ); tree.addProperty( property ); } } } return tree; } private void appendSubTree(EmbeddedNodesTree tree, String currentProperty, String relationshipType, int index) { EmbeddedNodesTree subTree = tree.getChild( relationshipType ); if ( subTree == null ) { subTree = new EmbeddedNodesTree(); tree.addChild( relationshipType, subTree ); } if ( isPartOfEmbedded( currentProperty ) ) { int firstDot = currentProperty.indexOf( "." ); String relType = currentProperty.substring( 0, firstDot ); String subProperty = currentProperty.substring( firstDot + 1 ); appendSubTree( tree, subProperty, relType, index ); } else { EmbeddedNodeProperty property = new EmbeddedNodeProperty(); property.setColumn( currentProperty ); property.setParam( index ); subTree.addProperty( property ); } } /* * Given an embedded properties path returns the cypher representation that can be appended to a MERGE or CREATE * query. */ private static String[] appendEmbeddedNodes(String path, StringBuilder queryBuilder) { String[] columns = split( path ); for ( int i = 0; i < columns.length - 1; i++ ) { queryBuilder.append( " - [:" ); appendRelationshipType( queryBuilder, columns[i] ); queryBuilder.append( "] ->" ); if ( i < columns.length - 2 ) { queryBuilder.append( " (e" ); queryBuilder.append( i ); queryBuilder.append( ":" ); queryBuilder.append( EMBEDDED ); queryBuilder.append( ") MERGE (e" ); queryBuilder.append( i ); queryBuilder.append( ")" ); } } queryBuilder.append( " (e:" ); queryBuilder.append( EMBEDDED ); queryBuilder.append( ")" ); return columns; } private static class EmbeddedNodesTree { final List<EmbeddedNodeProperty> properties = new ArrayList<EmbeddedNodeProperty>(); final Map<String, EmbeddedNodesTree> children = new HashMap<String, EmbeddedNodesTree>(); public void addProperty(EmbeddedNodeProperty property) { properties.add( property ); } public List<EmbeddedNodeProperty> getProperties() { return properties; } public void addChild(String relationshipType, EmbeddedNodesTree subTree) { children.put( relationshipType, subTree ); } public EmbeddedNodesTree getChild(String relationshipType) { return children.get( relationshipType ); } public Map<String, EmbeddedNodesTree> getChildren() { return children; } @Override public String toString() { StringBuilder builder = new StringBuilder(); builder.append( "EmbeddedTree [properties=" ); builder.append( properties ); builder.append( ", children=" ); builder.append( children ); builder.append( "]" ); return builder.toString(); } } private class EmbeddedNodeProperty { private String column; private int param; public String getColumn() { return column; } public void setColumn(String column) { this.column = column; } public int getParam() { return param; } public void setParam(int param) { this.param = param; } @Override public String toString() { StringBuilder builder = new StringBuilder(); builder.append( "[" ); builder.append( column ); builder.append( ", " ); builder.append( param ); builder.append( "]" ); return builder.toString(); } } }