Skip to content

Instantly share code, notes, and snippets.

@lutovich
Created February 4, 2016 08:30
Show Gist options
  • Save lutovich/3b677f2b1fbf5ee38109 to your computer and use it in GitHub Desktop.
Save lutovich/3b677f2b1fbf5ee38109 to your computer and use it in GitHub Desktop.
Neo4j Procedure able to create/execute/drop other procedures from specified JAR files
import java.lang.reflect.Field;
import java.lang.reflect.Method;
import java.net.MalformedURLException;
import java.net.URL;
import java.net.URLClassLoader;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.util.HashMap;
import java.util.Map;
import java.util.Objects;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
import java.util.stream.Stream;
import org.neo4j.graphdb.GraphDatabaseService;
import org.neo4j.helpers.Exceptions;
import org.neo4j.logging.Log;
import org.neo4j.procedure.Context;
import org.neo4j.procedure.Name;
import org.neo4j.procedure.PerformsWrites;
import org.neo4j.procedure.Procedure;
import static java.util.stream.Collectors.joining;
public class MetaProcedures
{
private static Map<String,ProcedureInvoker> procedures = new HashMap<>();
private static Lock lock = new ReentrantLock();
@Context
public GraphDatabaseService db;
@Context
public Log log;
@Procedure
public Stream<Output> createProcedure(
@Name( "jarPath" ) String jarPath,
@Name( "name" ) String name )
{
lock.lock();
try
{
log.info( "createProcedure(): jarPath: '" + jarPath + "' name: '" + name + "'" );
try
{
if ( procedures.containsKey( name ) )
{
log.info( "createProcedure(): Procedure " + name + " already exists" );
return Stream.of( new Output( "FAILURE: Procedure " + name + " already exists" ) );
}
String[] classAndMethod = name.split( "\\." );
Object procedure = new JarWorker( jarPath ).createProcedure( classAndMethod[0] );
ProcedureInvoker invoker = new ProcedureInvoker( procedure, classAndMethod[1] );
procedures.put( name, invoker );
return Stream.of( new Output( "SUCCESS: created" ) );
}
catch ( Exception e )
{
log.info( "createProcedure(): FAILURE", e );
return failureStream( e );
}
}
finally
{
lock.unlock();
}
}
@Procedure
@PerformsWrites
public Stream<Output> executeProcedure( @Name( "name" ) String name )
{
lock.lock();
try
{
log.info( "callProcedure(): name: '" + name + "'" );
ProcedureInvoker invoker = procedures.get( name );
if ( invoker == null )
{
log.info( "callProcedure(): Procedure " + name + " not defined" );
return Stream.of( new Output( "FAILURE: Procedure " + name + " not defined" ) );
}
try
{
return Stream.of( new Output( "SUCCESS: " + invoker.invoke() ) );
}
catch ( Exception e )
{
log.info( "callProcedure(): FAILURE", e );
return failureStream( e );
}
}
finally
{
lock.unlock();
}
}
@Procedure
public Stream<Output> dropProcedure( @Name( "name" ) String name )
{
lock.lock();
try
{
log.info( "dropProcedure(): name: '" + name + "'" );
ProcedureInvoker invoker = procedures.remove( name );
if ( invoker == null )
{
log.info( "dropProcedure(): Procedure " + name + " does not exist" );
return Stream.of( new Output( "SUCCESS: Procedure " + name + " does not exist" ) );
}
return Stream.of( new Output( "SUCCESS: Procedure " + name + " dropped" ) );
}
finally
{
lock.unlock();
}
}
private static Stream<Output> failureStream( Exception e )
{
return Stream.of( new Output( "FAILURE: " + Exceptions.stringify( e ) ) );
}
public static class Output
{
public String value;
public Output( String value )
{
this.value = value;
}
}
private class JarWorker
{
final ClassLoader classLoader;
JarWorker( String jarPath )
{
try
{
URL[] urls = {Paths.get( jarPath ).toAbsolutePath().toUri().toURL()};
this.classLoader = new URLClassLoader( urls, MetaProcedures.class.getClassLoader() );
}
catch ( MalformedURLException e )
{
throw new RuntimeException( e );
}
}
Object createProcedure( String name )
{
log.info( "Loading class: " + name );
try
{
Class<?> clazz = classLoader.loadClass( name );
Object procedure = clazz.newInstance();
setField( procedure, GraphDatabaseService.class, db );
setField( procedure, Log.class, log );
return procedure;
}
catch ( Exception e )
{
throw new RuntimeException( e );
}
}
void setField( Object procedure, Class<?> clazz, Object value )
{
Field[] fields = procedure.getClass().getFields();
for ( Field field : fields )
{
if ( field.isAnnotationPresent( Context.class ) && field.getType().equals( clazz ) )
{
try
{
field.set( procedure, value );
}
catch ( IllegalAccessException e )
{
throw new RuntimeException( e );
}
}
}
}
}
private static class ProcedureInvoker
{
final Object procedure;
final Method method;
ProcedureInvoker( Object procedure, String methodName ) throws Exception
{
this.procedure = procedure;
this.method = findMethod( procedure, methodName );
}
String invoke() throws Exception
{
Object invocationResult = method.invoke( procedure );
if ( method.getReturnType().equals( void.class ) )
{
return "VOID procedure invoked";
}
return ((Stream<?>) invocationResult).map( Objects::toString ).collect( joining( ", " ) );
}
Method findMethod( Object procedure, String name ) throws Exception
{
Method[] methods = procedure.getClass().getMethods();
for ( Method method : methods )
{
if ( method.getName().equals( name ) &&
method.isAnnotationPresent( Procedure.class ) &&
(method.getReturnType().equals( Stream.class ) || method.getReturnType().equals( void.class )) &&
method.getParameterCount() == 0 )
{
return method;
}
}
throw new RuntimeException( "No suitable method found" );
}
}
}
@lutovich
Copy link
Author

lutovich commented Feb 4, 2016

Example usage would be:

h/Downloads/test-procedure/target/test-procedure-1.0-SNAPSHOT.jar", "HelloWorld.say")
CALL executeProcedure("HelloWorld.say")
CALL dropProcedure("HelloWorld.say")
CALL executeProcedure("HelloWorld.say")

CALL createProcedure("/Users/lutovich/Downloads/test-procedure/target/test-procedure-1.0-SNAPSHOT.jar", "Person.create")
CALL createProcedure("/Users/lutovich/Downloads/test-procedure/target/test-procedure-1.0-SNAPSHOT.jar", "Person.list")
CALL createProcedure("/Users/lutovich/Downloads/test-procedure/target/test-procedure-1.0-SNAPSHOT.jar", "Person.removeAll")
CALL executeProcedure("Person.list")
CALL executeProcedure("Person.removeAll")
CALL executeProcedure("Person.list")
CALL executeProcedure("Person.create")
CALL executeProcedure("Person.list")
CALL dropProcedure("Person.removeAll")
CALL executeProcedure("Person.removeAll")
CALL executeProcedure("Person.create")
CALL executeProcedure("Person.list")

Person procedure: https://gist.github.com/lutovich/27e4b8842982b16b8b07

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment