All Downloads are FREE. Search and download functionalities are using the official Maven repository.

org.neo4j.shell.kernel.apps.TransactionProvidingApp Maven / Gradle / Ivy

There is a newer version: 3.3.2
Show newest version
/*
 * Copyright (c) 2002-2016 "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 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 General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License
 * along with this program.  If not, see .
 */
package org.neo4j.shell.kernel.apps;

import java.lang.reflect.Array;
import java.rmi.RemoteException;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.TreeMap;
import java.util.function.Function;
import java.util.regex.Pattern;

import org.neo4j.graphdb.Direction;
import org.neo4j.graphdb.GraphDatabaseService;
import org.neo4j.graphdb.Label;
import org.neo4j.graphdb.Node;
import org.neo4j.graphdb.NotFoundException;
import org.neo4j.graphdb.Path;
import org.neo4j.graphdb.PathExpander;
import org.neo4j.graphdb.PathExpanderBuilder;
import org.neo4j.graphdb.PropertyContainer;
import org.neo4j.graphdb.Relationship;
import org.neo4j.graphdb.RelationshipType;
import org.neo4j.graphdb.Transaction;
import org.neo4j.helpers.collection.Pair;
import org.neo4j.shell.App;
import org.neo4j.shell.AppCommandParser;
import org.neo4j.shell.Continuation;
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.AbstractApp;
import org.neo4j.shell.kernel.GraphDatabaseShellServer;
import org.neo4j.shell.util.json.JSONArray;
import org.neo4j.shell.util.json.JSONException;

import static org.neo4j.kernel.api.KernelTransaction.Type.implicit;
import static org.neo4j.kernel.api.security.AccessMode.Static.FULL;
import static org.neo4j.shell.ShellException.stackTraceAsString;

/**
 * An implementation of {@link App} which has common methods and functionality
 * to use with neo4j.
 */
public abstract class TransactionProvidingApp extends AbstractApp
{
    private static final Label[] EMPTY_LABELS = new Label[0];
    private static final RelationshipType[] EMPTY_REL_TYPES = new RelationshipType[0];

    private static final Function CREATE_LABELS = values -> {
        Label[] labels = new Label[values.length];
        for ( int i = 0; i < values.length; i++ )
        {
            labels[i] = Label.label( values[i] );
        }
        return labels;
    };

    private static final Function CREATE_REL_TYPES = values -> {
        RelationshipType[] types = new RelationshipType[values.length];
        for ( int i = 0; i < values.length; i++ )
        {
            types[i] = RelationshipType.withName( values[i] );
        }
        return types;
    };

    protected static final String[] STANDARD_EVAL_IMPORTS = new String[] {
        "org.neo4j.graphdb",
        "org.neo4j.graphdb.event",
        "org.neo4j.graphdb.index",
        "org.neo4j.graphdb.traversal",
        "org.neo4j.kernel"
    };

    protected static final OptionDefinition OPTION_DEF_FOR_C = new OptionDefinition(
            OptionValueType.MUST,
            "Command to run for each returned node. Use $i for node/relationship id, example:\n" +
            "-c \"ls -f name $i\". Multiple commands can be supplied with && in between" );

    /**
     * @param server the {@link GraphDatabaseShellServer} to get the current
     * node/relationship from.
     * @param session the {@link Session} used by the client.
     * @return the current node/relationship the client stands on
     * at the moment.
     * @throws ShellException if some error occured.
     */
    public static NodeOrRelationship getCurrent(
        GraphDatabaseShellServer server, Session session ) throws ShellException
    {
        String currentThing = session.getCurrent();
        NodeOrRelationship result;
        /*                           Note: Artifact of removing the ref node, revisit and clean up */
        if ( currentThing == null || currentThing.equals( "(?)" )  )
        {
            throw new ShellException( "Not currently standing on any entity." );
        }
        else
        {
            TypedId typedId = new TypedId( currentThing );
            result = getThingById( server, typedId );
        }
        return result;
    }

