@Override public void start() throws Throwable { ProcedureJarLoader loader = new ProcedureJarLoader( compiler, log ); ProcedureJarLoader.Callables callables = loader.loadProceduresFromDir( pluginDir ); for ( CallableProcedure procedure : callables.procedures() ) { register( procedure ); } for ( CallableUserFunction function : callables.functions() ) { register( function ); } for ( CallableUserAggregationFunction function : callables.aggregationFunctions() ) { register( function ); } // And register built-in procedures builtin.accept( this ); } }
public Callables loadProceduresFromDir( File root ) throws IOException, KernelException { if ( root == null || !root.exists() ) { return Callables.empty(); } Callables out = new Callables(); File[] dirListing = root.listFiles( ( dir, name ) -> name.endsWith( ".jar" ) ); if ( dirListing == null ) { return Callables.empty(); } if ( !allJarFilesAreValidZipFiles( Stream.of( dirListing ) ) ) { throw new ZipException( "Some jar procedure files are invalid, see log for details." ); } URL[] jarFilesURLs = Stream.of( dirListing ).map( this::toURL ).toArray( URL[]::new ); URLClassLoader loader = new URLClassLoader( jarFilesURLs, this.getClass().getClassLoader() ); for ( URL jarFile : jarFilesURLs ) { loadProcedures( jarFile, loader, out ); } return out; }
private Callables loadProcedures( URL jar, ClassLoader loader, Callables target ) throws IOException, KernelException { RawIterator<Class<?>,IOException> classes = listClassesIn( jar, loader ); while ( classes.hasNext() ) { Class<?> next = classes.next(); target.addAllProcedures( compiler.compileProcedure( next, null, false ) ); target.addAllFunctions( compiler.compileFunction( next ) ); target.addAllAggregationFunctions( compiler.compileAggregationFunction( next ) ); } return target; }
@Test public void shouldLoadProcedureFromJar() throws Throwable { // Given URL jar = createJarFor( ClassWithOneProcedure.class ); // When List<CallableProcedure> procedures = jarloader.loadProceduresFromDir( parentDir( jar ) ).procedures(); // Then List<ProcedureSignature> signatures = procedures.stream().map( CallableProcedure::signature ).collect( toList() ); assertThat( signatures, contains( procedureSignature( "org","neo4j", "kernel", "impl", "proc", "myProcedure" ).out( "someNumber", NTInteger ).build() )); assertThat( asList( procedures.get( 0 ).apply( new BasicContext(), new Object[0], resourceTracker ) ), contains( IsEqual.equalTo( new Object[]{1337L} )) ); }
@Test public void shouldGiveHelpfulErrorOnRawStreamProcedure() throws Throwable { // Given URL jar = createJarFor( ClassWithRawStream.class ); // Expect exception.expect( ProcedureException.class ); exception.expectMessage( String.format("Procedures must return a Stream of records, where a record is a concrete class%n" + "that you define and not a raw Stream." )); // When jarloader.loadProceduresFromDir( parentDir( jar ) ); }
@Test public void shouldReturnEmptySetOnNullArgument() throws Exception { // given ProcedureJarLoader jarloader = new ProcedureJarLoader( new ReflectiveProcedureCompiler( new TypeMappers(), new ComponentRegistry(), registryWithUnsafeAPI(), log, procedureConfig() ), NullLog.getInstance() ); // when ProcedureJarLoader.Callables callables = jarloader.loadProceduresFromDir( null ); // then assertEquals( 0, callables.procedures().size() + callables.functions().size() ); }
@Test public void shouldGiveHelpfulErrorOnWildCardProcedure() throws Throwable { // Given URL jar = createJarFor( ClassWithWildCardStream.class ); // Expect exception.expect( ProcedureException.class ); exception.expectMessage( String.format("Procedures must return a Stream of records, where a record is a concrete class%n" + "that you define and not a Stream<?>." )); // When jarloader.loadProceduresFromDir( parentDir( jar ) ); }
public Callables loadProceduresFromDir( File root ) throws IOException, KernelException { if ( root == null || !root.exists() ) { return Callables.empty(); } Callables out = new Callables(); File[] dirListing = root.listFiles( ( dir, name ) -> name.endsWith( ".jar" ) ); if ( dirListing == null ) { return Callables.empty(); } if ( !allJarFilesAreValidZipFiles( Stream.of( dirListing ) ) ) { throw new ZipException( "Some jar procedure files are invalid, see log for details." ); } URL[] jarFilesURLs = Stream.of( dirListing ).map( this::toURL ).toArray( URL[]::new ); URLClassLoader loader = new URLClassLoader( jarFilesURLs, this.getClass().getClassLoader() ); for ( URL jarFile : jarFilesURLs ) { loadProcedures( jarFile, loader, out ); } return out; }
private Callables loadProcedures( URL jar, ClassLoader loader, Callables target ) throws IOException, KernelException { RawIterator<Class<?>,IOException> classes = listClassesIn( jar, loader ); while ( classes.hasNext() ) { Class<?> next = classes.next(); target.addAllProcedures( compiler.compileProcedure( next, null, false ) ); target.addAllFunctions( compiler.compileFunction( next ) ); target.addAllAggregationFunctions( compiler.compileAggregationFunction( next ) ); } return target; }
@Test public void shouldLogHelpfullyWhenPluginJarIsCorrupt() throws Exception { // given URL theJar = createJarFor( ClassWithOneProcedure.class, ClassWithAnotherProcedure.class, ClassWithNoProcedureAtAll.class ); corruptJar( theJar ); AssertableLogProvider logProvider = new AssertableLogProvider( true ); ProcedureJarLoader jarloader = new ProcedureJarLoader( new ReflectiveProcedureCompiler( new TypeMappers(), new ComponentRegistry(), registryWithUnsafeAPI(), log, procedureConfig() ), logProvider.getLog( ProcedureJarLoader.class ) ); // when try { jarloader.loadProceduresFromDir( parentDir( theJar ) ); fail( "Should have logged and thrown exception." ); } catch ( ZipException expected ) { // then logProvider.assertContainsLogCallContaining( escapeJava( String.format( "Plugin jar file: %s corrupted.", new File( theJar.toURI() ).toPath() ) ) ); } }
@Test public void shouldLoadProcedureFromJarWithSpacesInFilename() throws Throwable { // Given URL jar = new JarBuilder().createJarFor( tmpdir.newFile( new Random().nextInt() + " some spaces in filename.jar" ), ClassWithOneProcedure.class); // When List<CallableProcedure> procedures = jarloader.loadProceduresFromDir( parentDir( jar ) ).procedures(); // Then List<ProcedureSignature> signatures = procedures.stream().map( CallableProcedure::signature ).collect( toList() ); assertThat( signatures, contains( procedureSignature( "org", "neo4j", "kernel", "impl", "proc", "myProcedure" ).out( "someNumber", NTInteger ).build() ) ); assertThat( asList( procedures.get( 0 ).apply( new BasicContext(), new Object[0], resourceTracker ) ), contains( IsEqual.equalTo( new Object[]{1337L} ) ) ); }
@Test public void shouldWorkOnPathsWithSpaces() throws Exception { // given File fileWithSpacesInName = tmpdir.newFile( new Random().nextInt() + " some spaces in the filename" + ".jar" ); URL theJar = new JarBuilder().createJarFor( fileWithSpacesInName, ClassWithOneProcedure.class ); corruptJar( theJar ); AssertableLogProvider logProvider = new AssertableLogProvider( true ); ProcedureJarLoader jarloader = new ProcedureJarLoader( new ReflectiveProcedureCompiler( new TypeMappers(), new ComponentRegistry(), registryWithUnsafeAPI(), log, procedureConfig() ), logProvider.getLog( ProcedureJarLoader.class ) ); // when try { jarloader.loadProceduresFromDir( parentDir( theJar ) ); fail( "Should have logged and thrown exception." ); } catch ( ZipException expected ) { // then logProvider.assertContainsLogCallContaining( escapeJava( String.format( "Plugin jar file: %s corrupted.", fileWithSpacesInName.toPath() ) ) ); } }
@Test public void shouldGiveHelpfulErrorOnGenericStreamProcedure() throws Throwable { // Given URL jar = createJarFor( ClassWithGenericStream.class ); // Expect exception.expect( ProcedureException.class ); exception.expectMessage( String.format("Procedures must return a Stream of records, where a record is a concrete class%n" + "that you define and not a parameterized type such as java.util.List<org.neo4j" + ".kernel.impl.proc.ProcedureJarLoaderTest$Output>.")); // When jarloader.loadProceduresFromDir( parentDir( jar ) ); }
@Override public void start() throws Throwable { ProcedureJarLoader loader = new ProcedureJarLoader( compiler, log ); ProcedureJarLoader.Callables callables = loader.loadProceduresFromDir( pluginDir ); for ( CallableProcedure procedure : callables.procedures() ) { register( procedure ); } for ( CallableUserFunction function : callables.functions() ) { register( function ); } for ( CallableUserAggregationFunction function : callables.aggregationFunctions() ) { register( function ); } // And register built-in procedures builtin.accept( this ); } }
@Test public void shouldLoadProcedureFromJarWithMultipleProcedureClasses() throws Throwable { // Given URL jar = createJarFor( ClassWithOneProcedure.class, ClassWithAnotherProcedure.class, ClassWithNoProcedureAtAll.class ); // When List<CallableProcedure> procedures = jarloader.loadProceduresFromDir( parentDir( jar ) ).procedures(); // Then List<ProcedureSignature> signatures = procedures.stream().map( CallableProcedure::signature ).collect( toList() ); assertThat( signatures, containsInAnyOrder( procedureSignature( "org","neo4j", "kernel", "impl", "proc", "myOtherProcedure" ).out( "someNumber", NTInteger ).build(), procedureSignature( "org","neo4j", "kernel", "impl", "proc", "myProcedure" ).out( "someNumber", NTInteger ).build() )); }
@Test public void shouldLoadProcedureWithArgumentFromJar() throws Throwable { // Given URL jar = createJarFor( ClassWithProcedureWithArgument.class ); // When List<CallableProcedure> procedures = jarloader.loadProceduresFromDir( parentDir( jar ) ).procedures(); // Then List<ProcedureSignature> signatures = procedures.stream().map( CallableProcedure::signature ).collect( toList() ); assertThat( signatures, contains( procedureSignature( "org","neo4j", "kernel", "impl", "proc", "myProcedure" ) .in( "value", NTInteger ) .out( "someNumber", NTInteger ) .build() )); assertThat( asList(procedures.get( 0 ).apply( new BasicContext(), new Object[]{42L}, resourceTracker ) ), contains( IsEqual.equalTo( new Object[]{42L} )) ); }
@Test public void shouldLoadProceduresFromDirectory() throws Throwable { // Given createJarFor( ClassWithOneProcedure.class ); createJarFor( ClassWithAnotherProcedure.class ); // When List<CallableProcedure> procedures = jarloader.loadProceduresFromDir( tmpdir.getRoot() ).procedures(); // Then List<ProcedureSignature> signatures = procedures.stream().map( CallableProcedure::signature ).collect( toList() ); assertThat( signatures, containsInAnyOrder( procedureSignature( "org","neo4j", "kernel", "impl", "proc", "myOtherProcedure" ).out( "someNumber", NTInteger ).build(), procedureSignature( "org","neo4j", "kernel", "impl", "proc", "myProcedure" ).out( "someNumber", NTInteger ).build() )); }
@Test public void shouldGiveHelpfulErrorOnInvalidProcedure() throws Throwable { // Given URL jar = createJarFor( ClassWithOneProcedure.class, ClassWithInvalidProcedure.class ); // Expect exception.expect( ProcedureException.class ); exception.expectMessage( String.format("Procedures must return a Stream of records, where a record is a concrete class%n" + "that you define, with public non-final fields defining the fields in the record.%n" + "If you''d like your procedure to return `boolean`, you could define a record class " + "like:%n" + "public class Output '{'%n" + " public boolean out;%n" + "'}'%n" + "%n" + "And then define your procedure as returning `Stream<Output>`." )); // When jarloader.loadProceduresFromDir( parentDir( jar ) ); }