34.2. Unmanaged Extensions

Sometimes you’ll want finer grained control over your application’s interactions with Neo4j than cypher provides. For these situations you can use the unmanaged extension API.

[Caution]Caution

This is a sharp tool, allowing users to deploy arbitrary JAX-RS classes to the server so be careful when using this. In particular it’s easy to consume lots of heap space on the server and degrade performance. If in doubt please ask for help via one of the community channels (see Chapter 32, Community Support).

Introduction to unmanaged extensions

The first step when writing an unmanaged extension is to create a project which includes dependencies to the JAX-RS and Neo4j core jars. In Maven this would be achieved by adding the following lines to the pom file:

<dependency>
    <groupId>javax.ws.rs</groupId>
    <artifactId>javax.ws.rs-api</artifactId>
    <version>2.0</version>
    <scope>provided</scope>
</dependency>
<dependency>
    <groupId>org.neo4j</groupId>
    <artifactId>neo4j</artifactId>
    <version>2.2.0-M02</version>
    <scope>provided</scope>
</dependency>

Now we’re ready to write our extension.

In our code we’ll interact with the database using GraphDatabaseService which we can get access to by using the @Context annotation. The following examples serves as a template which you can base your extension on:

Unmanaged extension example 

@Path( "/helloworld" )
public class HelloWorldResource
{
    private final GraphDatabaseService database;

    public HelloWorldResource( @Context GraphDatabaseService database )
    {
        this.database = database;
    }

    @GET
    @Produces( MediaType.TEXT_PLAIN )
    @Path( "/{nodeId}" )
    public Response hello( @PathParam( "nodeId" ) long nodeId )
    {
        // Do stuff with the database
        return Response.status( Status.OK ).entity(
                ("Hello World, nodeId=" + nodeId).getBytes( Charset.forName("UTF-8") ) ).build();
    }
}

The full source code is found here: HelloWorldResource.java

Having built your code, the resulting jar file (and any custom dependencies) should be placed in the $NEO4J_SERVER_HOME/plugins directory. We also need to tell Neo4j where to look for the extension by adding some configuration in the conf/neo4j-server.properties file:

#Comma separated list of JAXRS packages containing JAXRS Resource, one package name for each mountpoint.
org.neo4j.server.thirdparty_jaxrs_classes=org.neo4j.examples.server.unmanaged=/examples/unmanaged

Our hello method will now respond to GET requests at the URI: http://{neo4j_server}:{neo4j_port}/examples/unmanaged/helloworld/{nodeId}. e.g.

curl http://localhost:7474/examples/unmanaged/helloworld/123

which results in

Hello World, nodeId=123

Streaming JSON responses

When writing unmanaged extensions we have greater control over the amount of memory that our Neo4j queries use. If we keep too much state around it can lead to more frequent full Garbage Collection and subsequent unresponsiveness by the Neo4j server.

A common way that state can creep in is the creation of JSON objects to represent the result of a query which we then send back to our application. Neo4j’s Transactional Cypher HTTP endpoint (see Section 21.1, “Transactional Cypher HTTP endpoint”) streams responses back to the client and we should follow in its footsteps.

For example, the following unmanaged extension streams an array of a person’s colleagues:

Unmanaged extension streaming example 

@Path("/colleagues")
public class ColleaguesResource
{
    private GraphDatabaseService graphDb;
    private final ObjectMapper objectMapper;

    private static final DynamicRelationshipType ACTED_IN = DynamicRelationshipType.withName( "ACTED_IN" );
    private static final Label PERSON = DynamicLabel.label( "Person" );

    public ColleaguesResource( @Context GraphDatabaseService graphDb )
    {
        this.graphDb = graphDb;
        this.objectMapper = new ObjectMapper();
    }

