package org.neo4j.util.matching;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Iterator;
import java.util.List;
import java.util.NoSuchElementException;
import java.util.Set;
import java.util.Stack;
import org.neo4j.api.core.Node;
import org.neo4j.api.core.Relationship;
public class PatternMatcher
{
private static PatternMatcher matcher = new PatternMatcher();
private PatternMatcher()
{
}
public static PatternMatcher getMatcher()
{
return matcher;
}
public Iterable<PatternMatch> match( PatternNode start,
Node startNode )
{
return new PatternFinder( start, startNode );
}
public Iterable<PatternMatch> match( PatternNode start,
Node startNode, Collection<PatternNode> optional )
{
if ( optional == null || optional.size() < 1 )
{
return new PatternFinder( start, startNode );
}
else
{
return new PatternFinder( start, startNode, false, optional );
}
}
public Iterable<PatternMatch> match( PatternNode start,
Node startNode, PatternNode... optional )
{
return match( start, startNode, Arrays.asList( optional ) );
}
private static class OptionalPatternFinder
{
private List<PatternFinder> optionalFinders;
private List<PatternMatch> currentMatches;
private Collection<PatternNode> optionalNodes;
private PatternMatch baseMatch;
private int position = -1;
OptionalPatternFinder( PatternMatch baseMatch,
Collection<PatternNode> optionalNodes )
{
this.baseMatch = baseMatch;
this.optionalNodes = optionalNodes;
initialize();
}
private boolean first = true;
PatternMatch findNextOptionalPatterns()
{
if ( position < 0 )
{
return null;
}
if ( first && anyMatchFound() )
{
first = false;
return PatternMatch.merge( currentMatches );
}
boolean found = false;
for ( ; position >= 0; position-- )
{
if ( optionalFinders.get( position ).hasNext() )
{
currentMatches.set(
position, optionalFinders.get( position ).next() );
if ( position < currentMatches.size() - 1 )
{
position++;
reset( position );
}
found = true;
break;
}
}
if ( !found )
{
return null;
}
return PatternMatch.merge( currentMatches );
}
boolean anyMatchFound()
{
return !currentMatches.isEmpty();
}
private void initialize()
{
optionalFinders = new ArrayList<PatternFinder>();
currentMatches = new ArrayList<PatternMatch>();
for ( PatternNode node : optionalNodes )
{
PatternFinder finder = new PatternFinder(
node, this.getNodeFor( node ), true );
if ( finder.hasNext() )
{
optionalFinders.add( finder );
currentMatches.add( finder.next() );
position++;
}
}
}
private Node getNodeFor( PatternNode node )
{
for ( PatternElement element : baseMatch.getElements() )
{
if ( node.getLabel().equals(
element.getPatternNode().getLabel() ) )
{
return element.getNode();
}
}
throw new RuntimeException(
"Optional graph isn't connected to the main graph." );
}
private void reset( int fromIndex )
{
for ( int i = fromIndex; i < optionalFinders.size(); i++ )
{
PatternFinder finder = optionalFinders.get( i );
PatternFinder newFinder = new PatternFinder(
finder.getStartPatternNode(), finder.getStartNode(),
true );
optionalFinders.set( i, newFinder );
// Only patterns with matches were added in the first place,
// so newFinder must have at least one match.
currentMatches.set( i, newFinder.next() );
position = i;
}
}
}
private static class PatternFinder implements Iterable<PatternMatch>,
Iterator<PatternMatch>
{
private Set<Relationship> visitedRels =
new HashSet<Relationship>();
private PatternPosition currentPosition;
private OptionalPatternFinder optionalFinder;
private PatternNode startPatternNode;
private Node startNode;
private Collection<PatternNode> optionalNodes;
private boolean optional;
PatternFinder( PatternNode start, Node startNode )
{
this( start, startNode, false );
}
PatternFinder( PatternNode start, Node startNode, boolean optional )
{
this.startPatternNode = start;
this.startNode = startNode;
currentPosition =
new PatternPosition( startNode, start, optional );
this.optional = optional;
}
PatternFinder( PatternNode start, Node startNode, boolean optional,
PatternNode... optionalNodes )
{
this( start, startNode, optional, Arrays.asList( optionalNodes ) );
}
PatternFinder( PatternNode start, Node startNode, boolean optional,
Collection<PatternNode> optionalNodes )
{
this( start, startNode, optional );
this.optionalNodes = optionalNodes;
}
PatternNode getStartPatternNode()
{
return startPatternNode;
}
Node getStartNode()
{
return startNode;
}
private static class CallPosition
{
private PatternPosition patternPosition;
private Iterator<Relationship> relItr;
private Relationship lastRel;
private PatternRelationship currentPRel;
private boolean popUncompleted;
CallPosition( PatternPosition patternPosition,
Relationship lastRel,
Iterator<Relationship> relItr,
PatternRelationship currentPRel, boolean popUncompleted )
{
this.patternPosition = patternPosition;
this.relItr = relItr;
this.lastRel = lastRel;
this.currentPRel = currentPRel;
this.popUncompleted = popUncompleted;
}
public void setLastVisitedRelationship( Relationship rel )
{
this.lastRel = rel;
}
public Relationship getLastVisitedRelationship()
{
return lastRel;
}
public boolean shouldPopUncompleted()
{
return popUncompleted;
}
public PatternPosition getPatternPosition()
{
return patternPosition;
}
public PatternRelationship getPatternRelationship()
{
return currentPRel;
}
public Iterator<Relationship> getRelationshipIterator()
{
return relItr;
}
}
private Stack<CallPosition> callStack =
new Stack<CallPosition>();
private Stack<PatternPosition> uncompletedPositions =
new Stack<PatternPosition>();
private Stack<PatternElement> foundElements =
new Stack<PatternElement>();
private PatternMatch findNextMatch()
{
if ( callStack.isEmpty() && currentPosition != null )
{
// try find first match
if ( traverse( currentPosition, true ) )
{
// found first match, return it
currentPosition = null;
HashMap<PatternNode,PatternElement> filteredElements =
new HashMap<PatternNode, PatternElement>();
HashMap<PatternRelationship,Relationship> relElements =
new HashMap<PatternRelationship,Relationship>();
for ( PatternElement element : foundElements )
{
filteredElements.put( element.getPatternNode(),
element );
relElements.put( element.getFromPatternRelationship(),
element.getFromRelationship() );
}
PatternMatch patternMatch = new PatternMatch(
filteredElements, relElements );
foundElements.pop();
return patternMatch;
}
currentPosition = null;
}
else if ( !callStack.isEmpty() )
{
// try find other match from last found match
boolean matchFound = false;
do
{
CallPosition callStackInformation = callStack.peek();
matchFound = traverse( callStackInformation );
} while ( !callStack.isEmpty() && !matchFound );
if ( matchFound )
{
// found another match, returning it
HashMap<PatternNode,PatternElement> filteredElements =
new HashMap<PatternNode, PatternElement>();
HashMap<PatternRelationship,Relationship> relElements =
new HashMap<PatternRelationship,Relationship>();
for ( PatternElement element : foundElements )
{
filteredElements.put( element.getPatternNode(), element );
relElements.put( element.getFromPatternRelationship(),
element.getFromRelationship() );
}
PatternMatch patternMatch = new PatternMatch(
filteredElements, relElements );
foundElements.pop();
return patternMatch;
}
}
return null;
}
private boolean traverse( CallPosition callPos )
{
// make everything like it was before we returned previous match
PatternPosition currentPos = callPos.getPatternPosition();
PatternRelationship pRel = callPos.getPatternRelationship();
pRel.mark();
visitedRels.remove( callPos.getLastVisitedRelationship() );
Node currentNode = currentPos.getCurrentNode();
Iterator<Relationship> relItr = callPos.getRelationshipIterator();
while ( relItr.hasNext() )
{
Relationship rel = relItr.next();
if ( visitedRels.contains( rel ) )
{
continue;
}
if ( !checkProperties( pRel, rel ) )
{
continue;
}
Node otherNode = rel.getOtherNode( currentNode );
PatternNode otherPosition = pRel.getOtherNode(
currentPos.getPatternNode() );
pRel.mark();
visitedRels.add( rel );
if ( traverse( new PatternPosition( otherNode,
otherPosition, pRel, rel, optional ), true ) )
{
callPos.setLastVisitedRelationship( rel );
return true;
}
visitedRels.remove( rel );
pRel.unMark();
}
pRel.unMark();
if ( callPos.shouldPopUncompleted() )
{
uncompletedPositions.pop();
}
callStack.pop();
foundElements.pop();
return false;
}
private boolean traverse( PatternPosition currentPos,
boolean pushElement )
{
PatternNode pNode = currentPos.getPatternNode();
Node currentNode = currentPos.getCurrentNode();
if ( !checkProperties( pNode, currentNode ) )
{
return false;
}
if ( pushElement )
{
foundElements.push( new PatternElement(
currentPos.getPatternNode(),
currentPos.fromPatternRel(),
currentPos.getCurrentNode(),
currentPos.fromRelationship() ) );
}
if ( currentPos.hasNext() )
{
boolean popUncompleted = false;
PatternRelationship pRel = currentPos.next();
if ( currentPos.hasNext() )
{
uncompletedPositions.push( currentPos );
popUncompleted = true;
}
assert !pRel.isMarked();
Iterator<Relationship> relItr = getRelationshipIterator(
currentPos.getPatternNode(), currentNode, pRel );
pRel.mark();
while ( relItr.hasNext() )
{
Relationship rel = relItr.next();
if ( visitedRels.contains( rel ) )
{
continue;
}
if ( !checkProperties( pRel, rel ) )
{
continue;
}
Node otherNode = rel.getOtherNode( currentNode );
PatternNode otherPosition = pRel.getOtherNode(
currentPos.getPatternNode() );
visitedRels.add( rel );
CallPosition callPos = new CallPosition(
currentPos, rel, relItr, pRel, popUncompleted );
callStack.push( callPos );
if ( traverse( new PatternPosition( otherNode,
otherPosition, pRel, rel, optional ), true ) )
{
return true;
}
callStack.pop();
visitedRels.remove( rel );
}
pRel.unMark();
if ( popUncompleted )
{
uncompletedPositions.pop();
}
foundElements.pop();
return false;
}
boolean matchFound = true;
if ( !uncompletedPositions.isEmpty() )
{
PatternPosition digPos = uncompletedPositions.pop();
digPos.reset();
matchFound = traverse( digPos, false );
uncompletedPositions.push( digPos );
return matchFound;
}
return true;
}
private Iterator<Relationship> getRelationshipIterator(
PatternNode fromNode, Node currentNode, PatternRelationship pRel )
{
Iterator<Relationship> relItr = null;
if ( pRel.anyRelType() )
{
relItr = currentNode.getRelationships( pRel.getDirectionFrom(
fromNode ) ).iterator();
}
else
{
relItr = currentNode.getRelationships( pRel.getType(),
pRel.getDirectionFrom( fromNode ) ).iterator();
}
return relItr;
}
private boolean checkProperties(
PatternNode patternNode, Node neoNode )
{
for ( String propertyName : patternNode.getPropertiesExist() )
{
if ( !neoNode.hasProperty( propertyName ) )
{
return false;
}
}
for ( String propertyName : patternNode.getPropertiesEqual() )
{
if ( !neoPropertyHasValue( neoNode, patternNode,
propertyName ) )
{
return false;
}
}
return true;
}
private boolean checkProperties(
PatternRelationship patternRel, Relationship neoRel )
{
for ( String propertyName : patternRel.getPropertiesExist() )
{
if ( !neoRel.hasProperty( propertyName ) )
{
return false;
}
}
for ( String propertyName : patternRel.getPropertiesEqual() )
{
if ( !neoPropertyHasValue( neoRel, patternRel, propertyName ) )
{
return false;
}
}
return true;
}
private boolean neoPropertyHasValue( Node neoNode,
PatternNode patternNode, String propertyName )
{
if ( !neoNode.hasProperty( propertyName ) )
{
return false;
}
Object[] patternValues =
patternNode.getPropertyValue( propertyName );
Object neoValue = neoNode.getProperty( propertyName );
Collection<Object> neoValues =
NeoArrayPropertyUtil.neoValueToCollection( neoValue );
neoValues.retainAll( Arrays.asList( patternValues ) );
return !neoValues.isEmpty();
}
private boolean neoPropertyHasValue( Relationship neoRel,
PatternRelationship patternRel, String propertyName )
{
if ( !neoRel.hasProperty( propertyName ) )
{
return false;
}
Object[] patternValues =
patternRel.getPropertyValue( propertyName );
Object neoValue = neoRel.getProperty( propertyName );
Collection<Object> neoValues =
NeoArrayPropertyUtil.neoValueToCollection( neoValue );
neoValues.retainAll( Arrays.asList( patternValues ) );
return !neoValues.isEmpty();
}
public Iterator<PatternMatch> iterator()
{
return this;
}
private PatternMatch match = null;
private PatternMatch optionalMatch = null;
public boolean hasNext()
{
if ( match == null )
{
match = findNextMatch();
optionalFinder = null;
}
else if ( optionalNodes != null )
{
if ( optionalFinder == null )
{
optionalFinder = new OptionalPatternFinder(
match, optionalNodes );
}
if ( optionalMatch == null )
{
optionalMatch =
optionalFinder.findNextOptionalPatterns();
}
if ( optionalMatch == null && optionalFinder.anyMatchFound() )
{
match = null;
return hasNext();
}
}
return match != null;
}
public PatternMatch next()
{
if ( match == null )
{
match = findNextMatch();
optionalFinder = null;
}
PatternMatch matchToReturn = match;
PatternMatch optionalMatchToReturn = null;
if ( match != null && optionalNodes != null )
{
if ( optionalFinder == null )
{
optionalFinder = new OptionalPatternFinder(
match, optionalNodes );
}
if ( optionalMatch == null )
{
optionalMatch = optionalFinder.findNextOptionalPatterns();
}
optionalMatchToReturn = optionalMatch;
optionalMatch = null;
if ( optionalMatchToReturn == null )
{
match = null;
if ( optionalFinder.anyMatchFound() )
{
return next();
}
}
}
else
{
match = null;
}
if ( matchToReturn == null )
{
throw new NoSuchElementException();
}
return optionalMatchToReturn != null ?
PatternMatch.merge( matchToReturn, optionalMatchToReturn ) :
matchToReturn;
}
public void remove()
{
throw new UnsupportedOperationException();
}
}
}