diff --git a/plexus-java/src/main/java/org/codehaus/plexus/languages/java/jpms/JavaModuleDescriptor.java b/plexus-java/src/main/java/org/codehaus/plexus/languages/java/jpms/JavaModuleDescriptor.java index bb0f4cb..4e861fe 100644 --- a/plexus-java/src/main/java/org/codehaus/plexus/languages/java/jpms/JavaModuleDescriptor.java +++ b/plexus-java/src/main/java/org/codehaus/plexus/languages/java/jpms/JavaModuleDescriptor.java @@ -21,6 +21,7 @@ import java.util.Collections; import java.util.LinkedHashSet; +import java.util.List; import java.util.Objects; import java.util.Set; @@ -38,9 +39,13 @@ public class JavaModuleDescriptor private boolean automatic; - private Set requires = new LinkedHashSet(); + private Set requires = new LinkedHashSet<>(); - private Set exports = new LinkedHashSet(); + private Set exports = new LinkedHashSet<>(); + + private Set uses = new LinkedHashSet<>(); + + private Set provides = new LinkedHashSet<>(); public String name() { @@ -62,6 +67,16 @@ public Set exports() return Collections.unmodifiableSet( exports ); } + public Set provides() + { + return Collections.unmodifiableSet( provides ); + } + + public Set uses() + { + return Collections.unmodifiableSet( uses ); + } + public static JavaModuleDescriptor.Builder newModule( String name ) { return new Builder( name ).setAutomatic( false ); @@ -145,8 +160,7 @@ private Builder setAutomatic( boolean isAutomatic ) */ public Builder requires​( Set modifiers, String name ) { - JavaRequires requires = new JavaRequires( modifiers, name ); - jModule.requires.add( requires ); + jModule.requires.add( new JavaRequires( modifiers, name ) ); return this; } @@ -158,8 +172,7 @@ private Builder setAutomatic( boolean isAutomatic ) */ public Builder requires( String name ) { - JavaRequires requires = new JavaRequires( name ); - jModule.requires.add( requires ); + jModule.requires.add( new JavaRequires( name ) ); return this; } @@ -171,8 +184,7 @@ public Builder requires( String name ) */ public Builder exports( String source ) { - JavaExports exports = new JavaExports( source ); - jModule.exports.add( exports ); + jModule.exports.add( new JavaExports( source ) ); return this; } @@ -185,8 +197,25 @@ public Builder exports( String source ) */ public Builder exports( String source, Set targets ) { - JavaExports exports = new JavaExports( source, targets ); - jModule.exports.add( exports ); + jModule.exports.add( new JavaExports( source, targets ) ); + return this; + } + + /** + * Adds a service dependence. + * + * @param service The service type + * @return This Builder + */ + public Builder uses( String service ) + { + jModule.uses.add( service ); + return this; + } + + public Builder provides​( String service, List providers ) + { + jModule.provides.add( new JavaProvides( service, providers ) ); return this; } @@ -349,4 +378,67 @@ public boolean equals( Object obj ) return true; } } + + /** + * Represents Module.Provides + * + * @author Robert Scholte + * @since 1.0.0 + */ + public static class JavaProvides + { + private final String service; + + private final List providers; + + private JavaProvides( String service, List providers ) + { + this.service = service; + this.providers = providers; + } + + public String service() + { + return service; + } + + public List providers() + { + return providers; + } + + @Override + public int hashCode() + { + return Objects.hash( service, providers ); + } + + @Override + public boolean equals( Object obj ) + { + if ( this == obj ) + { + return true; + } + if ( obj == null ) + { + return false; + } + if ( getClass() != obj.getClass() ) + { + return false; + } + + JavaProvides other = (JavaProvides) obj; + if ( !Objects.equals( service, other.service ) ) + { + return false; + } + if ( !Objects.equals( providers, other.providers ) ) + { + return false; + } + return true; + } + } } diff --git a/plexus-java/src/main/java/org/codehaus/plexus/languages/java/jpms/LocationManager.java b/plexus-java/src/main/java/org/codehaus/plexus/languages/java/jpms/LocationManager.java index 467255d..000641a 100644 --- a/plexus-java/src/main/java/org/codehaus/plexus/languages/java/jpms/LocationManager.java +++ b/plexus-java/src/main/java/org/codehaus/plexus/languages/java/jpms/LocationManager.java @@ -33,6 +33,7 @@ import javax.inject.Singleton; import org.codehaus.plexus.component.annotations.Component; +import org.codehaus.plexus.languages.java.jpms.JavaModuleDescriptor.JavaProvides; /** * Maps artifacts to modules and analyzes the type of required modules @@ -114,7 +115,15 @@ public ResolvePathsResult resolvePaths( final ResolvePathsRequest requ result.setMainModuleDescriptor( mainModuleDescriptor ); - Map availableNamedModules = new HashMap<>(); + // key = service, value = names of modules that provide this service + Map> availableProviders = new HashMap<>(); + + if( mainModuleDescriptor != null && request.isIncludeAllProviders() ) + { + collectProviders( mainModuleDescriptor, availableProviders ); + } + + Map availableNamedModules = new HashMap<>(); Map moduleNameSources = new HashMap<>(); @@ -163,6 +172,11 @@ public String extract( Path path ) moduleNameSources.put( moduleDescriptor.name(), source ); availableNamedModules.put( moduleDescriptor.name(), moduleDescriptor ); + + if ( request.isIncludeAllProviders() ) + { + collectProviders( moduleDescriptor, availableProviders ); + } } pathElements.put( t, moduleDescriptor ); @@ -196,12 +210,21 @@ public String extract( Path path ) if ( mainModuleDescriptor != null ) { Set requiredNamedModules = new HashSet<>(); - + requiredNamedModules.add( mainModuleDescriptor.name() ); - requiredNamedModules.addAll( request.getAdditionalModules() ); + selectRequires( mainModuleDescriptor, + Collections.unmodifiableMap( availableNamedModules ), + Collections.unmodifiableMap( availableProviders ), + requiredNamedModules ); - select( mainModuleDescriptor, Collections.unmodifiableMap( availableNamedModules ), requiredNamedModules ); + for ( String additionalModule : request.getAdditionalModules() ) + { + selectModule( additionalModule, + Collections.unmodifiableMap( availableNamedModules ), + Collections.unmodifiableMap( availableProviders ), + requiredNamedModules ); + } // in case of identical module names, first one wins Set collectedModules = new HashSet<>( requiredNamedModules.size() ); @@ -258,8 +281,9 @@ else if ( descriptorPath.endsWith( "module-info.class" ) ) private ResolvePathResult resolvePath( Path path, ModuleNameExtractor fileModulenameExtractor ) throws IOException { ResolvePathResult result = new ResolvePathResult(); + JavaModuleDescriptor moduleDescriptor = null; - + // either jar or outputDirectory if ( Files.isRegularFile( path ) || Files.exists( path.resolve( "module-info.class" ) ) ) { @@ -293,23 +317,62 @@ private ResolvePathResult resolvePath( Path path, ModuleNameExtractor fileModule moduleDescriptor = JavaModuleDescriptor.newAutomaticModule( moduleName ).build(); } } - result.setModuleDescriptor( moduleDescriptor ); + return result; } - private void select( JavaModuleDescriptor module, Map availableModules, - Set namedModules ) + private void selectRequires( JavaModuleDescriptor module, + Map availableModules, + Map> availableProviders, + Set namedModules ) { for ( JavaModuleDescriptor.JavaRequires requires : module.requires() ) { - String requiresName = requires.name(); - JavaModuleDescriptor requiredModule = availableModules.get( requiresName ); + selectModule( requires.name(), availableModules, availableProviders, namedModules ); + } + + for ( String uses : module.uses() ) + { + if ( availableProviders.containsKey( uses ) ) + { + for ( String providerModule : availableProviders.get( uses ) ) + { + JavaModuleDescriptor requiredModule = availableModules.get( providerModule ); + + if ( requiredModule != null && namedModules.add( providerModule ) ) + { + selectRequires( requiredModule, availableModules, availableProviders, namedModules ); + } + } + } + } + } + + private void selectModule( String module, Map availableModules, Map> availableProviders, + Set namedModules ) + { + JavaModuleDescriptor requiredModule = availableModules.get( module ); - if ( requiredModule != null && namedModules.add( requiresName ) ) + if ( requiredModule != null && namedModules.add( module ) ) + { + selectRequires( requiredModule, availableModules, availableProviders, namedModules ); + } + } + + private void collectProviders( JavaModuleDescriptor moduleDescriptor, Map> availableProviders ) + { + for ( JavaProvides provides : moduleDescriptor.provides() ) + { + Set providingModules = availableProviders.get( provides.service() ); + + if ( providingModules == null ) { - select( requiredModule, availableModules, namedModules ); + providingModules = new HashSet<>(); + + availableProviders.put( provides.service(), providingModules ); } + providingModules.add( moduleDescriptor.name() ); } } } diff --git a/plexus-java/src/main/java/org/codehaus/plexus/languages/java/jpms/ResolvePathsRequest.java b/plexus-java/src/main/java/org/codehaus/plexus/languages/java/jpms/ResolvePathsRequest.java index 0d7ebcc..7eed208 100644 --- a/plexus-java/src/main/java/org/codehaus/plexus/languages/java/jpms/ResolvePathsRequest.java +++ b/plexus-java/src/main/java/org/codehaus/plexus/languages/java/jpms/ResolvePathsRequest.java @@ -22,6 +22,7 @@ import java.io.File; import java.nio.file.Path; import java.nio.file.Paths; +import java.util.Arrays; import java.util.Collection; import java.util.Collections; @@ -40,6 +41,8 @@ public abstract class ResolvePathsRequest private Collection pathElements; private Collection additionalModules; + + private boolean includeAllProviders; private ResolvePathsRequest() { @@ -54,6 +57,11 @@ public static ResolvePathsRequest withFiles( Collection files ) return ofFiles( files ); } + public static ResolvePathsRequest ofFiles( File... files ) + { + return ofFiles( Arrays.asList( files ) ); + } + public static ResolvePathsRequest ofFiles( Collection files ) { ResolvePathsRequest request = new ResolvePathsRequest() @@ -78,6 +86,11 @@ public static ResolvePathsRequest withPaths( Collection paths ) return ofPaths( paths ); } + public static ResolvePathsRequest ofPaths( Path... paths ) + { + return ofPaths( Arrays.asList( paths ) ); + } + public static ResolvePathsRequest ofPaths( Collection paths ) { ResolvePathsRequest request = new ResolvePathsRequest() { @@ -100,6 +113,11 @@ public static ResolvePathsRequest withStrings( Collection string return ofStrings( strings ); } + public static ResolvePathsRequest ofStrings( String... strings ) + { + return ofStrings( Arrays.asList( strings ) ); + } + public static ResolvePathsRequest ofStrings( Collection strings ) { ResolvePathsRequest request = new ResolvePathsRequest() { @@ -178,4 +196,21 @@ public Collection getAdditionalModules() } return additionalModules; } + + /** + * Will also include all modules that contain providers for used services, should only be used at runtime (not during compile nor test) + * + * @param includeAllProviders + * @return this request + */ + public ResolvePathsRequest setIncludeAllProviders( boolean includeAllProviders ) + { + this.includeAllProviders = includeAllProviders; + return this; + } + + public boolean isIncludeAllProviders() + { + return includeAllProviders; + } } diff --git a/plexus-java/src/main/java9/org/codehaus/plexus/languages/java/jpms/BinaryModuleInfoParser.java b/plexus-java/src/main/java9/org/codehaus/plexus/languages/java/jpms/BinaryModuleInfoParser.java index 2bb36d9..ae2440a 100644 --- a/plexus-java/src/main/java9/org/codehaus/plexus/languages/java/jpms/BinaryModuleInfoParser.java +++ b/plexus-java/src/main/java9/org/codehaus/plexus/languages/java/jpms/BinaryModuleInfoParser.java @@ -61,6 +61,8 @@ JavaModuleDescriptor parse( InputStream in ) throws IOException } } + + return builder.build(); } } diff --git a/plexus-java/src/site/markdown/locationmanager.md b/plexus-java/src/site/markdown/locationmanager.md index 7baac48..aa4c92c 100644 --- a/plexus-java/src/site/markdown/locationmanager.md +++ b/plexus-java/src/site/markdown/locationmanager.md @@ -24,6 +24,8 @@ Additional methods are: - `setAdditionalModules`, in case the consumer wants to use `--add-modules` +- `setIncludeAllProviders`, in general would only be used at runtime, not during compile or test. In case `uses` is used, all modules with matching `provides` are added as well. + - `setJdkHome`, should point to Java 9 or above in case the runtime of this library is Java 7 or 8 - `setMainModuleDescriptor`, which can either be a `module-info.java` or `module-info.class` diff --git a/plexus-java/src/test/java/org/codehaus/plexus/languages/java/jpms/LocationManagerTest.java b/plexus-java/src/test/java/org/codehaus/plexus/languages/java/jpms/LocationManagerTest.java index 4e44766..ba5cffb 100644 --- a/plexus-java/src/test/java/org/codehaus/plexus/languages/java/jpms/LocationManagerTest.java +++ b/plexus-java/src/test/java/org/codehaus/plexus/languages/java/jpms/LocationManagerTest.java @@ -20,11 +20,8 @@ */ import static org.hamcrest.CoreMatchers.is; -import static org.hamcrest.CoreMatchers.not; import static org.hamcrest.CoreMatchers.nullValue; -import static org.hamcrest.CoreMatchers.startsWith; import static org.junit.Assert.assertThat; -import static org.junit.Assume.assumeThat; import static org.mockito.Matchers.any; import static org.mockito.Mockito.when; @@ -183,7 +180,7 @@ public void testNonJar() throws Exception @Test public void testAdditionalModules() throws Exception { - Path p = Paths.get( "src/test/resources/jar.empty/plexus-java-1.0.0-SNAPSHOT.jar" ); + Path p = Paths.get( "src/test/resources/mock/jar0" ); JavaModuleDescriptor descriptor = JavaModuleDescriptor.newModule( "base" ).build(); when( qdoxParser.fromSourcePath( any( Path.class ) ) ).thenReturn( descriptor ); @@ -206,7 +203,7 @@ public void testAdditionalModules() throws Exception @Test public void testResolvePath() throws Exception { - Path abc = Paths.get( "src/test/resources/jar.descriptor/asm-6.0_BETA.jar" ); + Path abc = Paths.get( "src/test/resources/mock/jar0" ); ResolvePathRequest request = ResolvePathRequest.ofPath( abc ); when( asmParser.getModuleDescriptor( abc ) ).thenReturn( JavaModuleDescriptor.newModule( "org.objectweb.asm" ).build() ); @@ -216,4 +213,97 @@ public void testResolvePath() throws Exception assertThat( result.getModuleDescriptor(), is( JavaModuleDescriptor.newModule( "org.objectweb.asm" ).build() ) ); assertThat( result.getModuleNameSource(), is( ModuleNameSource.MODULEDESCRIPTOR ) ); } + + @Test + public void testNoMatchingProviders() throws Exception + { + Path abc = Paths.get( "src/test/resources/mock/module-info.java" ); // some file called module-info.java + Path def = Paths.get( "src/test/resources/mock/jar0" ); // any existing file + ResolvePathsRequest request = ResolvePathsRequest.ofPaths( def ).setMainModuleDescriptor( abc ).setIncludeAllProviders( true ); + + when( qdoxParser.fromSourcePath( abc ) ).thenReturn( JavaModuleDescriptor.newModule( "abc" ).uses( "device" ).build() ); + when( asmParser.getModuleDescriptor( def ) ).thenReturn( JavaModuleDescriptor.newModule( "def" ).provides​( "tool", Arrays.asList( "java", "javac" ) ).build() ); + + ResolvePathsResult result = locationManager.resolvePaths( request ); + assertThat( result.getPathElements().size(), is( 1 ) ); + assertThat( result.getModulepathElements().size(), is( 0 ) ); + assertThat( result.getClasspathElements().size(), is( 1 ) ); + assertThat( result.getPathExceptions().size(), is( 0 ) ); + } + + + @Test + public void testMainModuleDescriptorWithProviders() throws Exception + { + Path abc = Paths.get( "src/test/resources/mock/module-info.java" ); // some file called module-info.java + Path def = Paths.get( "src/test/resources/mock/jar0" ); // any existing file + ResolvePathsRequest request = ResolvePathsRequest.ofPaths( def ).setMainModuleDescriptor( abc ).setIncludeAllProviders( true ); + + when( qdoxParser.fromSourcePath( abc ) ).thenReturn( JavaModuleDescriptor.newModule( "abc" ).uses( "tool" ).build() ); + when( asmParser.getModuleDescriptor( def ) ).thenReturn( JavaModuleDescriptor.newModule( "def" ).provides​( "tool", Arrays.asList( "java", "javac" ) ).build() ); + + ResolvePathsResult result = locationManager.resolvePaths( request ); + assertThat( result.getPathElements().size(), is( 1 ) ); + assertThat( result.getModulepathElements().size(), is( 1 ) ); + assertThat( result.getClasspathElements().size(), is( 0 ) ); + assertThat( result.getPathExceptions().size(), is( 0 ) ); + } + + @Test + public void testMainModuleDescriptorWithProvidersDontIncludeProviders() throws Exception + { + Path abc = Paths.get( "src/test/resources/mock/module-info.java" ); // some file called module-info.java + Path def = Paths.get( "src/test/resources/mock/jar0" ); // any existing file + ResolvePathsRequest request = ResolvePathsRequest.ofPaths( def ).setMainModuleDescriptor( abc ); + + when( qdoxParser.fromSourcePath( abc ) ).thenReturn( JavaModuleDescriptor.newModule( "abc" ).uses( "tool" ).build() ); + when( asmParser.getModuleDescriptor( def ) ).thenReturn( JavaModuleDescriptor.newModule( "def" ).provides​( "tool", Arrays.asList( "java", "javac" ) ).build() ); + + ResolvePathsResult result = locationManager.resolvePaths( request ); + assertThat( result.getPathElements().size(), is( 1 ) ); + assertThat( result.getModulepathElements().size(), is( 0 ) ); + assertThat( result.getClasspathElements().size(), is( 1 ) ); + assertThat( result.getPathExceptions().size(), is( 0 ) ); + } + + @Test + public void testTransitiveProviders() throws Exception + { + Path abc = Paths.get( "src/test/resources/mock/module-info.java" ); // some file called module-info.java + Path def = Paths.get( "src/test/resources/mock/jar0" ); // any existing file + Path ghi = Paths.get( "src/test/resources/mock/jar1" ); // any existing file + ResolvePathsRequest request = ResolvePathsRequest.ofPaths( def, ghi ).setMainModuleDescriptor( abc ).setIncludeAllProviders( true ); + + when( qdoxParser.fromSourcePath( abc ) ).thenReturn( JavaModuleDescriptor.newModule( "abc" ).requires( "ghi" ).build() ); + when( asmParser.getModuleDescriptor( def ) ).thenReturn( JavaModuleDescriptor.newModule( "def" ).provides​( "tool", Arrays.asList( "java", "javac" ) ).build() ); + when( asmParser.getModuleDescriptor( ghi ) ).thenReturn( JavaModuleDescriptor.newModule( "ghi" ).uses( "tool" ).build() ); + + + ResolvePathsResult result = locationManager.resolvePaths( request ); + assertThat( result.getPathElements().size(), is( 2 ) ); + assertThat( result.getModulepathElements().size(), is( 2 ) ); + assertThat( result.getClasspathElements().size(), is( 0 ) ); + assertThat( result.getPathExceptions().size(), is( 0 ) ); + } + + @Test + public void testDontIncludeProviders() throws Exception + { + Path abc = Paths.get( "src/test/resources/mock/module-info.java" ); // some file called module-info.java + Path def = Paths.get( "src/test/resources/mock/jar0" ); // any existing file + Path ghi = Paths.get( "src/test/resources/mock/jar1" ); // any existing file + ResolvePathsRequest request = ResolvePathsRequest.ofPaths( def, ghi ).setMainModuleDescriptor( abc ); + + when( qdoxParser.fromSourcePath( abc ) ).thenReturn( JavaModuleDescriptor.newModule( "abc" ).requires( "ghi" ).build() ); + when( asmParser.getModuleDescriptor( def ) ).thenReturn( JavaModuleDescriptor.newModule( "def" ).provides​( "tool", Arrays.asList( "java", "javac" ) ).build() ); + when( asmParser.getModuleDescriptor( ghi ) ).thenReturn( JavaModuleDescriptor.newModule( "ghi" ).uses( "tool" ).build() ); + + + ResolvePathsResult result = locationManager.resolvePaths( request ); + assertThat( result.getPathElements().size(), is( 2 ) ); + assertThat( result.getModulepathElements().size(), is( 1 ) ); + assertThat( result.getClasspathElements().size(), is( 1 ) ); + assertThat( result.getPathExceptions().size(), is( 0 ) ); + } + } diff --git a/plexus-java/src/test/resources/mock/jar0 b/plexus-java/src/test/resources/mock/jar0 new file mode 100644 index 0000000..e69de29 diff --git a/plexus-java/src/test/resources/mock/jar1 b/plexus-java/src/test/resources/mock/jar1 new file mode 100644 index 0000000..e69de29