    @GET
    @Path("/{personName}")
    public Response findColleagues( final @PathParam("personName") String personName )
    {
        StreamingOutput stream = new StreamingOutput()
        {
            @Override
            public void write( OutputStream os ) throws IOException, WebApplicationException
            {
                JsonGenerator jg = objectMapper.getJsonFactory().createJsonGenerator( os, JsonEncoding.UTF8 );
                jg.writeStartObject();
                jg.writeFieldName( "colleagues" );
                jg.writeStartArray();

                try ( Transaction tx = graphDb.beginTx();
                      ResourceIterator<Node> persons = graphDb.findNodes( PERSON, "name", personName ) )
                {
                    while ( persons.hasNext() )
                    {
                        Node person = persons.next();
                        for ( Relationship actedIn : person.getRelationships( ACTED_IN, OUTGOING ) )
                        {
                            Node endNode = actedIn.getEndNode();
                            for ( Relationship colleagueActedIn : endNode.getRelationships( ACTED_IN, INCOMING ) )
                            {
                                Node colleague = colleagueActedIn.getStartNode();
                                if ( !colleague.equals( person ) )
                                {
                                    jg.writeString( colleague.getProperty( "name" ).toString() );
                                }
                            }
                        }
                    }
                    tx.success();
                }

                jg.writeEndArray();
                jg.writeEndObject();
                jg.flush();
                jg.close();
            }
        };

        return Response.ok().entity( stream ).type( MediaType.APPLICATION_JSON ).build();
    }
}

The full source code is found here: ColleaguesResource.java

As well as depending on JAX-RS API this example also uses Jackson — a Java JSON library. You’ll need to add the following dependency to your Maven POM file (or equivalent):

<dependency>
    <groupId>org.codehaus.jackson</groupId>
    <artifactId>jackson-mapper-asl</artifactId>
    <version>1.9.7</version>
</dependency>

Our findColleagues method will now respond to GET requests at the URI: http://{neo4j_server}:{neo4j_port}/examples/unmanaged/colleagues/{personName}. For example:

curl http://localhost:7474/examples/unmanaged/colleagues/Keanu%20Reeves

which results in

{"colleagues":["Hugo Weaving","Carrie-Anne Moss","Laurence Fishburne"]}

Using Cypher in an unmanaged extension

You can execute Cypher queries by using the GraphDatabaseService that is injected into the extension.

[Note]Note

In Neo4j versions prior to 2.2 you had to retrieve an ExecutionEngine to execute queries. This has been deprecated, and we recommend you to update any existing code to use GraphDatabaseService instead.

For example, the following unmanaged extension retrieves a person’s colleagues using Cypher:

Unmanaged extension Cypher execution example 

@Path("/colleagues-cypher-execution")
public class ColleaguesCypherExecutionResource
{
    private final ObjectMapper objectMapper;
    private GraphDatabaseService graphDb;

    public ColleaguesCypherExecutionResource( @Context GraphDatabaseService graphDb )
    {
        this.graphDb = graphDb;
        this.objectMapper = new ObjectMapper();
    }

    @GET
    @Path("/{personName}")
    public Response findColleagues( final @PathParam("personName") String personName )
    {
        final Map<String, Object> params = MapUtil.map( "personName", personName );

        StreamingOutput stream = new StreamingOutput()
        {
            @Override
            public void write( OutputStream os ) throws IOException, WebApplicationException
            {
                JsonGenerator jg = objectMapper.getJsonFactory().createJsonGenerator( os, JsonEncoding.UTF8 );
                jg.writeStartObject();
                jg.writeFieldName( "colleagues" );
                jg.writeStartArray();

                try ( Transaction tx = graphDb.beginTx();
                      Result result = graphDb.execute( colleaguesQuery(), params ) )
                {
                    while ( result.hasNext() )
                    {
                        Map<String,Object> row = result.next();
                        jg.writeString( ((Node) row.get( "colleague" )).getProperty( "name" ).toString() );
                    }
                    tx.success();
                }

                jg.writeEndArray();
                jg.writeEndObject();
                jg.flush();
                jg.close();
            }
        };

        return Response.ok().entity( stream ).type( MediaType.APPLICATION_JSON ).build();
    }

    private String colleaguesQuery()
    {
        return "MATCH (p:Person {name: {personName} })-[:ACTED_IN]->()<-[:ACTED_IN]-(colleague) RETURN colleague";
    }
}

The full source code is found here: ColleaguesCypherExecutionResource.java

Our findColleagues method will now respond to GET requests at the URI: http://{neo4j_server}:{neo4j_port}/examples/unmanaged/colleagues-cypher-execution/{personName}. e.g.

curl http://localhost:7474/examples/unmanaged/colleagues-cypher-execution/Keanu%20Reeves

which results in

{"colleagues":["Hugo Weaving","Carrie-Anne Moss","Laurence Fishburne"]}