/** * Copyright (c) 2002-2010 "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 Affero 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 Affero General Public License for more details. * * You should have received a copy of the GNU Affero General Public License * along with this program. If not, see <http://www.gnu.org/licenses/>. */ package org.neo4j.util; import java.lang.reflect.Array; import java.util.ArrayList; import java.util.Arrays; import java.util.Collection; import java.util.List; import java.util.TreeSet; import javax.transaction.TransactionManager; import org.neo4j.graphdb.Direction; import org.neo4j.graphdb.GraphDatabaseService; import org.neo4j.graphdb.Node; import org.neo4j.graphdb.PropertyContainer; import org.neo4j.graphdb.Relationship; import org.neo4j.graphdb.RelationshipExpander; import org.neo4j.graphdb.RelationshipType; import org.neo4j.kernel.EmbeddedGraphDatabase; import org.neo4j.kernel.impl.transaction.LockManager; /** * Contains some convenience methods, for instance set/get/remove one property * wrapped in a transaction. * * Some event helper methods. * * Reference nodes helper methods. * * EventManager register/unregister methods are a pain in the ass, one throws * two exceptions and the other throws one. Added some helper methods here. * * @author mathew * */ public class GraphDatabaseUtil { private GraphDatabaseService graphDb; /** * @param graphDb the {@link GraphDatabaseService} to use in methods * which needs it. */ public GraphDatabaseUtil( GraphDatabaseService graphDb ) { this.graphDb = graphDb; } /** * @return the {@link GraphDatabaseService} from the constructor. */ public GraphDatabaseService graphDb() { return this.graphDb; } public static List<Object> getPropertyValues( PropertyContainer container, String key ) { Object value = container.getProperty( key, null ); return value == null ? new ArrayList<Object>() : propertyValueAsList( value ); } public static boolean addValueToArray( PropertyContainer container, String key, Object value ) { Collection<Object> values = getPropertyValues( container, key ); boolean changed = values.contains( value ) ? false : values.add( value ); if ( changed ) { container.setProperty( key, asPropertyValue( values ) ); } return changed; } public static boolean removeValueFromArray( PropertyContainer container, String key, Object value ) { Collection<Object> values = getPropertyValues( container, key ); boolean changed = values.remove( value ); if ( changed ) { if ( values.isEmpty() ) { container.removeProperty( key ); } else { container.setProperty( key, asPropertyValue( values ) ); } } return changed; } public static Node getSingleOtherNode( Node node, RelationshipType type, Direction direction ) { Relationship rel = node.getSingleRelationship( type, direction ); return rel == null ? null : rel.getOtherNode( node ); } public static Node getOrCreateSingleOtherNode( Node fromNode, RelationshipType type, Direction direction ) { Node otherNode = null; Relationship singleRelationship = fromNode.getSingleRelationship( type, direction ); if ( singleRelationship != null ) { otherNode = singleRelationship.getOtherNode( fromNode ); } else { otherNode = fromNode.getGraphDatabase().createNode(); fromNode.createRelationshipTo( otherNode, type ); } return otherNode; } /** * @see #getOrCreateSubReferenceNode(RelationshipType, Direction) . * @param type the relationship type. * @return the sub-reference node for {@code type}. */ public Node getOrCreateSubReferenceNode( RelationshipType type ) { return getOrCreateSubReferenceNode( type, Direction.OUTGOING ); } /** * Tries to get a sub reference node with relationship type * <code>type</code>. If it doesn't exist, it is created. There can be * only one of any given type. * * [NodeSpaceReferenceNode] -- type --> [SubReferenceNode] * * @param type the relationship type. * @param direction the direction of the relationship. * @return the sub-reference node. */ public Node getOrCreateSubReferenceNode( RelationshipType type, Direction direction ) { return getOrCreateSingleOtherNode( graphDb().getReferenceNode(), type, direction ); } /** * Returns the sub-reference node for a relationship type as a collection. * @param <T> the instance class for objects in the result collection. * @param type the relationship type for the sub-reference node. * @param clazz the instance class for objects in the result collection. * @return the sub-reference node for a relationship type as a collection. */ public <T extends NodeWrapper> Collection<T> getSubReferenceNodeCollection( RelationshipType type, Class<T> clazz ) { return new NodeWrapperRelationshipSet<T>( getOrCreateSubReferenceNode( type ), type, clazz ); } public LockManager getLockManager() { return getLockManager( graphDb ); } public static LockManager getLockManager( GraphDatabaseService graphDb ) { return ( ( EmbeddedGraphDatabase ) graphDb ).getConfig().getLockManager(); } public TransactionManager getTransactionManager() { return ( ( EmbeddedGraphDatabase ) graphDb() ).getConfig().getTxModule().getTxManager(); } public static Object[] propertyValueAsArray( Object propertyValue ) { if ( propertyValue.getClass().isArray() ) { int length = Array.getLength( propertyValue ); Object[] result = new Object[ length ]; for ( int i = 0; i < length; i++ ) { result[ i ] = Array.get( propertyValue, i ); } return result; } else { return new Object[] { propertyValue }; } } public static List<Object> propertyValueAsList( Object propertyValue ) { return new ArrayList<Object>( Arrays.asList( propertyValueAsArray( propertyValue ) ) ); } public static Object asPropertyValue( Collection<?> values ) { if ( values.isEmpty() ) { return null; } if ( values.size() == 1 ) { return values.iterator().next(); } Object array = Array.newInstance( values.iterator().next().getClass(), values.size() ); int index = 0; for ( Object value : values ) { Array.set( array, index++, value ); } return array; } public static Integer incrementAndGetCounter( Node node, String propertyKey ) { acquireWriteLock( node ); int value = ( Integer ) node.getProperty( propertyKey, 0 ); value++; node.setProperty( propertyKey, value ); return value; } public static Integer decrementAndGetCounter( Node node, String propertyKey, int notLowerThan ) { int value = ( Integer ) node.getProperty( propertyKey, 0 ); value--; value = value < notLowerThan ? notLowerThan : value; node.setProperty( propertyKey, value ); return value; } public static String sumNodeContents( Node node ) { StringBuilder result = new StringBuilder( "== Summary of " + node + " ==" ); appendRelationships( result, node, node.getRelationships( Direction.OUTGOING ) ); appendRelationships( result, node, node.getRelationships( Direction.INCOMING ) ); appendProperties( result, node ); return result.toString(); } public static String sumRelationshipContents( Relationship relationship ) { StringBuilder result = new StringBuilder( "== Summary of " + relationship + " ==" ); appendProperties( result, relationship ); return result.toString(); } private static void appendRelationships( StringBuilder result, Node fromNode, Iterable<Relationship> relationships ) { for ( Relationship rel : relationships ) { if ( rel.getStartNode().equals( fromNode ) ) { result.append( fromNode + " --<" + rel.getType().name() + ">--> " + rel.getEndNode() ); } else { result.append( fromNode + " <--<" + rel.getType().name() + ">-- " + rel.getStartNode() ); } result.append( "\n" ); } } private static void appendProperties( StringBuilder result, PropertyContainer entity ) { for ( String key : sort( entity.getPropertyKeys() ) ) { for ( Object value : propertyValueAsArray( entity.getProperty( key ) ) ) { result.append( "*" + key + "=[" + value + "]" ); result.append( "\n" ); } } } private static String[] sort( Iterable<String> keys ) { TreeSet<String> set = new TreeSet<String>(); for ( String key : keys ) { set.add( key ); } return set.toArray( new String[ set.size() ] ); } /** * Looks to see if there exists a relationship between two given nodes. * It starts iterating over relationships from the node which you think * has the least amount of relationships of the two nodes. If it has * iterated over a specified amount of relationships without finding * a match it spawns a new thread which will iterate over relationships * from the other node in the reverse direction. The loop will exit * when/if any thread finds a match or when there are no more relationships * left to iterate over. * * @param nodeYouThinkHasLeastRelationships the node of the two which you * think/know has the least amount of relationship (that will be * iterated over). * @param secondNode the node of the two you suspect has more relationships * than the first node. * @param expander the {@link RelationshipExpander} to use to expand the * relationships to iterate over. * @return the first relationship, if any, with the given criterias (type, * direction, filter) between the two nodes. */ public static Relationship getExistingRelationshipBetween( Node nodeYouThinkHasLeastRelationships, Node secondNode, RelationshipExpander expander ) { return getExistingRelationshipBetween( nodeYouThinkHasLeastRelationships, secondNode, expander, null, 50 ); } /** * TODO Use RelationshipExpander later on * * Looks to see if there exists a relationship between two given nodes. * It starts iterating over relationships from the node which you think * has the least amount of relationships of the two nodes. If it has * iterated over a specified amount of relationships without finding * a match it spawns a new thread which will iterate over relationships * from the other node in the reverse direction. The loop will exit * when/if any thread finds a match or when there are no more relationships * left to iterate over. * * @param nodeYouThinkHasLeastRelationships the node of the two which you * think/know has the least amount of relationship (that will be * iterated over). * @param secondNode the node of the two you suspect has more relationships * than the first node. * @param expander the {@link RelationshipExpander} to use to expand the * relationships to iterate over. * @param filterOrNull a filter for which relationships to take into * consideration. Is allowed to be {@code null}. * @param spawnThreadThreshold the threshold for when to spawn the other * thread which iterates from the second node. * @return the first relationship, if any, with the given criterias (type, * direction, filter) between the two nodes. */ public static Relationship getExistingRelationshipBetween( Node nodeYouThinkHasLeastRelationships, Node secondNode, RelationshipExpander expander, ObjectFilter<Relationship> filterOrNull, int spawnThreadThreshold ) { NodeFinder finderFromTheOtherNode = null; try { int counter = 0; for ( Relationship rel : expander.expand( nodeYouThinkHasLeastRelationships ) ) { if ( finderFromTheOtherNode != null && finderFromTheOtherNode.foundRelationship != null ) { return finderFromTheOtherNode.foundRelationship; } counter++; if ( filterOrNull != null && !filterOrNull.pass( rel ) ) { continue; } Node otherNode = rel.getOtherNode( nodeYouThinkHasLeastRelationships ); if ( otherNode.equals( secondNode ) ) { return rel; } if ( counter == spawnThreadThreshold ) { finderFromTheOtherNode = new NodeFinder( nodeYouThinkHasLeastRelationships, expander.reversed(), secondNode, filterOrNull ); finderFromTheOtherNode.start(); } } } finally { if ( finderFromTheOtherNode != null ) { // To make it exit finderFromTheOtherNode.found = true; } } return null; } private static class NodeFinder extends Thread { private final Node nodeWithLeastRels; private final Iterable<Relationship> relationships; private final Node secondNode; private final ObjectFilter<Relationship> filterOrNull; private volatile boolean found; private volatile Relationship foundRelationship = null; NodeFinder( Node nodeWithLeastRels, RelationshipExpander expander, Node secondNode, ObjectFilter<Relationship> filterOrNull ) { this.nodeWithLeastRels = nodeWithLeastRels; this.relationships = expander.expand( secondNode ); this.secondNode = secondNode; this.filterOrNull = filterOrNull; } @Override public void run() { for ( Relationship rel : relationships ) { // So that found can be set from outside to make it exit // the loop if ( found ) { break; } if ( filterOrNull != null && !filterOrNull.pass( rel ) ) { continue; } Node otherNode = rel.getOtherNode( secondNode ); if ( otherNode.equals( nodeWithLeastRels ) ) { foundRelationship = rel; break; } } } } public static void acquireWriteLock( PropertyContainer entity ) { // TODO At the moment this is the best way of doing it, if you don't want to use // the LockManager (and release the lock yourself) entity.removeProperty( "___dummy_property_for_locking___" ); } }