/* * Copyright (c) 2002-2009 "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.shell.kernel.apps; import java.rmi.RemoteException; import java.util.ArrayList; import java.util.HashMap; import java.util.List; import java.util.Map; import java.util.regex.Pattern; import org.neo4j.graphdb.Direction; import org.neo4j.graphdb.Node; import org.neo4j.graphdb.RelationshipType; import org.neo4j.graphdb.ReturnableEvaluator; import org.neo4j.graphdb.StopEvaluator; import org.neo4j.graphdb.Traverser; import org.neo4j.graphdb.Traverser.Order; import org.neo4j.shell.AppCommandParser; import org.neo4j.shell.OptionValueType; import org.neo4j.shell.Output; import org.neo4j.shell.Session; import org.neo4j.shell.ShellException; /** * Traverses the graph using {@link Traverser}. */ public class Trav extends GraphDatabaseApp { /** * Constructs a new command which can traverse the graph. */ public Trav() { super(); this.addValueType( "o", new OptionContext( OptionValueType.MUST, "The traversal order [BREADTH_FIRST/DEPTH_FIRST/breadth/depth]" ) ); this.addValueType( "r", new OptionContext( OptionValueType.MUST, "The relationship type(s) expressed as a JSON string " + "(supports regex\n" + "matching of the types) f.ex. " + "\"MY_REL_TYPE:out,.*_HAS_.*:both\".\n" + "Matching is case-insensitive." ) ); this.addValueType( "f", new OptionContext( OptionValueType.MUST, "Filters node property keys/values. Supplied either as a single " + "value\n" + "or as a JSON string where both keys and values can " + "contain regex.\n" + "Starting/ending {} brackets are optional. Examples:\n" + "\"username\"\n" + " nodes which has property 'username' gets listed\n" + "\".*name: ma.*, age: ''\"\n" + " nodes which has any key matching '.*name' where the " + "property value\n" + " for that key matches 'ma.*' AND has the 'age' property " + "gets listed" ) ); this.addValueType( "i", new OptionContext( OptionValueType.NONE, "Filters are case-insensitive (case-sensitive by default)" ) ); this.addValueType( "l", new OptionContext( OptionValueType.NONE, "Filters matches more loosely, i.e. it's considered a match if " + "just\n" + "a part of a value matches the pattern, not necessarily " + "the whole value" ) ); this.addValueType( "c", new OptionContext( OptionValueType.MUST, "Command to run for each returned node. Use $n as a node-id " + "replacement.\n" + "Example: -c \"ls -f name $n\". Multiple commands " + "can be supplied with\n" + "&& in between" ) ); } @Override public String getDescription() { return "Traverses the node space from your current position (pwd). " + "It's a reflection\n" + "of the neo4j traverser API with some options for filtering " + "which nodes\n" + "will be returned."; } @Override protected String exec( AppCommandParser parser, Session session, Output out ) throws ShellException, RemoteException { assertCurrentIsNode( session ); Node node = this.getCurrent( session ).asNode(); boolean caseInsensitiveFilters = parser.options().containsKey( "i" ); boolean looseFilters = parser.options().containsKey( "l" ); Object[] relationshipTypes = parseRelationshipTypes( parser, out, caseInsensitiveFilters, looseFilters ); if ( relationshipTypes.length == 0 ) { out.println( "No matching relationship types" ); return null; } StopEvaluator stopEvaluator = parseStopEvaluator( parser ); ReturnableEvaluator returnableEvaluator = parseReturnableEvaluator( parser ); Order order = parseOrder( parser ); String filterString = parser.options().get( "f" ); Map<String, Object> filterMap = filterString != null ? parseFilter( filterString, out ) : null; String commandToRun = parser.options().get( "c" ); String[] commandsToRun = commandToRun != null ? commandToRun.split( Pattern.quote( "&&" ) ) : new String[ 0 ]; for ( Node traversedNode : node.traverse( order, stopEvaluator, returnableEvaluator, relationshipTypes ) ) { boolean hit = false; if ( filterMap == null ) { hit = true; } else { Map<String, Boolean> matchPerFilterKey = new HashMap<String, Boolean>(); for ( String key : traversedNode.getPropertyKeys() ) { for ( Map.Entry<String, Object> filterEntry : filterMap.entrySet() ) { String filterKey = filterEntry.getKey(); if ( matchPerFilterKey.containsKey( filterKey ) ) { continue; } if ( matches( newPattern( filterKey, caseInsensitiveFilters ), key, caseInsensitiveFilters, looseFilters ) ) { Object value = traversedNode.getProperty( key ); String filterPattern = filterEntry.getValue() != null ? filterEntry.getValue().toString() : null; if ( matches( newPattern( filterPattern, caseInsensitiveFilters ), value.toString(), caseInsensitiveFilters, looseFilters ) ) { matchPerFilterKey.put( filterKey, true ); } } } } if ( matchPerFilterKey.size() == filterMap.size() ) { hit = true; } } if ( hit ) { out.println( getDisplayName( getServer(), session, traversedNode ) ); Map<String, Object> data = new HashMap<String, Object>(); data.put( "n", traversedNode.getId() ); for ( String command : commandsToRun ) { String line = templateString( command, "\\$", data ); getServer().interpretLine( line, session, out ); } out.println(); } } return null; } private String templateString( String templateString, String variablePrefix, Map<String, Object> data ) { // Sort data strings on length. Map<Integer, List<String>> lengthMap = new HashMap<Integer, List<String>>(); int longest = 0; for ( String key : data.keySet() ) { int length = key.length(); if ( length > longest ) { longest = length; } List<String> innerList = null; Integer innerKey = Integer.valueOf( length ); if ( lengthMap.containsKey( innerKey ) ) { innerList = lengthMap.get( innerKey ); } else { innerList = new ArrayList<String>(); lengthMap.put( innerKey, innerList ); } innerList.add( key ); } // Replace it. String result = templateString; for ( int i = longest; i >= 0; i-- ) { Integer lengthKey = Integer.valueOf( i ); if ( !lengthMap.containsKey( lengthKey ) ) { continue; } List<String> list = lengthMap.get( lengthKey ); for ( String key : list ) { String replacement = data.get( key ).toString(); String regExpMatchString = variablePrefix + key; result = result.replaceAll( regExpMatchString, replacement ); } } return result; } private Order parseOrder( AppCommandParser parser ) { return ( Order ) parseEnum( Order.class, parser.options().get( "o" ), Order.DEPTH_FIRST ); } private ReturnableEvaluator parseReturnableEvaluator( AppCommandParser parser ) { // TODO return ReturnableEvaluator.ALL_BUT_START_NODE; } private StopEvaluator parseStopEvaluator( AppCommandParser parser ) { // TODO return StopEvaluator.END_OF_GRAPH; } private Object[] parseRelationshipTypes( AppCommandParser parser, Output out, boolean caseInsensitiveFilters, boolean looseFilters ) throws ShellException, RemoteException { String option = parser.options().get( "r" ); List<Object> result = new ArrayList<Object>(); if ( option == null ) { for ( RelationshipType type : getServer().getDb().getRelationshipTypes() ) { result.add( type ); result.add( Direction.BOTH ); } } else { Map<String, Object> map = parseFilter( option, out ); List<RelationshipType> allRelationshipTypes = new ArrayList<RelationshipType>(); for ( RelationshipType type : getServer().getDb().getRelationshipTypes() ) { allRelationshipTypes.add( type ); } for ( Map.Entry<String, Object> entry : map.entrySet() ) { String type = entry.getKey(); Direction direction = getDirection( ( String ) entry.getValue(), Direction.BOTH ); Pattern typePattern = newPattern( type, caseInsensitiveFilters ); for ( RelationshipType relationshipType : allRelationshipTypes ) { if ( relationshipType.name().equals( type ) || matches( typePattern, relationshipType.name(), caseInsensitiveFilters, looseFilters ) ) { result.add( relationshipType ); result.add( direction ); } } } } return result.toArray(); } }