/**
* 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.List;
import java.util.TreeSet;
import org.neo4j.graphdb.Node;
import org.neo4j.graphdb.Relationship;
import org.neo4j.helpers.Service;
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;
import org.neo4j.shell.TextUtil;
import org.neo4j.shell.impl.RelationshipToNodeIterable;
/**
* Mimics the POSIX application with the same name, i.e. traverses to a node.
*/
@Service.Implementation( App.class )
public class Cd extends ReadOnlyGraphDatabaseApp
{
private static final String START_ALIAS = "start";
private static final String END_ALIAS = "end";
/**
* The {@link Session} key to use to store the current node and working
* directory (i.e. the path which the client got to it).
*/
public static final String WORKING_DIR_KEY = "WORKING_DIR";
/**
* Constructs a new cd application.
*/
public Cd()
{
this.addOptionDefinition( "a", new OptionDefinition( OptionValueType.NONE,
"Absolute id, new primitive doesn't need to be connected to the current one" ) );
this.addOptionDefinition( "r", new OptionDefinition( OptionValueType.NONE,
"Makes the supplied id represent a relationship instead of a node" ) );
}
@Override
public String getDescription()
{
return "Changes the current node or relationship, i.e. traverses " +
"one step to another node or relationship. Usage: cd <id>";
}
@Override
public List<String> completionCandidates( String partOfLine, Session session )
{
String lastWord = TextUtil.lastWordOrQuoteOf( partOfLine, false );
if ( lastWord.startsWith( "-" ) )
{
return super.completionCandidates( partOfLine, session );
}
try
{
TreeSet<String> result = new TreeSet<String>();
NodeOrRelationship current = getCurrent( session );
if ( current.isNode() )
{
// TODO Check if -r is supplied
Node node = current.asNode();
for ( Node otherNode : RelationshipToNodeIterable.wrap(
node.getRelationships(), node ) )
{
long otherNodeId = otherNode.getId();
String title = findTitle( getServer(), session, otherNode );
if ( title != null )
{
if ( !result.contains( title ) )
{
maybeAddCompletionCandidate( result, title + "," + otherNodeId,
lastWord );
}
}
maybeAddCompletionCandidate( result, "" + otherNodeId, lastWord );
}
}
else
{
maybeAddCompletionCandidate( result, START_ALIAS, lastWord );
maybeAddCompletionCandidate( result, END_ALIAS, lastWord );
Relationship rel = current.asRelationship();
maybeAddCompletionCandidate( result, "" + rel.getStartNode().getId(), lastWord );
maybeAddCompletionCandidate( result, "" + rel.getEndNode().getId(), lastWord );
}
return new ArrayList<String>( result );
}
catch ( ShellException e )
{
e.printStackTrace();
return super.completionCandidates( partOfLine, session );
}
}
private static void maybeAddCompletionCandidate( Collection<String> candidates,
String candidate, String lastWord )
{
if ( lastWord.length() == 0 || candidate.startsWith( lastWord ) )
{
candidates.add( candidate );
}
}
@Override
protected String exec( AppCommandParser parser, Session session,
Output out ) throws ShellException, RemoteException
{
List<TypedId> paths = readPaths( session );
NodeOrRelationship current = getCurrent( session );
NodeOrRelationship newThing = null;
if ( parser.arguments().isEmpty() )
{
newThing = NodeOrRelationship.wrap(
getServer().getDb().getReferenceNode() );
paths.clear();
}
else
{
String arg = parser.arguments().get( 0 );
TypedId newId = current.getTypedId();
if ( arg.equals( ".." ) )
{
if ( paths.size() > 0 )
{
newId = paths.remove( paths.size() - 1 );
}
}
else if ( arg.equals( "." ) )
{
}
else if ( arg.equals( START_ALIAS ) || arg.equals( END_ALIAS ) )
{
newId = getStartOrEnd( current, arg );
paths.add( current.getTypedId() );
}
else
{
long suppliedId = -1;
try
{
suppliedId = Long.parseLong( arg );
}
catch ( NumberFormatException e )
{
suppliedId = findNodeWithTitle( current.asNode(), arg, session );
if ( suppliedId == -1 )
{
throw new ShellException( "No connected node with title '" + arg + "'" );
}
}
newId = parser.options().containsKey( "r" ) ?
new TypedId( NodeOrRelationship.TYPE_RELATIONSHIP, suppliedId ) :
new TypedId( NodeOrRelationship.TYPE_NODE, suppliedId );
if ( newId.equals( current.getTypedId() ) )
{
throw new ShellException( "Can't cd to where you stand" );
}
boolean absolute = parser.options().containsKey( "a" );
if ( !absolute && !this.isConnected( current, newId ) )
{
throw new ShellException(
getDisplayName( getServer(), session, newId, false ) +
" isn't connected to the current primitive," +
" use -a to force it to go there anyway" );
}
paths.add( current.getTypedId() );
}
newThing = this.getThingById( newId );
}
setCurrent( session, newThing );
session.set( WORKING_DIR_KEY, this.makePath( paths ) );
return null;
}
private long findNodeWithTitle( Node node, String match, Session session )
{
Object[] matchParts = splitNodeTitleAndId( match );
if ( matchParts[1] != null )
{
return (Long) matchParts[1];
}
String titleMatch = (String) matchParts[0];
for ( Node otherNode : RelationshipToNodeIterable.wrap( node.getRelationships(), node ) )
{
String title = findTitle( getServer(), session, otherNode );
if ( titleMatch.equals( title ) )
{
return otherNode.getId();
}
}
return -1;
}
private Object[] splitNodeTitleAndId( String string )
{
int index = string.lastIndexOf( "," );
String title = null;
Long id = null;
try
{
id = Long.parseLong( string.substring( index + 1 ) );
title = string.substring( 0, index );
}
catch ( NumberFormatException e )
{
title = string;
}
return new Object[] { title, id };
}
private TypedId getStartOrEnd( NodeOrRelationship current, String arg )
throws ShellException
{
if ( !current.isRelationship() )
{
throw new ShellException( "Only allowed on relationships" );
}
Node newNode = null;
if ( arg.equals( START_ALIAS ) )
{
newNode = current.asRelationship().getStartNode();
}
else if ( arg.equals( END_ALIAS ) )
{
newNode = current.asRelationship().getEndNode();
}
else
{
throw new ShellException( "Unknown alias '" + arg + "'" );
}
return NodeOrRelationship.wrap( newNode ).getTypedId();
}
private boolean isConnected( NodeOrRelationship current, TypedId newId )
throws ShellException
{
if ( current.isNode() )
{
Node currentNode = current.asNode();
for ( Relationship rel : currentNode.getRelationships() )
{
if ( newId.isNode() )
{
if ( rel.getOtherNode( currentNode ).getId() ==
newId.getId() )
{
return true;
}
}
else
{
if ( rel.getId() == newId.getId() )
{
return true;
}
}
}
}
else
{
if ( newId.isRelationship() )
{
return false;
}
Relationship relationship = current.asRelationship();
if ( relationship.getStartNode().getId() == newId.getId() ||
relationship.getEndNode().getId() == newId.getId() )
{
return true;
}
}
return false;
}
/**
* Reads the session variable specified in {@link #WORKING_DIR_KEY} and
* returns it as a list of typed ids.
* @param session the session to read from.
* @return the working directory as a list.
* @throws RemoteException if an RMI error occurs.
*/
public static List<TypedId> readPaths( Session session )
throws RemoteException
{
List<TypedId> list = new ArrayList<TypedId>();
String path = (String) session.get( WORKING_DIR_KEY );
if ( path != null && path.trim().length() > 0 )
{
for ( String typedId : path.split( "," ) )
{
list.add( new TypedId( typedId ) );
}
}
return list;
}
private String makePath( List<TypedId> paths )
{
StringBuffer buffer = new StringBuffer();
for ( TypedId typedId : paths )
{
if ( buffer.length() > 0 )
{
buffer.append( "," );
}
buffer.append( typedId.toString() );
}
return buffer.length() > 0 ? buffer.toString() : null;
}
}