diff --git a/jgrab-runner/src/main/java/com/athaydes/jgrab/Classpath.java b/jgrab-runner/src/main/java/com/athaydes/jgrab/Classpath.java new file mode 100644 index 0000000..5a2a527 --- /dev/null +++ b/jgrab-runner/src/main/java/com/athaydes/jgrab/Classpath.java @@ -0,0 +1,60 @@ +package com.athaydes.jgrab; + +import java.io.File; +import java.util.List; +import java.util.SortedSet; +import java.util.TreeSet; + +import static java.util.Collections.unmodifiableList; +import static java.util.Collections.unmodifiableSortedSet; + +public final class Classpath { + private static final Classpath EMPTY = new Classpath( new TreeSet<>(), List.of() ); + + public final SortedSet dependencies; + public final List resolvedArtifacts; + public final String hash; + + public Classpath( SortedSet dependencies, List resolvedArtifacts ) { + this( dependencies, resolvedArtifacts, Dependency.hashOf( dependencies ) ); + } + + public Classpath( SortedSet dependencies, + List resolvedArtifacts, + String hash ) { + this.dependencies = unmodifiableSortedSet( dependencies ); + this.resolvedArtifacts = unmodifiableList( resolvedArtifacts ); + this.hash = hash; + } + + public static Classpath empty() { + return EMPTY; + } + + public boolean isEmpty() { + return resolvedArtifacts.isEmpty(); + } + + @Override + public boolean equals( Object o ) { + if ( this == o ) return true; + if ( o == null || getClass() != o.getClass() ) return false; + + Classpath classpath = ( Classpath ) o; + + return hash.equals( classpath.hash ); + } + + @Override + public int hashCode() { + return hash.hashCode(); + } + + @Override + public String toString() { + return "Classpath{" + + "dependencies=" + dependencies + + ", resolvedArtifacts=" + resolvedArtifacts + + '}'; + } +} diff --git a/jgrab-runner/src/main/java/com/athaydes/jgrab/Dependency.java b/jgrab-runner/src/main/java/com/athaydes/jgrab/Dependency.java index 9838f6e..52f1c09 100644 --- a/jgrab-runner/src/main/java/com/athaydes/jgrab/Dependency.java +++ b/jgrab-runner/src/main/java/com/athaydes/jgrab/Dependency.java @@ -42,7 +42,7 @@ public static Dependency of( String declaration ) { return new Dependency( parts[ 0 ], parts[ 1 ], parts.length == 2 ? "latest" : parts[ 2 ] ); } - public static String hashOf( Collection dependencies ) { + public static String hashOf( SortedSet dependencies ) { var list = new ArrayList<>( dependencies ); list.sort( COMPARATOR ); @@ -60,7 +60,7 @@ public static String hashOf( Collection dependencies ) { return Base64.getUrlEncoder().encodeToString( digest.digest() ); } - public static Set parseDependencies( Stream codeLines ) { + public static SortedSet parseDependencies( Stream codeLines ) { return codeLines.flatMap( line -> { Matcher matcher = JGRAB_PATTERN.matcher( line ); if ( matcher.matches() ) { @@ -68,7 +68,7 @@ public static Set parseDependencies( Stream codeLines ) { } else { return Stream.empty(); } - } ).collect( Collectors.toSet() ); + } ).collect( Collectors.toCollection( TreeSet::new ) ); } @Override diff --git a/jgrab-runner/src/main/java/com/athaydes/jgrab/code/JavaCode.java b/jgrab-runner/src/main/java/com/athaydes/jgrab/code/JavaCode.java index c210319..e38e47c 100644 --- a/jgrab-runner/src/main/java/com/athaydes/jgrab/code/JavaCode.java +++ b/jgrab-runner/src/main/java/com/athaydes/jgrab/code/JavaCode.java @@ -2,7 +2,7 @@ import com.athaydes.jgrab.Dependency; -import java.util.Set; +import java.util.SortedSet; import java.util.regex.Matcher; import java.util.regex.Pattern; @@ -18,7 +18,7 @@ public interface JavaCode { Pattern CLASS_PATTERN = Pattern.compile( "\\s*((public|static|private|abstract|final)\\s+)*\\s*class\\s+(?[a-zA-Z_0-9.$]+).*" ); - Set extractDependencies(); + SortedSet extractDependencies(); String getClassName(); diff --git a/jgrab-runner/src/main/java/com/athaydes/jgrab/code/StdinJavaCode.java b/jgrab-runner/src/main/java/com/athaydes/jgrab/code/StdinJavaCode.java index 18ea154..817d028 100644 --- a/jgrab-runner/src/main/java/com/athaydes/jgrab/code/StdinJavaCode.java +++ b/jgrab-runner/src/main/java/com/athaydes/jgrab/code/StdinJavaCode.java @@ -9,7 +9,7 @@ import java.util.ArrayList; import java.util.List; import java.util.Scanner; -import java.util.Set; +import java.util.SortedSet; import java.util.stream.Stream; /** @@ -49,7 +49,7 @@ public boolean isSnippet() { } @Override - public Set extractDependencies() { + public SortedSet extractDependencies() { return Dependency.parseDependencies( Stream.of( lines ) ); } diff --git a/jgrab-runner/src/main/java/com/athaydes/jgrab/code/StringJavaCode.java b/jgrab-runner/src/main/java/com/athaydes/jgrab/code/StringJavaCode.java index a5794ac..1c86714 100644 --- a/jgrab-runner/src/main/java/com/athaydes/jgrab/code/StringJavaCode.java +++ b/jgrab-runner/src/main/java/com/athaydes/jgrab/code/StringJavaCode.java @@ -2,7 +2,7 @@ import com.athaydes.jgrab.Dependency; -import java.util.Set; +import java.util.SortedSet; import java.util.stream.Stream; /** @@ -21,7 +21,7 @@ public StringJavaCode( String code ) { } @Override - public Set extractDependencies() { + public SortedSet extractDependencies() { return Dependency.parseDependencies( Stream.of( codeLines ) ); } diff --git a/jgrab-runner/src/main/java/com/athaydes/jgrab/daemon/JGrabDaemon.java b/jgrab-runner/src/main/java/com/athaydes/jgrab/daemon/JGrabDaemon.java index 423599c..fe7bef4 100644 --- a/jgrab-runner/src/main/java/com/athaydes/jgrab/daemon/JGrabDaemon.java +++ b/jgrab-runner/src/main/java/com/athaydes/jgrab/daemon/JGrabDaemon.java @@ -1,6 +1,6 @@ package com.athaydes.jgrab.daemon; -import com.athaydes.jgrab.Dependency; +import com.athaydes.jgrab.Classpath; import com.athaydes.jgrab.code.JavaCode; import com.athaydes.jgrab.code.StringJavaCode; import com.athaydes.jgrab.ivy.IvyGrabber; @@ -9,11 +9,12 @@ import org.slf4j.Logger; import org.slf4j.LoggerFactory; -import java.io.*; +import java.io.BufferedReader; +import java.io.IOException; +import java.io.InputStreamReader; +import java.io.PrintStream; import java.net.ServerSocket; import java.net.Socket; -import java.util.List; -import java.util.Set; import java.util.regex.Pattern; /** @@ -51,6 +52,8 @@ public static void start( RunArgs runArgs ) { return; } + JGrabRunner.populateClassLoaderCache( libsCache.loadCache().values() ); + while ( true ) { try ( Socket clientSocket = serverSocket.accept(); final PrintStream out = new PrintStream( clientSocket.getOutputStream(), true ); @@ -117,12 +120,11 @@ public static void start( RunArgs runArgs ) { logSourceCode( code ); - Set deps = code.extractDependencies(); + var deps = code.extractDependencies(); logger.debug( "Dependencies to grab: {}", deps ); - List libs = libsCache.libsFor( deps, - () -> grabber.grab( deps ) ); + var classpath = libsCache.classpathOf( deps, () -> grabber.grab( deps ) ); System.setIn( clientSocket.getInputStream() ); System.setOut( out ); @@ -130,7 +132,7 @@ public static void start( RunArgs runArgs ) { // run this synchronously, which means only one program can run per daemon at a time try { - runArgs.accept( code, args, libs ); + runArgs.accept( code, args, classpath ); } catch ( Throwable t ) { t.printStackTrace( out ); } @@ -154,7 +156,7 @@ private static void logSourceCode( Object source ) { public interface RunArgs { void accept( JavaCode javaCode, String[] args, - List libs ); + Classpath classpath ); } } diff --git a/jgrab-runner/src/main/java/com/athaydes/jgrab/daemon/PersistentCache.java b/jgrab-runner/src/main/java/com/athaydes/jgrab/daemon/PersistentCache.java index adf27fd..1a328e7 100644 --- a/jgrab-runner/src/main/java/com/athaydes/jgrab/daemon/PersistentCache.java +++ b/jgrab-runner/src/main/java/com/athaydes/jgrab/daemon/PersistentCache.java @@ -1,5 +1,6 @@ package com.athaydes.jgrab.daemon; +import com.athaydes.jgrab.Classpath; import com.athaydes.jgrab.Dependency; import com.athaydes.jgrab.JGrabHome; import org.slf4j.Logger; @@ -20,13 +21,13 @@ /** * A persistent cache for resolved dependencies */ -class PersistentCache { +final class PersistentCache { private static final Logger logger = LoggerFactory.getLogger( PersistentCache.class ); private final File cacheFile; private final AtomicBoolean isCacheLoaded = new AtomicBoolean( false ); - private final Map, List> cache = new HashMap<>(); + private final Map cache = new HashMap<>(); PersistentCache() { this( new File( JGrabHome.getDir(), "deps-cache" ) ); @@ -62,13 +63,11 @@ void save() throws IOException { tempCacheFile.createNewFile(); try ( BufferedWriter writer = Files.newBufferedWriter( tempCacheFile.toPath(), StandardOpenOption.WRITE ) ) { - for (Map.Entry, List> entry : cache.entrySet()) { - var dependencies = new ArrayList<>( entry.getKey() ); - dependencies.sort( Dependency.COMPARATOR ); - String deps = dependencies.stream() + for ( var classpath : cache.values() ) { + String deps = classpath.dependencies.stream() .map( Dependency::canonicalNotation ) .collect( Collectors.joining( "," ) ); - String libs = entry.getValue().stream() + String libs = classpath.resolvedArtifacts.stream() .map( File::getAbsolutePath ) .collect( Collectors.joining( File.pathSeparator ) ); @@ -90,22 +89,25 @@ void save() throws IOException { } } - List libsFor( Set dependencies, - Supplier> compute ) { + Classpath classpathOf( SortedSet dependencies, + Supplier> compute ) { if ( dependencies.isEmpty() ) { - return Collections.emptyList(); + return Classpath.empty(); } if ( !isCacheLoaded.getAndSet( true ) ) { cache.putAll( loadCache() ); } - return cache.computeIfAbsent( dependencies, ( ignore ) -> compute.get() ); + return cache.computeIfAbsent( Dependency.hashOf( dependencies ), ( hash ) -> + new Classpath( dependencies, compute.get(), hash ) ); } - Map, List> loadCache() { - logger.debug( "Initializing dependencies cache" ); - return cacheFrom( loadCacheEntries() ); + Map loadCache() { + logger.debug( "Loading dependencies cache" ); + var result = cacheFrom( loadCacheEntries() ); + isCacheLoaded.set( true ); + return result; } private List loadCacheEntries() { @@ -126,16 +128,16 @@ private List loadCacheEntries() { return cacheEntries; } - private Map, List> cacheFrom( List cacheEntries ) { - Map, List> cache = new HashMap<>(); + private Map cacheFrom( List cacheEntries ) { + Map cache = new HashMap<>(); - for (String entry : cacheEntries) { + for ( String entry : cacheEntries ) { String[] parts = entry.split( " " ); if ( parts.length == 2 ) { try { - Set deps = Stream.of( parts[ 0 ].split( "," ) ) + var deps = Stream.of( parts[ 0 ].split( "," ) ) .map( Dependency::of ) - .collect( Collectors.toSet() ); + .collect( Collectors.toCollection( TreeSet::new ) ); List libs = Stream.of( parts[ 1 ].split( File.pathSeparator ) ) .map( File::new ) @@ -143,7 +145,7 @@ private Map, List> cacheFrom( List cacheEntries ) if ( libs.stream().allMatch( File::isFile ) ) { logger.debug( "Loading dependency entry from cache: {} -> {}", deps, libs ); - cache.put( deps, libs ); + cache.put( Dependency.hashOf( deps ), new Classpath( deps, libs ) ); } else { logger.info( "Ignoring cache entry because not all lib files exist" ); } diff --git a/jgrab-runner/src/main/java/com/athaydes/jgrab/jbuild/JBuildGrabber.java b/jgrab-runner/src/main/java/com/athaydes/jgrab/jbuild/JBuildGrabber.java index 719019d..5f2aa89 100644 --- a/jgrab-runner/src/main/java/com/athaydes/jgrab/jbuild/JBuildGrabber.java +++ b/jgrab-runner/src/main/java/com/athaydes/jgrab/jbuild/JBuildGrabber.java @@ -47,7 +47,7 @@ public List grab( Collection toGrab ) { NonEmptyCollection.of( List.of( new FileArtifactRetriever( MavenUtils.mavenHome() ), new HttpArtifactRetriever( log, MavenUtils.MAVEN_CENTRAL_URL ) ) ) ); - var outputDir = outputDirHashOf( toGrab ); + var outputDir = outputDirHashOf( new TreeSet<>( toGrab ) ); if ( outputDir.isDirectory() ) { // the dir already exists, reuse that! @@ -78,7 +78,7 @@ private static Void throwErrors( NonEmptyCollection errors ) { throw new JGrabError( "Errors occurred while grabbing dependencies: " + errors ); } - private File outputDirHashOf( Collection toGrab ) { + private File outputDirHashOf( SortedSet toGrab ) { //noinspection ResultOfMethodCallIgnored cacheDir.mkdirs(); diff --git a/jgrab-runner/src/main/java/com/athaydes/jgrab/runner/FileJavaCode.java b/jgrab-runner/src/main/java/com/athaydes/jgrab/runner/FileJavaCode.java index 38ed7d8..799775d 100644 --- a/jgrab-runner/src/main/java/com/athaydes/jgrab/runner/FileJavaCode.java +++ b/jgrab-runner/src/main/java/com/athaydes/jgrab/runner/FileJavaCode.java @@ -10,7 +10,7 @@ import java.nio.file.Files; import java.nio.file.Path; import java.util.List; -import java.util.Set; +import java.util.SortedSet; import java.util.regex.Matcher; /** @@ -43,7 +43,7 @@ public boolean isSnippet() { } @Override - public Set extractDependencies() { + public SortedSet extractDependencies() { return Dependency.parseDependencies( lines.stream() ); } diff --git a/jgrab-runner/src/main/java/com/athaydes/jgrab/runner/JGrabRunner.java b/jgrab-runner/src/main/java/com/athaydes/jgrab/runner/JGrabRunner.java index d083586..6aa27f2 100644 --- a/jgrab-runner/src/main/java/com/athaydes/jgrab/runner/JGrabRunner.java +++ b/jgrab-runner/src/main/java/com/athaydes/jgrab/runner/JGrabRunner.java @@ -1,6 +1,6 @@ package com.athaydes.jgrab.runner; -import com.athaydes.jgrab.Dependency; +import com.athaydes.jgrab.Classpath; import com.athaydes.jgrab.code.JavaCode; import com.athaydes.jgrab.code.StdinJavaCode; import com.athaydes.jgrab.code.StringJavaCode; @@ -19,10 +19,10 @@ import java.nio.file.Files; import java.nio.file.Path; import java.nio.file.Paths; -import java.util.Collections; -import java.util.List; -import java.util.Set; +import java.util.Collection; +import java.util.Map; import java.util.concurrent.Callable; +import java.util.concurrent.ConcurrentHashMap; import java.util.jar.JarFile; /** @@ -37,6 +37,8 @@ public class JGrabRunner { JBuildGrabber.INSTANCE; // IvyGrabber.getInstance(); + private static final Map classLoaderCache = new ConcurrentHashMap<>(); + public static final String SNIPPET_OPTION = "-e"; private static JGrabOptions parseOptions( String[] args ) { @@ -142,26 +144,24 @@ private static void run( JavaCode javaCode, String[] args ) { logger.debug( "JGrab using directory: {}", tempDir ); - Set toGrab = javaCode.extractDependencies(); + var toGrab = javaCode.extractDependencies(); logger.debug( "Dependencies to grab: {}", toGrab ); - List libs; + Classpath classpath; if ( !toGrab.isEmpty() ) { - libs = grabber.grab( toGrab ); + classpath = new Classpath( toGrab, grabber.grab( toGrab ) ); } else { - libs = Collections.emptyList(); + classpath = Classpath.empty(); } - run( javaCode, args, libs ); + run( javaCode, args, classpath ); } private static void run( JavaCode javaCode, String[] args, - List libs ) { - ClassLoaderContext classLoaderContext = libs.isEmpty() ? - DefaultClassLoaderContext.INSTANCE : - new JGrabClassLoaderContext( libs ); + Classpath classpath ) { + var classLoaderContext = classLoaderFor( classpath ); if ( javaCode.isSnippet() ) { runJavaSnippet( javaCode.getCode(), classLoaderContext ); @@ -170,6 +170,21 @@ private static void run( JavaCode javaCode, } } + public static void populateClassLoaderCache( Collection classpaths ) { + logger.debug( "Populating ClassLoader cache with {} entries", classpaths.size() ); + for ( Classpath classpath : classpaths ) { + classLoaderFor( classpath ); + } + } + + private static ClassLoaderContext classLoaderFor( Classpath classpath ) { + if ( classpath.isEmpty() ) { + return DefaultClassLoaderContext.INSTANCE; + } + return classLoaderCache.computeIfAbsent( classpath.hash, ignore -> + new JGrabClassLoaderContext( classpath.resolvedArtifacts ) ); + } + private static void runJavaSnippet( String snippet, ClassLoaderContext classLoaderContext ) { logger.debug( "Running Java snippet" ); diff --git a/jgrab-runner/src/test/java/com/athaydes/jgrab/daemon/PersistentCacheTest.java b/jgrab-runner/src/test/java/com/athaydes/jgrab/daemon/PersistentCacheTest.java index 9963295..219b227 100644 --- a/jgrab-runner/src/test/java/com/athaydes/jgrab/daemon/PersistentCacheTest.java +++ b/jgrab-runner/src/test/java/com/athaydes/jgrab/daemon/PersistentCacheTest.java @@ -1,5 +1,6 @@ package com.athaydes.jgrab.daemon; +import com.athaydes.jgrab.Classpath; import com.athaydes.jgrab.Dependency; import org.junit.Test; @@ -8,10 +9,7 @@ import java.nio.charset.StandardCharsets; import java.nio.file.Files; import java.nio.file.StandardOpenOption; -import java.util.HashMap; -import java.util.List; -import java.util.Map; -import java.util.Set; +import java.util.*; import java.util.function.Supplier; import java.util.stream.Collectors; import java.util.stream.Stream; @@ -24,9 +22,9 @@ public class PersistentCacheTest { public void canLoadValidCache() throws IOException { File tempDir = Files.createTempDirectory( "jgrab-persistent-cache-test" ).toFile(); - File dep1 = new File( tempDir, "valid-dep.jar" ); - File dep2 = new File( tempDir, "other-dep.jar" ); - File dep3 = new File( tempDir, "guava-20.0.jar" ); + File dep1 = new File( tempDir, "1-valid-dep.jar" ); + File dep2 = new File( tempDir, "2-other-dep.jar" ); + File dep3 = new File( tempDir, "3-guava-20.0.jar" ); for ( File file1 : List.of( dep1, dep2, dep3 ) ) { assertTrue( file1.createNewFile() ); @@ -48,16 +46,16 @@ public void canLoadValidCache() throws IOException { throw new RuntimeException( "Should not be called" ); }; - Set firstDeps = asDependencies( Stream.of( + var firstDeps = asDependencies( Stream.of( "some:valid-dep:1.0", "other-dep:name" ) ); - Set secondDeps = asDependencies( Stream.of( "com.guava:guava:20.0" ) ); + var secondDeps = asDependencies( Stream.of( "com.guava:guava:20.0" ) ); - List firstEntry = cache.libsFor( firstDeps, badSupplier ); - List secondEntry = cache.libsFor( secondDeps, badSupplier ); + var firstEntry = cache.classpathOf( firstDeps, badSupplier ); + var secondEntry = cache.classpathOf( secondDeps, badSupplier ); - assertEquals( List.of( dep1, dep2 ), firstEntry ); - assertEquals( List.of( dep3 ), secondEntry ); + assertEquals( List.of( dep1, dep2 ), firstEntry.resolvedArtifacts ); + assertEquals( List.of( dep3 ), secondEntry.resolvedArtifacts ); } @Test @@ -82,35 +80,35 @@ public void newCacheCanBePopulatedThenSavedAsFile() throws IOException { Supplier> firstSupplier = () -> List.of( dep1, dep2 ); Supplier> secondSupplier = () -> List.of( dep3 ); - Set firstDeps = asDependencies( Stream.of( + var firstDeps = asDependencies( Stream.of( "some:valid-dep:1.0", "other-dep:name" ) ); - Set secondDeps = asDependencies( Stream.of( "com.guava:guava:20.0" ) ); + var secondDeps = asDependencies( Stream.of( "com.guava:guava:20.0" ) ); - List firstEntry = cache.libsFor( firstDeps, firstSupplier ); - List secondEntry = cache.libsFor( secondDeps, secondSupplier ); + var firstEntry = cache.classpathOf( firstDeps, firstSupplier ); + var secondEntry = cache.classpathOf( secondDeps, secondSupplier ); // the dependencies should be created and returned - assertEquals( List.of( dep1, dep2 ), firstEntry ); - assertEquals( List.of( dep3 ), secondEntry ); + assertEquals( List.of( dep1, dep2 ), firstEntry.resolvedArtifacts ); + assertEquals( List.of( dep3 ), secondEntry.resolvedArtifacts ); // saving the cache now, should write out the dependencies to the file cache.save(); assertTrue( file.isFile() ); - Map, List> expectedCache = new HashMap<>( 2 ); - expectedCache.put( firstDeps, firstEntry ); - expectedCache.put( secondDeps, secondEntry ); + Map expectedCache = new HashMap<>( 2 ); + expectedCache.put( firstEntry.hash, firstEntry ); + expectedCache.put( secondEntry.hash, secondEntry ); // when we load a new cache from the previous cache's file, the contents should be loaded as expected - Map, List> newCache = new PersistentCache( file ).loadCache(); + var newCache = new PersistentCache( file ).loadCache(); assertEquals( expectedCache, newCache ); } - private Set asDependencies( Stream declarations ) { - return declarations.map( Dependency::of ).collect( Collectors.toSet() ); + private SortedSet asDependencies( Stream declarations ) { + return declarations.map( Dependency::of ).collect( Collectors.toCollection( TreeSet::new ) ); } }