    protected NodeOrRelationship getCurrent( Session session )
        throws ShellException
    {
        return getCurrent( getServer(), session );
    }

    public static boolean isCurrent( Session session, NodeOrRelationship thing ) throws ShellException
    {
        String currentThing = session.getCurrent();
        return currentThing != null && currentThing.equals(
                thing.getTypedId().toString() );
    }

    protected static void clearCurrent( Session session )
    {
        session.setCurrent( getDisplayNameForNonExistent());
    }

    protected static void setCurrent( Session session,
                                      NodeOrRelationship current ) throws ShellException
    {
        session.setCurrent( current.getTypedId().toString() );
    }

    protected void assertCurrentIsNode( Session session )
        throws ShellException
    {
        NodeOrRelationship current = getCurrent( session );
        if ( !current.isNode() )
        {
            throw new ShellException(
                "You must stand on a node to be able to do this" );
        }
    }

    @Override
    public GraphDatabaseShellServer getServer()
    {
        return ( GraphDatabaseShellServer ) super.getServer();
    }

    protected static RelationshipType getRelationshipType( String name )
    {
        return RelationshipType.withName( name );
    }

    protected static Direction getDirection( String direction ) throws ShellException
    {
        return getDirection( direction, Direction.OUTGOING );
    }

    protected static Direction getDirection( String direction,
        Direction defaultDirection ) throws ShellException
    {
        return parseEnum( Direction.class, direction, defaultDirection );
    }

    protected static NodeOrRelationship getThingById(
        GraphDatabaseShellServer server, TypedId typedId ) throws ShellException
    {
        NodeOrRelationship result;
        if ( typedId.isNode() )
        {
            try
            {
                result = NodeOrRelationship.wrap(
                    server.getDb().getNodeById( typedId.getId() ) );
            }
            catch ( NotFoundException e )
            {
                throw new ShellException( "Node " + typedId.getId() +
                    " not found" );
            }
        }
        else
        {
            try
            {
                result = NodeOrRelationship.wrap(
                    server.getDb().getRelationshipById( typedId.getId() ) );
            }
            catch ( NotFoundException e )
            {
                throw new ShellException( "Relationship " + typedId.getId() +
                    " not found" );
            }
        }
        return result;
    }

    protected NodeOrRelationship getThingById( TypedId typedId )
        throws ShellException
    {
        return getThingById( getServer(), typedId );
    }

    protected Node getNodeById( long id )
    {
        return this.getServer().getDb().getNodeById( id );
    }

    @Override
    public Continuation execute( AppCommandParser parser, Session session, Output out ) throws Exception
    {
        try ( Transaction tx = getServer().getDb().beginTransaction( implicit, FULL ) )
        {
            getServer().registerTopLevelTransactionInProgress( session.getId() );
            Continuation result = this.exec( parser, session, out );
            if ( result == Continuation.EXCEPTION_CAUGHT )
            {
                tx.failure();
            }
            else
            {
                tx.success();
            }
            return result;
        }
    }

    @Override
    public final List completionCandidates( String partOfLine, Session session ) throws ShellException
    {
        try ( Transaction tx = getServer().getDb().beginTx() )
        {
            List result = completionCandidatesInTx( partOfLine, session );
            tx.success();
            return result;
        }
    }

    protected List completionCandidatesInTx( String partOfLine, Session session ) throws ShellException
    {
        /*
         * Calls super of the non-tx version (completionCandidates). In an implementation the call hierarchy would be:
         *
         * TransactionProvidingApp.completionCandidates()
         *    --> MyApp.completionCandidatesInTx() - calls super.completionCandidatesInTx()
         *       --> TransactionProvidingApp.completionCandidatesInTx()
         *          --> AbstractApp.completionCandidates()
         */
        return super.completionCandidates( partOfLine, session );
    }

    protected String directionAlternatives()
    {
        return "OUTGOING, INCOMING, o, i";
    }

    protected abstract Continuation exec( AppCommandParser parser, Session session,
        Output out ) throws Exception;

