/** * 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.shell.kernel.apps; import java.rmi.RemoteException; import java.util.ArrayList; import java.util.Collection; import java.util.Collections; import java.util.Comparator; import java.util.List; import java.util.Map; import java.util.TreeMap; import org.neo4j.graphdb.Direction; import org.neo4j.graphdb.Expander; import org.neo4j.graphdb.Relationship; import org.neo4j.graphdb.RelationshipExpander; import org.neo4j.graphdb.RelationshipType; import org.neo4j.helpers.Service; import org.neo4j.kernel.Traversal; import org.neo4j.shell.App; import org.neo4j.shell.AppCommandParser; import org.neo4j.shell.OptionDefinition; import org.neo4j.shell.OptionValueType; import org.neo4j.shell.Output; import org.neo4j.shell.Session; import org.neo4j.shell.ShellException; /** * Mimics the POSIX application with the same name, i.e. lists * properties/relationships on a node or a relationship. */ @Service.Implementation( App.class ) public class Ls extends ReadOnlyGraphDatabaseApp { /** * Constructs a new "ls" application. */ public Ls() { super(); this.addOptionDefinition( "b", new OptionDefinition( OptionValueType.NONE, "Brief summary instead of full content" ) ); this.addOptionDefinition( "d", new OptionDefinition( OptionValueType.MUST, "Direction filter for relationships: " + this.directionAlternatives() ) ); this.addOptionDefinition( "v", new OptionDefinition( OptionValueType.NONE, "Verbose mode" ) ); this.addOptionDefinition( "p", new OptionDefinition( OptionValueType.NONE, "Lists properties" ) ); this.addOptionDefinition( "r", new OptionDefinition( OptionValueType.NONE, "Lists relationships" ) ); this.addOptionDefinition( "f", new OptionDefinition( OptionValueType.MUST, "Filters property keys/values. Supplied either as a single value " + "or as a JSON string where both keys and values can contain regex. " + "Starting/ending {} brackets are optional. Examples:\n" + "\"username\"\n" + " property/relationship 'username' gets listed\n" + "\".*name: ma.*, age: ''\"\n" + " properties with keys matching '.*name' and values matching ma.*'\n" + " gets listed, as well as the 'age' property. Also " + "relationships\n" + " matching" + " '.*name' or 'age' gets listed" ) ); this.addOptionDefinition( "i", new OptionDefinition( OptionValueType.NONE, "Filters are case-insensitive (case-sensitive by default)" ) ); this.addOptionDefinition( "l", new OptionDefinition( OptionValueType.NONE, "Filters matches more loosely, i.e. it's considered a match if just " + "a part of a value matches the pattern, not necessarily the whole value" ) ); } @Override public String getDescription() { return "Lists the contents of the current node or relationship. " + "Optionally supply\n" + "node id for listing a certain node using \"ls <node-id>\""; } @Override protected String exec( AppCommandParser parser, Session session, Output out ) throws ShellException, RemoteException { boolean brief = parser.options().containsKey( "b" ); boolean verbose = parser.options().containsKey( "v" ); boolean displayProperties = parser.options().containsKey( "p" ); boolean displayRelationships = parser.options().containsKey( "r" ); boolean caseInsensitiveFilters = parser.options().containsKey( "i" ); boolean looseFilters = parser.options().containsKey( "l" ); Map<String, Object> filterMap = parseFilter( parser.options().get( "f" ), out ); if ( !displayProperties && !displayRelationships ) { displayProperties = true; displayRelationships = true; } NodeOrRelationship thing = null; if ( parser.arguments().isEmpty() ) { thing = this.getCurrent( session ); } else { thing = NodeOrRelationship.wrap( this.getNodeById( Long .parseLong( parser.arguments().get( 0 ) ) ) ); } if ( displayProperties ) { displayProperties( thing, out, verbose, filterMap, caseInsensitiveFilters, looseFilters, brief ); } if ( displayRelationships ) { if ( thing.isNode() ) { displayRelationships( parser, thing, session, out, verbose, filterMap, caseInsensitiveFilters, looseFilters, brief ); } else { displayNodes( parser, thing, session, out ); } } return null; } private void displayNodes( AppCommandParser parser, NodeOrRelationship thing, Session session, Output out ) throws RemoteException, ShellException { Relationship rel = thing.asRelationship(); out.println( getDisplayName( getServer(), session, rel.getStartNode(), false ) + " --" + getDisplayName( getServer(), session, rel, true, false ) + "-> " + getDisplayName( getServer(), session, rel.getEndNode(), false ) ); } private Iterable<String> sortKeys( Iterable<String> source ) { List<String> list = new ArrayList<String>(); for ( String item : source ) { list.add( item ); } Collections.sort( list, new Comparator<String>() { public int compare( String item1, String item2 ) { return item1.toLowerCase().compareTo( item2.toLowerCase() ); } } ); return list; } private Map<String, Collection<Relationship>> readAllRelationships( Iterable<Relationship> source ) { Map<String, Collection<Relationship>> map = new TreeMap<String, Collection<Relationship>>(); for ( Relationship rel : source ) { String type = rel.getType().name().toLowerCase(); Collection<Relationship> rels = map.get( type ); if ( rels == null ) { rels = new ArrayList<Relationship>(); map.put( type, rels ); } rels.add( rel ); } return map; } private void displayProperties( NodeOrRelationship thing, Output out, boolean verbose, Map<String, Object> filterMap, boolean caseInsensitiveFilters, boolean looseFilters, boolean brief ) throws RemoteException { int longestKey = findLongestKey( thing ); int count = 0; for ( String key : sortKeys( thing.getPropertyKeys() ) ) { boolean matches = filterMap.isEmpty(); Object value = thing.getProperty( key ); for ( Map.Entry<String, Object> filter : filterMap.entrySet() ) { if ( matches( newPattern( filter.getKey(), caseInsensitiveFilters ), key, caseInsensitiveFilters, looseFilters ) ) { String filterValue = filter.getValue() != null ? filter.getValue().toString() : null; if ( matches( newPattern( filterValue, caseInsensitiveFilters ), value.toString(), caseInsensitiveFilters, looseFilters ) ) { matches = true; break; } } } if ( !matches ) { continue; } count++; if ( !brief ) { StringBuilder builder = new StringBuilder(); builder.append( "*" + key ); builder.append( multiply( " ", longestKey - key.length() + 1 ) ); builder.append( "=" + format( value, true ) ); if ( verbose ) { builder.append( " (" + getNiceType( value ) + ")" ); } out.println( builder.toString() ); } } if ( brief ) { out.println( "Property count: " + count ); } } private void displayRelationships( AppCommandParser parser, NodeOrRelationship thing, Session session, Output out, boolean verbose, Map<String, Object> filterMap, boolean caseInsensitiveFilters, boolean looseFilters, boolean brief ) throws ShellException, RemoteException { Direction direction = getDirection( parser.options().get( "d" ), Direction.BOTH ); boolean displayOutgoing = direction == Direction.BOTH || direction == Direction.OUTGOING; boolean displayIncoming = direction == Direction.BOTH || direction == Direction.INCOMING; if ( displayOutgoing ) { displayRelationships( thing, session, out, verbose, Direction.OUTGOING, "--", "->", filterMap, caseInsensitiveFilters, looseFilters, brief ); } if ( displayIncoming ) { displayRelationships( thing, session, out, verbose, Direction.INCOMING, "<-", "--", filterMap, caseInsensitiveFilters, looseFilters, brief ); } } private void displayRelationships( NodeOrRelationship thing, Session session, Output out, boolean verbose, Direction direction, String prefixString, String postfixString, Map<String, Object> filterMap, boolean caseInsensitiveFilters, boolean looseFilters, boolean brief ) throws ShellException, RemoteException { RelationshipExpander expander = toExpander( direction, filterMap, caseInsensitiveFilters, looseFilters ); Map<String, Collection<Relationship>> relationships = readAllRelationships( expander.expand( thing.asNode() ) ); for ( Map.Entry<String, Collection<Relationship>> entry : relationships.entrySet() ) { if ( brief ) { out.println( getDisplayName( getServer(), session, thing, true ) + " " + prefixString + getDisplayName( getServer(), session, entry.getValue().iterator().next(), false, true ) + postfixString + " x" + entry.getValue().size() ); } else { for ( Relationship rel : entry.getValue() ) { StringBuffer buf = new StringBuffer( getDisplayName( getServer(), session, thing, true ) ); buf.append( " " + prefixString ).append( getDisplayName( getServer(), session, rel, verbose, true ) ); buf.append( postfixString + " " ); buf.append( getDisplayName( getServer(), session, direction == Direction.OUTGOING ? rel.getEndNode() : rel.getStartNode(), true ) ); out.println( buf ); } } } } private RelationshipExpander toExpander( Direction direction, Map<String, Object> filterMap, boolean caseInsensitiveFilters, boolean looseFilters ) { Expander expander = Traversal.emptyExpander(); for ( RelationshipType type : getServer().getDb().getRelationshipTypes() ) { boolean matches = false; if ( filterMap == null || filterMap.isEmpty() ) { matches = true; } else { for ( String filter : filterMap.keySet() ) { if ( matches( newPattern( filter, caseInsensitiveFilters ), type.name(), caseInsensitiveFilters, looseFilters ) ) { matches = true; break; } } } if ( matches ) { expander = expander.add( type, direction ); } } return expander; } private static String getNiceType( Object value ) { return Set.getValueTypeName( value.getClass() ); } private static int findLongestKey( NodeOrRelationship thing ) { int length = 0; for ( String key : thing.getPropertyKeys() ) { if ( key.length() > length ) { length = key.length(); } } return length; } }