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

org.neo4j.bolt.v1.runtime.internal.CypherAdapterStream Maven / Gradle / Ivy

Go to download

The core of Neo4j Bolt Protocol, this contains the state machine for Bolt sessions.

There is a newer version: 5.26.1
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.bolt.v1.runtime.internal;

import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

import org.neo4j.bolt.v1.messaging.BoltIOException;
import org.neo4j.bolt.v1.runtime.spi.Record;
import org.neo4j.bolt.v1.runtime.spi.RecordStream;
import org.neo4j.graphdb.ExecutionPlanDescription;
import org.neo4j.graphdb.InputPosition;
import org.neo4j.graphdb.Notification;
import org.neo4j.graphdb.QueryExecutionType;
import org.neo4j.graphdb.QueryStatistics;
import org.neo4j.graphdb.Result;
import org.neo4j.graphdb.spatial.Point;
import org.neo4j.kernel.api.exceptions.Status;

public class CypherAdapterStream implements RecordStream
{
    private final Result delegate;
    private final String[] fieldNames;
    private CypherAdapterRecord currentRecord;

    public CypherAdapterStream( Result delegate )
    {
        this.delegate = delegate;
        this.fieldNames = delegate.columns().toArray( new String[delegate.columns().size()] );
        this.currentRecord = new CypherAdapterRecord( fieldNames );
    }

    @Override
    public void close()
    {
        delegate.close();
    }

    @Override
    public String[] fieldNames()
    {
        return fieldNames;
    }

    @Override
    public void accept( final Visitor visitor ) throws Exception
    {
        delegate.accept( row -> {
            visitor.visit( currentRecord.reset( row ) );
            return true;
        } );

        QueryExecutionType qt = delegate.getQueryExecutionType();
        visitor.addMetadata( "type", queryTypeCode( qt.queryType() ) );

        if ( delegate.getQueryStatistics().containsUpdates() )
        {
            Object stats = queryStats( delegate.getQueryStatistics() );
            visitor.addMetadata( "stats", stats );
        }
        if ( qt.requestedExecutionPlanDescription() )
        {
            ExecutionPlanDescription rootPlanTreeNode = delegate.getExecutionPlanDescription();
            String metadataFieldName = rootPlanTreeNode.hasProfilerStatistics() ? "profile" : "plan";
            visitor.addMetadata( metadataFieldName, ExecutionPlanConverter.convert( rootPlanTreeNode ) );
        }

        Iterable notifications = delegate.getNotifications();
        if ( notifications.iterator().hasNext() )
        {
            visitor.addMetadata( "notifications", NotificationConverter.convert( notifications ) );
        }
    }

    private Map queryStats( QueryStatistics queryStatistics )
    {
        Map result = new HashMap<>();
        addIfNonZero( result, "nodes-created", queryStatistics.getNodesCreated() );
        addIfNonZero( result, "nodes-deleted", queryStatistics.getNodesDeleted() );
        addIfNonZero( result, "relationships-created", queryStatistics.getRelationshipsCreated() );
        addIfNonZero( result, "relationships-deleted", queryStatistics.getRelationshipsDeleted() );
        addIfNonZero( result, "properties-set", queryStatistics.getPropertiesSet() );
        addIfNonZero( result, "labels-added", queryStatistics.getLabelsAdded() );
        addIfNonZero( result, "labels-removed", queryStatistics.getLabelsRemoved() );
        addIfNonZero( result, "indexes-added", queryStatistics.getIndexesAdded() );
        addIfNonZero( result, "indexes-removed", queryStatistics.getIndexesRemoved() );
        addIfNonZero( result, "constraints-added", queryStatistics.getConstraintsAdded() );
        addIfNonZero( result, "constraints-removed", queryStatistics.getConstraintsRemoved() );
        return result;
    }

    private void addIfNonZero( Map map, String name, int count )
    {
        if ( count > 0 )
        {
            map.put( name, count );
        }
    }

    private String queryTypeCode( QueryExecutionType.QueryType queryType )
    {
        switch (queryType)
        {
            case READ_ONLY:
                return "r";

            case READ_WRITE:
                return "rw";

            case WRITE:
                return "w";

            case SCHEMA_WRITE:
                return "s";

            default:
                return queryType.name();
        }
    }

    private static class CypherAdapterRecord implements Record
    {
        private final Object[] fields; // This exists solely to avoid re-creating a new array for each record
        private final String[] fieldNames;

        private CypherAdapterRecord( String[] fieldNames )
        {
            this.fields = new Object[fieldNames.length];
            this.fieldNames = fieldNames;
        }

        @Override
        public Object[] fields()
        {
            return fields;
        }

        public CypherAdapterRecord reset( Result.ResultRow cypherRecord ) throws BoltIOException
        {
            for ( int i = 0; i < fields.length; i++ )
            {
                fields[i] = cypherRecord.get( fieldNames[i] );
                assertPackable( fields[i] );
            }
            return this;
        }

        private void assertPackable( Object field ) throws BoltIOException
        {
            //TODO this is a temporary measure, currently the packing of points
            //fails in Neo4jPack#pack but it fails there when already begun writing
            //headers to the result, so failing there and writing an error while in the process
            //of writing a record results in a malformed error message.
            //What needs to be done is inverting this so that the field has a visitable method
            //that calls the proper pack method.
            if (field instanceof Point)
            {
                throw new BoltIOException( Status.Request.Invalid, "Point is not yet supported as a return type in Bolt" );
            }
        }
    }

    private static class NotificationConverter
    {
        public static Object convert( Iterable notifications )
        {
            List out = new ArrayList<>();
            for ( Notification notification : notifications )
            {
                Map notificationMap = new HashMap<>( 4 );
                notificationMap.put( "code", notification.getCode() );
                notificationMap.put( "title", notification.getTitle() );
                notificationMap.put( "description", notification.getDescription() );
                notificationMap.put( "severity", notification.getSeverity().toString() );

                InputPosition pos = notification.getPosition(); // position is optional
                if( !pos.equals( InputPosition.empty ) )
                {
                    // only add the position if it is not empty
                    Map posMap = new HashMap<>( 3 );
                    posMap.put( "offset", pos.getOffset() );
                    posMap.put( "line", pos.getLine() );
                    posMap.put( "column", pos.getColumn() );
                    notificationMap.put( "position", posMap );
                }

                out.add( notificationMap );
            }
            return out;
        }
    }
}