    protected void printPath( Path path, boolean quietPrint, Session session, Output out )
            throws RemoteException, ShellException
    {
        StringBuilder builder = new StringBuilder();
        Node currentNode = null;
        for ( PropertyContainer entity : path )
        {
            String display;
            if ( entity instanceof Relationship )
            {
                display = quietPrint ? "" : getDisplayName( getServer(), session, (Relationship) entity, false, true );
                display = withArrows( (Relationship) entity, display, currentNode );
            }
            else
            {
                currentNode = (Node) entity;
                display = getDisplayName( getServer(), session, currentNode, true );
            }
            builder.append( display );
        }
        out.println( builder.toString() );
    }

    protected void setProperties( PropertyContainer entity, String propertyJson ) throws ShellException
    {
        if ( propertyJson == null )
        {
            return;
        }

        try
        {
            Map properties = parseJSONMap( propertyJson );
            for ( Map.Entry entry : properties.entrySet() )
            {
                entity.setProperty( entry.getKey(), jsonToNeo4jPropertyValue( entry.getValue() ) );
            }
        }
        catch ( JSONException e )
        {
            throw ShellException.wrapCause( e );
        }
    }

    private Object jsonToNeo4jPropertyValue( Object value ) throws ShellException
    {
        try
        {
            if ( value instanceof JSONArray )
            {
                JSONArray array = (JSONArray) value;
                Object firstItem = array.get( 0 );
                Object resultArray = Array.newInstance( firstItem.getClass(), array.length() );
                for ( int i = 0; i < array.length(); i++ )
                {
                    Array.set( resultArray, i, array.get( i ) );
                }
                return resultArray;
            }
            return value;
        }
        catch ( JSONException e )
        {
            throw new ShellException( stackTraceAsString( e ) );
        }
    }

    protected void cdTo( Session session, Node node ) throws RemoteException, ShellException
    {
        List wd = readCurrentWorkingDir( session );
        try
        {
            wd.add( getCurrent( session ).getTypedId() );
        }
        catch ( ShellException e )
        {   // OK not found then
        }
        writeCurrentWorkingDir( wd, session );
        setCurrent( session, NodeOrRelationship.wrap( node ) );
    }

    private static String getDisplayNameForCurrent(
            GraphDatabaseShellServer server, Session session )
            throws ShellException
    {
        NodeOrRelationship current = getCurrent( server, session );
        return current.isNode() ? "(me)" : "";
    }

    public static String getDisplayNameForNonExistent()
    {
        return "(?)";
    }

    /**
     * @param server the {@link GraphDatabaseShellServer} to run at.
     * @param session the {@link Session} used by the client.
     * @param thing the thing to get the name-representation for.
     * @param checkForMe check if node/rel is the current one in the session
     * @return the display name for a {@link Node}.
     * @throws ShellException if an error occurs.
     */
    public static String getDisplayName( GraphDatabaseShellServer server,
        Session session, NodeOrRelationship thing, boolean checkForMe )
        throws ShellException
    {
        if ( thing.isNode() )
        {
            return getDisplayName( server, session, thing.asNode(),
                    checkForMe );
        }
        else
        {
            return getDisplayName( server, session, thing.asRelationship(),
                true, checkForMe );
        }
    }

    /**
     * @param server the {@link GraphDatabaseShellServer} to run at.
     * @param session the {@link Session} used by the client.
     * @param typedId the id for the item to display.
     * @param checkForMe check if node/rel is the current one in the session
     * @return a display string for the {@code typedId}.
     * @throws ShellException if an error occurs.
     */
    public static String getDisplayName( GraphDatabaseShellServer server,
        Session session, TypedId typedId, boolean checkForMe )
        throws ShellException
    {
        return getDisplayName( server, session,
            getThingById( server, typedId ), checkForMe );
    }

