package org.neo4j.kernel; import java.lang.reflect.Array; import java.util.HashMap; import java.util.Iterator; import java.util.Map; import org.neo4j.graphdb.Direction; import org.neo4j.graphdb.Expander; import org.neo4j.graphdb.Node; import org.neo4j.graphdb.NotFoundException; import org.neo4j.graphdb.Relationship; import org.neo4j.graphdb.RelationshipExpander; import org.neo4j.graphdb.RelationshipType; import org.neo4j.helpers.Pair; import org.neo4j.helpers.Predicate; import org.neo4j.helpers.collection.FilteringIterator; import org.neo4j.helpers.collection.IteratorWrapper; public abstract class StandardExpander implements Expander { private StandardExpander() { } static abstract class StandardExpansion<T> implements Expansion<T> { final StandardExpander expander; final Node start; StandardExpansion( StandardExpander expander, Node start ) { this.expander = expander; this.start = start; } String stringRepresentation( String nodesORrelationships ) { return "Expansion[" + start + ".expand( " + expander + " )." + nodesORrelationships + "()]"; } abstract StandardExpansion<T> createNew( @SuppressWarnings( "hiding" ) StandardExpander expander ); public StandardExpansion<T> including( RelationshipType type ) { return createNew( expander.add( type ) ); } public StandardExpansion<T> including( RelationshipType type, Direction direction ) { return createNew( expander.add( type, direction ) ); } public StandardExpansion<T> excluding( RelationshipType type ) { return createNew( expander.remove( type ) ); } public StandardExpander expander() { return expander; } public StandardExpansion<T> filterNodes( Predicate<? super Node> filter ) { return createNew( expander.addNodeFilter( filter ) ); } public StandardExpansion<T> filterRelationships( Predicate<? super Relationship> filter ) { return createNew( expander.addRelationsipFilter( filter ) ); } public T getSingle() { final Iterator<T> expanded = iterator(); if ( expanded.hasNext() ) { final T result = expanded.next(); if ( expanded.hasNext() ) { throw new NotFoundException( "More than one relationship found for " + this ); } return result; } return null; } public boolean isEmpty() { return !expander.doExpand( start ).hasNext(); } public StandardExpansion<Node> nodes() { return new NodeExpansion( expander, start ); } public StandardExpansion<Relationship> relationships() { return new RelationsipExpansion( expander, start ); } public StandardExpansion<Pair<Relationship, Node>> pairs() { return new PairExpansion( expander, start ); } } private static final class RelationsipExpansion extends StandardExpansion<Relationship> { RelationsipExpansion( StandardExpander expander, Node start ) { super( expander, start ); } @Override public String toString() { return stringRepresentation( "relationships" ); } @Override StandardExpansion<Relationship> createNew( @SuppressWarnings( "hiding" ) StandardExpander expander ) { return new RelationsipExpansion( expander, start ); } @Override public StandardExpansion<Relationship> relationships() { return this; } public Iterator<Relationship> iterator() { return expander.doExpand( start ); } } private static final class NodeExpansion extends StandardExpansion<Node> { NodeExpansion( StandardExpander expander, Node start ) { super( expander, start ); } @Override public String toString() { return stringRepresentation( "nodes" ); } @Override StandardExpansion<Node> createNew( @SuppressWarnings( "hiding" ) StandardExpander expander ) { return new NodeExpansion( expander, start ); } @Override public StandardExpansion<Node> nodes() { return this; } public Iterator<Node> iterator() { return new IteratorWrapper<Node, Relationship>( expander.doExpand( start ) ) { @Override protected Node underlyingObjectToObject( Relationship rel ) { return rel.getOtherNode( start ); } }; } } private static final class PairExpansion extends StandardExpansion<Pair<Relationship, Node>> { PairExpansion( StandardExpander expander, Node start ) { super( expander, start ); } @Override public String toString() { return stringRepresentation( "pairs" ); } @Override StandardExpansion<Pair<Relationship, Node>> createNew( @SuppressWarnings( "hiding" ) StandardExpander expander ) { return new PairExpansion( expander, start ); } @Override public StandardExpansion<Pair<Relationship, Node>> pairs() { return this; } public Iterator<Pair<Relationship, Node>> iterator() { return new IteratorWrapper<Pair<Relationship, Node>, Relationship>( expander.doExpand( start ) ) { @Override protected Pair<Relationship, Node> underlyingObjectToObject( Relationship rel ) { return new Pair<Relationship, Node>( rel, rel.getOtherNode( start ) ); } }; } } private static class AllExpander extends StandardExpander { private final Direction direction; AllExpander( Direction direction ) { this.direction = direction; } @Override void buildString( StringBuilder result ) { if ( direction != Direction.BOTH ) { result.append( direction ); result.append( ":" ); } result.append( "*" ); } @Override Iterator<Relationship> doExpand( Node start ) { if ( direction == Direction.BOTH ) { return start.getRelationships().iterator(); } else { return start.getRelationships( direction ).iterator(); } } @Override public StandardExpander add( RelationshipType type, Direction dir ) { return this; } @Override public StandardExpander remove( RelationshipType type ) { Map<String, Exclusion> exclude = new HashMap<String, Exclusion>(); exclude.put( type.name(), Exclusion.ALL ); return new ExcludingExpander( Exclusion.include( direction ), exclude ); } @Override public StandardExpander reversed() { return this; } } private enum Exclusion { ALL( null, "!" ) { @Override public boolean accept( Node start, Relationship rel ) { return false; } }, INCOMING( Direction.OUTGOING ) { @Override Exclusion reversed() { return OUTGOING; } }, OUTGOING( Direction.INCOMING ) { @Override Exclusion reversed() { return INCOMING; } }, NONE( Direction.BOTH, "" ) { @Override boolean includes( Direction direction ) { return true; } }; private final String string; private final Direction direction; private Exclusion( Direction direction, String string ) { this.direction = direction; this.string = string; } private Exclusion( Direction direction ) { this.direction = direction; this.string = "!" + name() + ":"; } @Override public final String toString() { return string; } boolean accept( Node start, Relationship rel ) { return matchDirection( direction, start, rel ); } Exclusion reversed() { return this; } boolean includes( Direction dir ) { return this.direction == dir; } static Exclusion include( Direction direction ) { switch ( direction ) { case INCOMING: return OUTGOING; case OUTGOING: return INCOMING; default: return NONE; } } } private static final class ExcludingExpander extends StandardExpander { private final Exclusion defaultExclusion; private final Map<String, Exclusion> exclusion; ExcludingExpander( Exclusion defaultExclusion, Map<String, Exclusion> exclusion ) { this.defaultExclusion = defaultExclusion; this.exclusion = exclusion; } @Override void buildString( StringBuilder result ) { // FIXME: not really correct result.append( defaultExclusion ); result.append( "*" ); for ( Map.Entry<String, Exclusion> entry : exclusion.entrySet() ) { result.append( "," ); result.append( entry.getValue() ); result.append( entry.getKey() ); } } @Override Iterator<Relationship> doExpand( final Node start ) { return new FilteringIterator<Relationship>( start.getRelationships().iterator(), new Predicate<Relationship>() { public boolean accept( Relationship rel ) { Exclusion exclude = exclusion.get( rel.getType().name() ); exclude = ( exclude == null ) ? defaultExclusion : exclude; return exclude.accept( start, rel ); } } ); } @Override public StandardExpander add( RelationshipType type, Direction direction ) { Exclusion excluded = exclusion.get( type.name() ); final Map<String, Exclusion> newExclusion; if ( ( ( excluded == null ) ? defaultExclusion : excluded ).includes( direction ) ) { return this; } else { excluded = Exclusion.include( direction ); if ( excluded == defaultExclusion ) { if ( exclusion.size() == 1 ) { return new AllExpander( defaultExclusion.direction ); } else { newExclusion = new HashMap<String, Exclusion>( exclusion ); newExclusion.remove( type.name() ); } } else { newExclusion = new HashMap<String, Exclusion>( exclusion ); newExclusion.put( type.name(), excluded ); } } return new ExcludingExpander( defaultExclusion, newExclusion ); } @Override public StandardExpander remove( RelationshipType type ) { Exclusion excluded = exclusion.get( type.name() ); if ( excluded == Exclusion.ALL ) { return this; } Map<String, Exclusion> newExclusion = new HashMap<String, Exclusion>( exclusion ); newExclusion.put( type.name(), Exclusion.ALL ); return new ExcludingExpander( defaultExclusion, newExclusion ); } @Override public StandardExpander reversed() { Map<String, Exclusion> newExclusion = new HashMap<String, Exclusion>(); for ( Map.Entry<String, Exclusion> entry : exclusion.entrySet() ) { newExclusion.put( entry.getKey(), entry.getValue().reversed() ); } return new ExcludingExpander( defaultExclusion.reversed(), newExclusion ); } } public static final StandardExpander DEFAULT = new AllExpander( Direction.BOTH ) { @Override public StandardExpander add( RelationshipType type, Direction direction ) { return create( type, direction ); } }; static class RegularExpander extends StandardExpander { final RelationshipType[] types; final Map<String, Direction> directions; RegularExpander( RelationshipType[] types, Map<String, Direction> dirs ) { this.types = types; this.directions = dirs; } @Override void buildString( StringBuilder result ) { String sep = ""; for ( RelationshipType type : types ) { result.append( sep ); sep = ","; Direction dir = directions.get( type.name() ); if ( dir != null ) { result.append( dir.name() ); result.append( ":" ); } result.append( type.name() ); } } @Override Iterator<Relationship> doExpand( final Node start ) { Iterable<Relationship> relationships = start.getRelationships( types ); if ( directions.isEmpty() ) { return relationships.iterator(); } else { return new FilteringIterator<Relationship>( relationships.iterator(), new Predicate<Relationship>() { public boolean accept( Relationship rel ) { Direction dir = directions.get( rel.getType().name() ); return matchDirection( dir == null ? Direction.BOTH : dir, start, rel ); } } ); } } @Override public StandardExpander add( RelationshipType type, Direction direction ) { Map<String, Direction> newDirections = directions; if ( direction != Direction.BOTH ) { newDirections = new HashMap<String, Direction>( directions ); newDirections.put( type.name(), direction ); } return createNew( append( types, type ), newDirections ); } RegularExpander createNew( RelationshipType[] newTypes, Map<String, Direction> newDirections ) { return new RegularExpander( newTypes, newDirections ); } @Override public StandardExpander remove( RelationshipType type ) { for ( int i = 0; i < types.length; i++ ) { if ( type.name().equals( types[i].name() ) ) { Map<String, Direction> newDirections = directions; if ( directions.containsKey( type.name() ) ) { newDirections = new HashMap<String, Direction>( directions ); } RelationshipType[] newTypes = new RelationshipType[types.length - 1]; System.arraycopy( types, 0, newTypes, 0, i ); System.arraycopy( types, i + 1, newTypes, i, types.length - i - 1 ); return createNew( types, newDirections ); } } return this; } @Override public StandardExpander reversed() { if ( directions.isEmpty() ) { return this; } else { Map<String, Direction> newDirections = new HashMap<String, Direction>(); for ( Map.Entry<String, Direction> entry : directions.entrySet() ) { newDirections.put( entry.getKey(), entry.getValue().reverse() ); } return createNew( types, newDirections ); } } } private static final class FilteringExpander extends StandardExpander { private final StandardExpander expander; private final Filter[] filters; FilteringExpander( StandardExpander expander, Filter... filters ) { this.expander = expander; this.filters = filters; } @Override void buildString( StringBuilder result ) { expander.buildString( result ); result.append( "; filter:" ); for ( Filter filter : filters ) { result.append( " " ); result.append( filter ); } } @Override Iterator<Relationship> doExpand( final Node start ) { return new FilteringIterator<Relationship>( expander.doExpand( start ), new Predicate<Relationship>() { public boolean accept( Relationship item ) { for ( Filter filter : filters ) { if ( filter.exclude( start, item ) ) return false; } return true; } } ); } @Override public StandardExpander addNodeFilter( Predicate<? super Node> filter ) { return new FilteringExpander( expander, append( filters, new NodeFilter( filter ) ) ); } @Override public StandardExpander addRelationsipFilter( Predicate<? super Relationship> filter ) { return new FilteringExpander( expander, append( filters, new RelationshipFilter( filter ) ) ); } @Override public StandardExpander add( RelationshipType type, Direction direction ) { return new FilteringExpander( expander.add( type, direction ), filters ); } @Override public StandardExpander remove( RelationshipType type ) { return new FilteringExpander( expander.remove( type ), filters ); } @Override public StandardExpander reversed() { return new FilteringExpander( expander.reversed(), filters ); } } private static final class TypeLimitingExpander extends StandardExpander { private final StandardExpander expander; private final Map<String, Direction> exclusion; TypeLimitingExpander( StandardExpander expander, Map<String, Direction> exclusion ) { this.expander = expander; this.exclusion = exclusion; } @Override void buildString( StringBuilder result ) { result.append( "*" ); for ( Map.Entry<String, Direction> entry : exclusion.entrySet() ) { result.append( ",!" ); if ( entry.getValue() != Direction.BOTH ) { result.append( entry.getValue().name() ); result.append( ":" ); } result.append( entry.getKey() ); } } @Override public StandardExpander add( RelationshipType type, Direction direction ) { Direction excluded = exclusion.get( type.name() ); final Map<String, Direction> newExclusion; if ( excluded == null ) { return this; } else if ( excluded == direction || direction == Direction.BOTH ) { if ( exclusion.size() == 1 ) { return expander; } else { newExclusion = new HashMap<String, Direction>( exclusion ); newExclusion.remove( type.name() ); } } else { newExclusion = new HashMap<String, Direction>( exclusion ); newExclusion.put( type.name(), direction.reverse() ); } return new TypeLimitingExpander( expander, newExclusion ); } @Override Iterator<Relationship> doExpand( final Node start ) { return new FilteringIterator<Relationship>( start.getRelationships().iterator(), new Predicate<Relationship>() { public boolean accept( Relationship item ) { Direction dir = exclusion.get( item.getType().name() ); return !matchDirection( dir, start, item ); } } ); } @Override public StandardExpander remove( RelationshipType type ) { Direction excluded = exclusion.get( type.name() ); if ( excluded == Direction.BOTH ) { return this; } Map<String, Direction> newExclusion = new HashMap<String, Direction>( exclusion ); newExclusion.put( type.name(), Direction.BOTH ); return new TypeLimitingExpander( expander, newExclusion ); } @Override public StandardExpander reversed() { Map<String, Direction> newExclusion = new HashMap<String, Direction>(); for ( Map.Entry<String, Direction> entry : exclusion.entrySet() ) { newExclusion.put( entry.getKey(), entry.getValue().reverse() ); } return new TypeLimitingExpander( expander, newExclusion ); } } private static final class WrappingExpander extends StandardExpander { private static final String IMMUTABLE = "Immutable Expander "; private final RelationshipExpander expander; WrappingExpander( RelationshipExpander expander ) { this.expander = expander; } @Override void buildString( StringBuilder result ) { result.append( expander ); } @Override Iterator<Relationship> doExpand( Node start ) { return expander.expand( start ).iterator(); } @Override public StandardExpander add( RelationshipType type, Direction direction ) { throw new UnsupportedOperationException( IMMUTABLE + expander ); } @Override public StandardExpander remove( RelationshipType type ) { throw new UnsupportedOperationException( IMMUTABLE + expander ); } @Override public StandardExpander reversed() { throw new UnsupportedOperationException( IMMUTABLE + expander ); } } private static abstract class Filter { abstract boolean exclude( Node start, Relationship item ); } private static final class NodeFilter extends Filter { private final Predicate<? super Node> predicate; NodeFilter( Predicate<? super Node> predicate ) { this.predicate = predicate; } @Override public String toString() { return predicate.toString(); } @Override boolean exclude( Node start, Relationship item ) { return !predicate.accept( item.getOtherNode( start ) ); } } private static final class RelationshipFilter extends Filter { private final Predicate<? super Relationship> predicate; RelationshipFilter( Predicate<? super Relationship> predicate ) { this.predicate = predicate; } @Override public String toString() { return predicate.toString(); } @Override boolean exclude( Node start, Relationship item ) { return !predicate.accept( item ); } } public final Expansion<Relationship> expand( Node start ) { return new RelationsipExpansion( this, start ); } static <T> T[] append( T[] array, T item ) { @SuppressWarnings( "unchecked" ) T[] result = (T[]) Array.newInstance( array.getClass().getComponentType(), array.length + 1 ); System.arraycopy( array, 0, result, 0, array.length ); result[array.length] = item; return result; } private static <T> T[] extract( Class<T[]> type, T obj1, T obj2, Object[] more, boolean odd ) { if ( more.length % 2 != 0 ) { throw new IllegalArgumentException(); } Object[] target = (Object[]) Array.newInstance( type.getComponentType(), ( more.length / 2 ) + 2 ); try { target[0] = obj1; target[1] = obj2; for ( int i = 2; i < target.length; i++ ) { target[i] = more[( i - 2 ) * 2 + ( odd ? 1 : 0 )]; } } catch ( ArrayStoreException cast ) { throw new IllegalArgumentException( cast ); } return type.cast( target ); } static boolean matchDirection( Direction dir, Node start, Relationship rel ) { switch ( dir ) { case INCOMING: return rel.getEndNode().equals( start ); case OUTGOING: return rel.getStartNode().equals( start ); } return true; } abstract Iterator<Relationship> doExpand( Node start ); @Override public final String toString() { StringBuilder result = new StringBuilder( "Expander[" ); buildString( result ); result.append( "]" ); return result.toString(); } abstract void buildString( StringBuilder result ); public final StandardExpander add( RelationshipType type ) { return add( type, Direction.BOTH ); } public abstract StandardExpander add( RelationshipType type, Direction direction ); public abstract StandardExpander remove( RelationshipType type ); public abstract StandardExpander reversed(); public StandardExpander addNodeFilter( Predicate<? super Node> filter ) { return new FilteringExpander( this, new NodeFilter( filter ) ); } public StandardExpander addRelationsipFilter( Predicate<? super Relationship> filter ) { return new FilteringExpander( this, new RelationshipFilter( filter ) ); } static StandardExpander wrap( RelationshipExpander expander ) { return new WrappingExpander( expander ); } static Expander create( Direction direction ) { return new AllExpander( direction ); } static StandardExpander create( RelationshipType type, Direction dir ) { final Map<String, Direction> dirs = new HashMap<String, Direction>(); if ( dir != Direction.BOTH ) { dirs.put( type.name(), dir ); } return new RegularExpander( new RelationshipType[] { type }, dirs ); } static StandardExpander create( RelationshipType type1, Direction dir1, RelationshipType type2, Direction dir2 ) { final Map<String, Direction> dirs = new HashMap<String, Direction>(); if ( dir1 != Direction.BOTH ) { dirs.put( type1.name(), dir1 ); } if ( dir2 != Direction.BOTH ) { dirs.put( type2.name(), dir2 ); } return new RegularExpander( new RelationshipType[] { type1, type2 }, dirs ); } static StandardExpander create( RelationshipType type1, Direction dir1, RelationshipType type2, Direction dir2, Object... more ) { RelationshipType[] types = extract( RelationshipType[].class, type1, type2, more, false ); Direction[] directions = extract( Direction[].class, dir1, dir2, more, true ); final Map<String, Direction> dirs = new HashMap<String, Direction>(); for ( int i = 0; i < directions.length; i++ ) { if ( directions[i] != Direction.BOTH ) { dirs.put( types[i].name(), directions[i] ); } } return new RegularExpander( types, dirs ); } }