    /**
     * @param server the {@link GraphDatabaseShellServer} to run at.
     * @param session the {@link Session} used by the client.
     * @param node the {@link Node} to get a display string for.
     * @param checkForMe check if node is the current one in the session
     * @return a display string for {@code node}.
     * @throws ShellException if an error occurs.
     */
    public static String getDisplayName( GraphDatabaseShellServer server,
        Session session, Node node, boolean checkForMe ) throws ShellException
    {
        if ( checkForMe &&
                isCurrent( session, NodeOrRelationship.wrap( node ) ) )
        {
            return getDisplayNameForCurrent( server, session );
        }

        String title = findTitle( session, node );
        StringBuilder result = new StringBuilder( "(" );
        result.append( title != null ? title + "," : "" );
        result.append( node.getId() );
        result.append( ")" );
        return result.toString();
    }

    protected static String findTitle( Session session, Node node ) throws ShellException
    {
        String keys = session.getTitleKeys();
        if ( keys == null )
        {
            return null;
        }

        String[] titleKeys = keys.split( Pattern.quote( "," ) );
        Pattern[] patterns = new Pattern[ titleKeys.length ];
        for ( int i = 0; i < titleKeys.length; i++ )
        {
            patterns[ i ] = Pattern.compile( titleKeys[ i ] );
        }
        for ( Pattern pattern : patterns )
        {
            for ( String nodeKey : node.getPropertyKeys() )
            {
                if ( matches( pattern, nodeKey, false, false ) )
                {
                    return trimLength( session,
                        format( node.getProperty( nodeKey ), false ) );
                }
            }
        }
        return null;
    }

    private static String trimLength( Session session, String string ) throws ShellException
    {
        String maxLengthString = session.getMaxTitleLength();
        int maxLength = maxLengthString != null ?
            Integer.parseInt( maxLengthString ) : Integer.MAX_VALUE;
        if ( string.length() > maxLength )
        {
            string = string.substring( 0, maxLength ) + "...";
        }
        return string;
    }

    /**
     * @param server the {@link GraphDatabaseShellServer} to run at.
     * @param session the {@link Session} used by the client.
     * @param relationship the {@link Relationship} to get a display name for.
     * @param verbose whether or not to include the relationship id as well.
     * @param checkForMe check if relationship is the current one in the session
     * @return a display string for the {@code relationship}.
     * @throws ShellException if an error occurs.
     */
    public static String getDisplayName( GraphDatabaseShellServer server,
        Session session, Relationship relationship, boolean verbose,
        boolean checkForMe ) throws ShellException
    {
        if ( checkForMe &&
                isCurrent( session, NodeOrRelationship.wrap( relationship ) ) )
        {
            return getDisplayNameForCurrent( server, session );
        }

        StringBuilder result = new StringBuilder( "[" );
        result.append( ":" ).append( relationship.getType().name() );
        result.append( verbose ? "," + relationship.getId() : "" );
        result.append( "]" );
        return result.toString();
    }

    public static String withArrows( Relationship relationship, String displayName, Node leftNode )
    {
        if ( relationship.getStartNode().equals( leftNode ) )
        {
            return "-" + displayName + "->";
        }
        else if ( relationship.getEndNode().equals( leftNode ) )
        {
            return "<-" + displayName + "-";
        }
        throw new IllegalArgumentException( leftNode + " is neither start nor end node to " + relationship );
    }

    protected static String fixCaseSensitivity( String string,
        boolean caseInsensitive )
    {
        return caseInsensitive ? string.toLowerCase() : string;
    }

    protected static Pattern newPattern( String pattern,
        boolean caseInsensitive )
    {
        return pattern == null ? null : Pattern.compile(
            fixCaseSensitivity( pattern, caseInsensitive ) );
    }

    protected static boolean matches( Pattern patternOrNull, String value,
        boolean caseInsensitive, boolean loose )
    {
        if ( patternOrNull == null )
        {
            return true;
        }

        value = fixCaseSensitivity( value, caseInsensitive );
        return loose ?
            patternOrNull.matcher( value ).find() :
            patternOrNull.matcher( value ).matches();
    }

    protected static > String niceEnumAlternatives( Class enumClass )
    {
        StringBuilder builder = new StringBuilder( "[" );
        int count = 0;
        for ( T enumConstant : enumClass.getEnumConstants() )
        {
            builder.append( count++ == 0 ? "" : ", " );
            builder.append( enumConstant.name() );
        }
        return builder.append( "]" ).toString();
    }

    protected static > T parseEnum(
        Class enumClass, String name, T defaultValue, Pair... additionalPairs )
    {
        if ( name == null )
        {
            return defaultValue;
        }

        name = name.toLowerCase();
        for ( T enumConstant : enumClass.getEnumConstants() )
        {
            if ( enumConstant.name().equalsIgnoreCase( name ) )
            {
                return enumConstant;
            }
        }
        for ( T enumConstant : enumClass.getEnumConstants() )
        {
            if ( enumConstant.name().toLowerCase().startsWith( name ) )
            {
                return enumConstant;
            }
        }

        for ( Pair additional : additionalPairs )
        {
            if ( additional.first().equalsIgnoreCase( name ) )
            {
                return additional.other();
            }
        }
        for ( Pair additional : additionalPairs )
        {
            if ( additional.first().toLowerCase().startsWith( name ) )
            {
                return additional.other();
            }
        }

        throw new IllegalArgumentException( "No '" + name + "' or '" +
            name + ".*' in " + enumClass );
    }

    protected static boolean filterMatches( Map filterMap, boolean caseInsensitiveFilters,
            boolean looseFilters, String key, Object value )
    {
        if ( filterMap == null || filterMap.isEmpty() )
        {
            return true;
        }
        for ( Map.Entry 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 ) )
                {
                    return true;
                }
            }
        }
        return false;
    }

    protected static String frame( String string, boolean frame )
    {
        return frame ? "[" + string + "]" : string;
    }

    protected static String format( Object value, boolean includeFraming )
    {
        String result;
        if ( value.getClass().isArray() )
        {
            StringBuilder buffer = new StringBuilder();
            int length = Array.getLength( value );
            for ( int i = 0; i < length; i++ )
            {
                Object singleValue = Array.get( value, i );
                if ( i > 0 )
                {
                    buffer.append( "," );
                }
                buffer.append( frame( singleValue.toString(),
                    includeFraming ) );
            }
            result = buffer.toString();
        }
        else
        {
            result = frame( value.toString(), includeFraming );
        }
        return result;
    }

    protected static void printAndInterpretTemplateLines( Collection templateLines,
            boolean forcePrintHitHeader, boolean newLineBetweenHits, NodeOrRelationship entity,
            GraphDatabaseShellServer server, Session session, Output out )
            throws ShellException, RemoteException
    {
        if ( templateLines.isEmpty() || forcePrintHitHeader )
        {
            out.println( getDisplayName( server, session, entity, true ) );
        }

        if ( !templateLines.isEmpty() )
        {
            Map data = new HashMap();
            data.put( "i", entity.getId() );
            for ( String command : templateLines )
            {
                String line = TextUtil.templateString( command, data );
                server.interpretLine( session.getId(), line, out );
            }
        }
        if ( newLineBetweenHits )
        {
            out.println();
        }
    }

    /**
     * Reads the session variable specified in {@link org.neo4j.shell.Variables#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 readCurrentWorkingDir( Session session ) throws RemoteException
    {
        List list = new ArrayList();
        String path = session.getPath();
        if ( path != null && path.trim().length() > 0 )
        {
            for ( String typedId : path.split( "," ) )
            {
                list.add( new TypedId( typedId ) );
            }
        }
        return list;
    }

    public static void writeCurrentWorkingDir( List paths, Session session ) throws RemoteException
    {
        String path = makePath( paths );
        session.setPath( path );
    }

    private static String makePath( List paths )
    {
        StringBuilder buffer = new StringBuilder();
        for ( TypedId typedId : paths )
        {
            if ( buffer.length() > 0 )
            {
                buffer.append( "," );
            }
            buffer.append( typedId.toString() );
        }
        return buffer.length() > 0 ? buffer.toString() : null;
    }

    protected static Map filterMapToTypes( GraphDatabaseService db,
            Direction defaultDirection, Map filterMap, boolean caseInsensitiveFilters,
            boolean looseFilters ) throws ShellException
    {
        Map matches = new TreeMap();
        for ( RelationshipType type : db.getAllRelationshipTypes() )
        {
            Direction direction = null;
            if ( filterMap == null || filterMap.isEmpty() )
            {
                direction = defaultDirection;
            }
            else
            {
                for ( Map.Entry entry : filterMap.entrySet() )
                {
                    if ( matches( newPattern( entry.getKey(), caseInsensitiveFilters ),
                        type.name(), caseInsensitiveFilters, looseFilters ) )
                    {
                        direction = getDirection( entry.getValue() != null ? entry.getValue().toString() : null, defaultDirection );
                        break;
                    }
                }
            }

            // It matches
            if ( direction != null )
            {
                matches.put( type.name(), direction );
            }
        }
        return matches.isEmpty() ? Collections.emptyMap() : matches;
    }

    protected static PathExpander toExpander( GraphDatabaseService db, Direction defaultDirection,
            Map relationshipTypes, boolean caseInsensitiveFilters, boolean looseFilters ) throws ShellException
    {
        defaultDirection = defaultDirection != null ? defaultDirection : Direction.BOTH;
        Map matches = filterMapToTypes( db, defaultDirection, relationshipTypes,
                caseInsensitiveFilters, looseFilters );
        if ( matches == null )
        {
            return PathExpanderBuilder.empty().build();
        }
        PathExpanderBuilder expander = PathExpanderBuilder.empty();
        for ( Map.Entry entry : matches.entrySet() )
        {
            expander = expander.add( RelationshipType.withName( entry.getKey() ),
                    entry.getValue() );
        }
        return expander.build();
    }

    protected static PathExpander toSortedExpander( GraphDatabaseService db, Direction defaultDirection,
            Map relationshipTypes, boolean caseInsensitiveFilters, boolean looseFilters ) throws ShellException
    {
        defaultDirection = defaultDirection != null ? defaultDirection : Direction.BOTH;
        Map matches = filterMapToTypes( db, defaultDirection, relationshipTypes,
                caseInsensitiveFilters, looseFilters );
        PathExpanderBuilder expander = PathExpanderBuilder.emptyOrderedByType();
        for ( Map.Entry entry : matches.entrySet() )
        {
            expander = expander.add( RelationshipType.withName( entry.getKey() ),
                    entry.getValue() );
        }
        return expander.build();
    }

    protected Label[] parseLabels( AppCommandParser parser )
    {
        return parseValues( parser, "l", EMPTY_LABELS, CREATE_LABELS );
    }

    protected RelationshipType[] parseRelTypes( AppCommandParser parser )
    {
        return parseValues( parser, "r", EMPTY_REL_TYPES, CREATE_REL_TYPES );
    }

    protected  T[] parseValues( AppCommandParser parser, String opt, T[] emptyValue, Function factory )
    {
        String typeValue = parser.option( opt, null );
        if ( typeValue == null )
        {
            return emptyValue;
        }
        typeValue = typeValue.trim();

        String[] stringItems;
        if ( typeValue.startsWith( "[" ) )
        {
            Object[] items = parseArray( typeValue );
            stringItems = new String[items.length];
            for ( int i = 0; i < items.length; i++ )
            {
                stringItems[i] = withOrWithoutColon( items[i].toString() );
            }
        }
        else
        {
            stringItems = new String[]{withOrWithoutColon( typeValue )};
        }
        return factory.apply( stringItems );
    }

    private String withOrWithoutColon( String item )
    {
        return item.startsWith( ":" ) ? item.substring( 1 ) : item;
    }
}




© 2015 - 2025 Weber Informatics LLC | Privacy Policy