diff --git a/src/main/java/org/codehaus/plexus/archiver/jar/JarToolModularJarArchiver.java b/src/main/java/org/codehaus/plexus/archiver/jar/JarToolModularJarArchiver.java
new file mode 100644
index 000000000..e8cb67834
--- /dev/null
+++ b/src/main/java/org/codehaus/plexus/archiver/jar/JarToolModularJarArchiver.java
@@ -0,0 +1,256 @@
+/**
+ *
+ * Copyright 2018 The Apache Software Foundation
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.codehaus.plexus.archiver.jar;
+
+import org.apache.commons.compress.parallel.InputStreamSupplier;
+import org.codehaus.plexus.archiver.ArchiverException;
+import org.codehaus.plexus.archiver.util.ArchiveEntryUtils;
+import org.codehaus.plexus.archiver.util.ResourceUtils;
+import org.codehaus.plexus.archiver.zip.ConcurrentJarCreator;
+import org.codehaus.plexus.components.io.resources.PlexusIoResource;
+import org.codehaus.plexus.util.FileUtils;
+
+import java.io.File;
+import java.io.IOException;
+import java.io.PrintStream;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.regex.Pattern;
+
+/**
+ * A {@link ModularJarArchiver} implementation that uses
+ * the {@code jar} tool provided by
+ * {@code java.util.spi.ToolProvider} to create
+ * modular JAR files.
+ *
+ *
+ * The basic JAR archive is created by {@link JarArchiver}
+ * and the {@code jar} tool is used to upgrade it to modular JAR.
+ *
+ *
+ * If the JAR file does not contain module descriptor
+ * or the JDK does not provide the {@code jar} tool
+ * (for example JDK prior to Java 9), then the
+ * archive created by {@link JarArchiver}
+ * is left unchanged.
+ */
+public class JarToolModularJarArchiver
+ extends ModularJarArchiver
+{
+ private static final String MODULE_DESCRIPTOR_FILE_NAME
+ = "module-info.class";
+
+ private static final Pattern MRJAR_VERSION_AREA
+ = Pattern.compile( "META-INF/versions/\\d+/" );
+
+ private Object jarTool;
+
+ private boolean moduleDescriptorFound;
+
+ private Path tempDir;
+
+ public JarToolModularJarArchiver()
+ {
+ try
+ {
+ Class> toolProviderClass =
+ Class.forName( "java.util.spi.ToolProvider" );
+ Object jarToolOptional = toolProviderClass
+ .getMethod( "findFirst", String.class )
+ .invoke( null, "jar" );
+
+ jarTool = jarToolOptional.getClass().getMethod( "get" )
+ .invoke( jarToolOptional );
+ }
+ catch ( ReflectiveOperationException | SecurityException e )
+ {
+ // Ignore. It is expected that the jar tool
+ // may not be available.
+ }
+ }
+
+ @Override
+ protected void zipFile( InputStreamSupplier is, ConcurrentJarCreator zOut,
+ String vPath, long lastModified, File fromArchive,
+ int mode, String symlinkDestination,
+ boolean addInParallel )
+ throws IOException, ArchiverException
+ {
+ // We store the module descriptors in temporary location
+ // and then add it to the JAR file using the JDK jar tool.
+ // It may look strange at first, but to update a JAR file
+ // you need to add new files[1] and the only files
+ // we're sure that exists in modular JAR file
+ // are the module descriptors.
+ //
+ // [1] There are some exceptions but we need at least one file to
+ // ensure it will work in all cases.
+ if ( jarTool != null && isModuleDescriptor( vPath ) )
+ {
+ getLogger().debug( "Module descriptor found: " + vPath );
+
+ moduleDescriptorFound = true;
+
+ // Copy the module descriptor to temporary directory
+ // so later then can be added to the JAR archive
+ // by the jar tool.
+
+ if ( tempDir == null )
+ {
+ tempDir = Files
+ .createTempDirectory( "plexus-archiver-modular_jar-" );
+ tempDir.toFile().deleteOnExit();
+ }
+
+ File destFile = tempDir.resolve( vPath ).toFile();
+ destFile.getParentFile().mkdirs();
+ destFile.deleteOnExit();
+
+ ResourceUtils.copyFile( is.get(), destFile );
+ ArchiveEntryUtils.chmod( destFile, mode );
+ destFile.setLastModified( lastModified == PlexusIoResource.UNKNOWN_MODIFICATION_DATE
+ ? System.currentTimeMillis()
+ : lastModified );
+ }
+ else
+ {
+ super.zipFile( is, zOut, vPath, lastModified,
+ fromArchive, mode, symlinkDestination, addInParallel );
+ }
+ }
+
+ @Override
+ protected void postCreateArchive()
+ throws ArchiverException
+ {
+ if ( !moduleDescriptorFound )
+ {
+ // no need to update the JAR archive
+ return;
+ }
+
+ try
+ {
+ getLogger().debug( "Using the jar tool to " +
+ "update the archive to modular JAR." );
+
+ Integer result = (Integer) jarTool.getClass()
+ .getMethod( "run",
+ PrintStream.class, PrintStream.class, String[].class )
+ .invoke( jarTool,
+ System.out, System.err,
+ getJarToolArguments() );
+
+ if ( result != null && result != 0 )
+ {
+ throw new ArchiverException( "Could not create modular JAR file. " +
+ "The JDK jar tool exited with " + result );
+ }
+ }
+ catch ( ReflectiveOperationException | SecurityException e )
+ {
+ throw new ArchiverException( "Exception occurred " +
+ "while creating modular JAR file", e );
+ }
+ finally
+ {
+ clearTempDirectory();
+ }
+ }
+
+ /**
+ * Returns {@code true} if {@code path}
+ * is a module descriptor.
+ */
+ private boolean isModuleDescriptor( String path )
+ {
+ if ( path.endsWith( MODULE_DESCRIPTOR_FILE_NAME ) )
+ {
+ String prefix = path.substring( 0,
+ path.lastIndexOf( MODULE_DESCRIPTOR_FILE_NAME ) );
+
+ // the path is a module descriptor if it located
+ // into the root of the archive or into the
+ // version are of a multi-release JAR file
+ return prefix.isEmpty() ||
+ MRJAR_VERSION_AREA.matcher( prefix ).matches();
+ }
+ else
+ {
+ return false;
+ }
+ }
+
+ /**
+ * Prepares the arguments for the jar tool.
+ * It takes into account the module version,
+ * main class, etc.
+ */
+ private String[] getJarToolArguments()
+ {
+ List args = new ArrayList<>();
+
+ args.add( "--update" );
+ args.add( "--file" );
+ args.add( getDestFile().getAbsolutePath() );
+
+ if ( getModuleMainClass() != null )
+ {
+ args.add( "--main-class" );
+ args.add( getModuleMainClass() );
+ }
+
+ if ( getModuleVersion() != null )
+ {
+ args.add( "--module-version" );
+ args.add( getModuleVersion() );
+ }
+
+ if ( !isCompress() )
+ {
+ args.add( "--no-compress" );
+ }
+
+ args.add( "-C" );
+ args.add( tempDir.toFile().getAbsolutePath() );
+ args.add( "." );
+
+ return args.toArray( new String[]{} );
+ }
+
+ /**
+ * Makes best effort the clean up
+ * the temporary directory used.
+ */
+ private void clearTempDirectory()
+ {
+ try
+ {
+ if ( tempDir != null )
+ {
+ FileUtils.deleteDirectory( tempDir.toFile() );
+ }
+ }
+ catch ( IOException e )
+ {
+ // Ignore. It is just best effort.
+ }
+ }
+
+}
diff --git a/src/main/java/org/codehaus/plexus/archiver/jar/ModularJarArchiver.java b/src/main/java/org/codehaus/plexus/archiver/jar/ModularJarArchiver.java
new file mode 100644
index 000000000..bf636e110
--- /dev/null
+++ b/src/main/java/org/codehaus/plexus/archiver/jar/ModularJarArchiver.java
@@ -0,0 +1,76 @@
+/**
+ *
+ * Copyright 2018 The Apache Software Foundation
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.codehaus.plexus.archiver.jar;
+
+/**
+ * Base class for creating modular JAR archives.
+ *
+ * Subclasses are required to be able to handle both
+ * JAR archives with module descriptor (modular JAR)
+ * and without ("regular" JAR).
+ * That would allow clients of this class to use
+ * it without prior knowledge if the classes
+ * they are going to add are part of module
+ * (contain module descriptor class) or not.
+ *
+ * @since 3.6
+ */
+public abstract class ModularJarArchiver
+ extends JarArchiver
+{
+ private String moduleMainClass;
+
+ private String moduleVersion;
+
+ public String getModuleMainClass()
+ {
+ return moduleMainClass;
+ }
+
+ /**
+ * Sets the module main class.
+ * Ignored if the JAR file does not contain
+ * module descriptor.
+ *
+ * Note that implementations may choose
+ * to replace the value set in the manifest as well.
+ *
+ * @param moduleMainClass the module main class.
+ */
+ public void setModuleMainClass( String moduleMainClass )
+ {
+ this.moduleMainClass = moduleMainClass;
+ }
+
+ public String getModuleVersion()
+ {
+ return moduleVersion;
+ }
+
+ /**
+ * Sets the module version.
+ * Ignored if the JAR file does not contain
+ * module descriptor.
+ *
+ * @param moduleVersion the module version.
+ */
+ public void setModuleVersion( String moduleVersion )
+ {
+ this.moduleVersion = moduleVersion;
+ }
+
+}
diff --git a/src/main/resources/META-INF/plexus/components.xml b/src/main/resources/META-INF/plexus/components.xml
index d19b7a89f..71d47ac1f 100644
--- a/src/main/resources/META-INF/plexus/components.xml
+++ b/src/main/resources/META-INF/plexus/components.xml
@@ -41,6 +41,12 @@
org.codehaus.plexus.archiver.jar.JarArchiver
per-lookup
+
+ org.codehaus.plexus.archiver.Archiver
+ mjar
+ org.codehaus.plexus.archiver.jar.JarToolModularJarArchiver
+ per-lookup
+
org.codehaus.plexus.archiver.Archiver
diff --git a/src/test/java/org/codehaus/plexus/archiver/jar/BaseJarArchiverTest.java b/src/test/java/org/codehaus/plexus/archiver/jar/BaseJarArchiverTest.java
new file mode 100644
index 000000000..383ea7f75
--- /dev/null
+++ b/src/test/java/org/codehaus/plexus/archiver/jar/BaseJarArchiverTest.java
@@ -0,0 +1,71 @@
+/**
+ *
+ * Copyright 2018 The Apache Software Foundation
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.codehaus.plexus.archiver.jar;
+
+import org.codehaus.plexus.archiver.ArchiverException;
+import org.codehaus.plexus.util.IOUtil;
+import org.junit.Test;
+
+import java.io.File;
+import java.io.FileInputStream;
+import java.io.IOException;
+import java.io.InputStream;
+import java.util.zip.ZipEntry;
+import java.util.zip.ZipFile;
+
+import static org.junit.Assert.assertNotNull;
+import static org.junit.Assert.assertTrue;
+
+public abstract class BaseJarArchiverTest
+{
+
+ /*
+ * Verify that the JarArchiver implementation
+ * could create basic JAR file
+ */
+ @Test
+ public void testCreateJar()
+ throws IOException, ArchiverException
+ {
+ File jarFile = new File( "target/output/testJar.jar" );
+ jarFile.delete();
+
+ JarArchiver archiver = getJarArchiver();
+ archiver.setDestFile( jarFile );
+ archiver.addDirectory( new File( "src/test/resources/java-classes" ) );
+
+ archiver.createArchive();
+
+ // verify that the JAR file is created and contains the expected files
+ try ( ZipFile resultingArchive = new ZipFile( jarFile ) )
+ {
+ // verify that the JAR file contains manifest file
+ assertNotNull( resultingArchive.getEntry( "META-INF/MANIFEST.MF" ) );
+
+ // verify the JAR contains the class and it is not corrupted
+ ZipEntry classFileEntry = resultingArchive.getEntry( "com/example/app/Main.class" );
+ InputStream resultingClassFile = resultingArchive.getInputStream( classFileEntry );
+ InputStream originalClassFile =
+ new FileInputStream( "src/test/resources/java-classes/com/example/app/Main.class" );
+
+ assertTrue( IOUtil.contentEquals( originalClassFile, resultingClassFile ) );
+ }
+ }
+
+ protected abstract JarArchiver getJarArchiver();
+
+}
diff --git a/src/test/java/org/codehaus/plexus/archiver/jar/JarArchiverTest.java b/src/test/java/org/codehaus/plexus/archiver/jar/JarArchiverTest.java
index 8529977a0..ee118cf4e 100644
--- a/src/test/java/org/codehaus/plexus/archiver/jar/JarArchiverTest.java
+++ b/src/test/java/org/codehaus/plexus/archiver/jar/JarArchiverTest.java
@@ -5,19 +5,20 @@
import java.io.IOException;
import java.util.Random;
import org.codehaus.plexus.archiver.ArchiverException;
-import junit.framework.TestCase;
+import org.junit.Test;
public class JarArchiverTest
- extends TestCase
+ extends BaseJarArchiverTest
{
+ @Test
public void testCreateManifestOnlyJar()
throws IOException, ManifestException, ArchiverException
{
File jarFile = File.createTempFile( "JarArchiverTest.", ".jar" );
jarFile.deleteOnExit();
- JarArchiver archiver = new JarArchiver();
+ JarArchiver archiver = getJarArchiver();
archiver.setDestFile( jarFile );
Manifest manifest = new Manifest();
@@ -30,18 +31,20 @@ public void testCreateManifestOnlyJar()
archiver.createArchive();
}
+ @Test
public void testNonCompressed()
throws IOException, ManifestException, ArchiverException
{
File jarFile = new File( "target/output/jarArchiveNonCompressed.jar" );
- JarArchiver archiver = new JarArchiver();
+ JarArchiver archiver = getJarArchiver();
archiver.setDestFile( jarFile );
archiver.setCompress( false );
archiver.addDirectory( new File( "src/test/resources/mjar179" ) );
archiver.createArchive();
}
+ @Test
public void testVeryLargeJar()
throws IOException, ManifestException, ArchiverException
{
@@ -65,10 +68,15 @@ public void testVeryLargeJar()
File jarFile = new File( "target/output/veryLargeJar.jar" );
- JarArchiver archiver = new JarArchiver();
+ JarArchiver archiver = getJarArchiver();
archiver.setDestFile( jarFile );
archiver.addDirectory( tmpDir );
archiver.createArchive();
}
+ @Override
+ protected JarArchiver getJarArchiver()
+ {
+ return new JarArchiver();
+ }
}
diff --git a/src/test/java/org/codehaus/plexus/archiver/jar/JarToolModularJarArchiverTest.java b/src/test/java/org/codehaus/plexus/archiver/jar/JarToolModularJarArchiverTest.java
new file mode 100644
index 000000000..609692f4c
--- /dev/null
+++ b/src/test/java/org/codehaus/plexus/archiver/jar/JarToolModularJarArchiverTest.java
@@ -0,0 +1,308 @@
+/**
+ *
+ * Copyright 2018 The Apache Software Foundation
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.codehaus.plexus.archiver.jar;
+
+import java.io.File;
+import java.io.InputStream;
+import java.lang.reflect.Method;
+import java.util.Arrays;
+import java.util.Enumeration;
+import java.util.HashSet;
+import java.util.Set;
+import java.util.zip.ZipEntry;
+import java.util.zip.ZipFile;
+
+import org.codehaus.plexus.archiver.ArchiverException;
+import org.junit.Before;
+import org.junit.Test;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertNotNull;
+import static org.junit.Assume.assumeFalse;
+import static org.junit.Assume.assumeTrue;
+
+public class JarToolModularJarArchiverTest
+ extends BaseJarArchiverTest
+{
+
+ private ModularJarArchiver archiver;
+
+ /*
+ * Configures the ModularJarArchiver for the test cases.
+ */
+ @Before
+ public void ModularJarArchiver()
+ throws Exception
+ {
+ File jarFile = new File( "target/output/modular.jar" );
+ jarFile.delete();
+
+ archiver = getJarArchiver();
+ archiver.setDestFile( jarFile );
+ archiver.addDirectory( new File( "src/test/resources/java-classes" ) );
+ }
+
+ /*
+ * Verify that the main class and the version are properly set for a modular JAR file.
+ */
+ @Test
+ public void testModularJarWithMainClassAndVersion()
+ throws Exception
+ {
+ assumeTrue( modulesAreSupported() );
+
+ archiver.addDirectory( new File( "src/test/resources/java-module-descriptor" ) );
+ archiver.setModuleVersion( "1.0.0" );
+ archiver.setModuleMainClass( "com.example.app.Main" );
+
+ archiver.createArchive();
+
+ // verify that the proper version and main class are set
+ assertModularJarFile( archiver.getDestFile(),
+ "1.0.0", "com.example.app.Main", "com.example.app", "com.example.resources" );
+ }
+
+ /*
+ * Verify that a modular JAR file is created even when no additional attributes are set.
+ */
+ @Test
+ public void testModularJar()
+ throws Exception
+ {
+ assumeTrue( modulesAreSupported() );
+
+ archiver.addDirectory( new File( "src/test/resources/java-module-descriptor" ) );
+ archiver.createArchive();
+
+ // verify that the proper version and main class are set
+ assertModularJarFile( archiver.getDestFile(),
+ null, null, "com.example.app", "com.example.resources" );
+ }
+
+ /*
+ * Verify that exception is thrown when the modular JAR is not valid.
+ */
+ @Test( expected = ArchiverException.class )
+ public void testInvalidModularJar()
+ throws Exception
+ {
+ assumeTrue( modulesAreSupported() );
+
+ archiver.addDirectory( new File( "src/test/resources/java-module-descriptor" ) );
+ // Not a valid version
+ archiver.setModuleVersion( "notAValidVersion" );
+
+ archiver.createArchive();
+ }
+
+ /*
+ * Verify that modular JAR files could be created even
+ * if the Java version does not support modules.
+ */
+ @Test
+ public void testModularJarPriorJava9()
+ throws Exception
+ {
+ assumeFalse( modulesAreSupported() );
+
+ archiver.addDirectory( new File( "src/test/resources/java-module-descriptor" ) );
+ archiver.setModuleVersion( "1.0.0" );
+ archiver.setModuleMainClass( "com.example.app.Main" );
+
+ archiver.createArchive();
+
+ // verify that the modular jar is created
+ try ( ZipFile resultingArchive = new ZipFile( archiver.getDestFile() ) )
+ {
+ assertNotNull( resultingArchive.getEntry( "module-info.class" ) );
+ }
+ }
+
+ /*
+ * Verify that the compression flag is respected.
+ */
+ @Test
+ public void testNoCompression()
+ throws Exception
+ {
+ assumeTrue( modulesAreSupported() );
+
+ archiver.addDirectory( new File( "src/test/resources/java-module-descriptor" ) );
+ archiver.setCompress( false );
+
+ archiver.createArchive();
+
+ // verify that the entries are not compressed
+ try ( ZipFile resultingArchive = new ZipFile( archiver.getDestFile() ) )
+ {
+ Enumeration extends ZipEntry> entries = resultingArchive.entries();
+
+ while ( entries.hasMoreElements() )
+ {
+ ZipEntry entry = entries.nextElement();
+
+ assertEquals( ZipEntry.STORED, entry.getMethod() );
+ }
+ }
+ }
+
+ /*
+ * Verify that the compression set in the "plain" JAR file
+ * is kept after it is updated to modular JAR file.
+ */
+ @Test
+ public void testCompression()
+ throws Exception
+ {
+ assumeTrue( modulesAreSupported() );
+
+ archiver.addDirectory( new File( "src/test/resources/java-module-descriptor" ) );
+ archiver.addFile( new File( "src/test/jars/test.jar" ), "META-INF/lib/test.jar" );
+ archiver.setRecompressAddedZips( false );
+
+ archiver.createArchive();
+
+ // verify that the compression is kept
+ try ( ZipFile resultingArchive = new ZipFile( archiver.getDestFile() ) )
+ {
+ Enumeration extends ZipEntry> entries = resultingArchive.entries();
+
+ while ( entries.hasMoreElements() )
+ {
+ ZipEntry entry = entries.nextElement();
+
+ int expectedMethod = entry.isDirectory() || entry.getName().endsWith( ".jar" )
+ ? ZipEntry.STORED
+ : ZipEntry.DEFLATED;
+ assertEquals( expectedMethod, entry.getMethod() );
+ }
+ }
+ }
+
+ /*
+ * Verify that a module descriptor in the versioned area is handled correctly.
+ */
+ @Test
+ public void testModularMultiReleaseJar()
+ throws Exception
+ {
+ assumeTrue( modulesAreSupported() );
+
+ archiver.addFile( new File( "src/test/resources/java-module-descriptor/module-info.class" ),
+ "META-INF/versions/9/module-info.class" );
+ Manifest manifest = new Manifest();
+ manifest.addConfiguredAttribute( new Manifest.Attribute( "Multi-Release", "true" ) );
+ archiver.addConfiguredManifest( manifest );
+ archiver.setModuleVersion( "1.0.0" );
+ archiver.setModuleMainClass( "com.example.app.Main" );
+
+ archiver.createArchive();
+
+ // verify that the resulting modular jar has the proper version and main class set
+ try ( ZipFile resultingArchive = new ZipFile( archiver.getDestFile() ) )
+ {
+ ZipEntry moduleDescriptorEntry =
+ resultingArchive.getEntry( "META-INF/versions/9/module-info.class" );
+ InputStream resultingModuleDescriptor = resultingArchive.getInputStream( moduleDescriptorEntry );
+
+ assertModuleDescriptor( resultingModuleDescriptor,
+ "1.0.0", "com.example.app.Main", "com.example.app", "com.example.resources" );
+ }
+ }
+
+ @Override
+ protected JarToolModularJarArchiver getJarArchiver()
+ {
+ return new JarToolModularJarArchiver();
+ }
+
+ private void assertModularJarFile( File jarFile ,
+ String expectedVersion, String expectedMainClass,
+ String... expectedPackages )
+ throws Exception
+ {
+ try ( ZipFile resultingArchive = new ZipFile( jarFile ) )
+ {
+ ZipEntry moduleDescriptorEntry = resultingArchive.getEntry( "module-info.class" );
+ InputStream resultingModuleDescriptor = resultingArchive.getInputStream( moduleDescriptorEntry );
+
+ assertModuleDescriptor( resultingModuleDescriptor,
+ expectedVersion, expectedMainClass, expectedPackages );
+ }
+ }
+
+ private void assertModuleDescriptor( InputStream moduleDescriptorInputStream,
+ String expectedVersion, String expectedMainClass,
+ String... expectedPackages )
+ throws Exception
+ {
+ // ModuleDescriptor methods are available from Java 9 so let's get by reflection
+ Class> moduleDescriptorClass = Class.forName( "java.lang.module.ModuleDescriptor" );
+ Class> optionalClass = Class.forName( "java.util.Optional" );
+ Method readMethod = moduleDescriptorClass.getMethod( "read", InputStream.class );
+ Method mainClassMethod = moduleDescriptorClass.getMethod( "mainClass" );
+ Method rawVersionMethod = moduleDescriptorClass.getMethod( "rawVersion" );
+ Method packagesMethod = moduleDescriptorClass.getMethod( "packages" );
+ Method isPresentMethod = optionalClass.getMethod( "isPresent" );
+ Method getMethod = optionalClass.getMethod( "get" );
+
+ // Read the module from the input stream
+ Object moduleDescriptor = readMethod.invoke( null, moduleDescriptorInputStream );
+
+ // Get the module main class
+ Object mainClassOptional = mainClassMethod.invoke( moduleDescriptor );
+ String actualMainClass = null;
+ if ( (boolean) isPresentMethod.invoke( mainClassOptional ) )
+ {
+ actualMainClass = (String) getMethod.invoke( mainClassOptional );
+ }
+
+ // Get the module version
+ Object versionOptional = rawVersionMethod.invoke( moduleDescriptor );
+ String actualVersion = null;
+ if ( (boolean) isPresentMethod.invoke( versionOptional ) )
+ {
+ actualVersion = (String) getMethod.invoke( versionOptional );
+ }
+
+ // Get the module packages
+ Set actualPackagesSet = (Set) packagesMethod.invoke( moduleDescriptor );
+ Set expectedPackagesSet = new HashSet<>( Arrays.asList( expectedPackages ) );
+
+ assertEquals( expectedMainClass, actualMainClass );
+ assertEquals( expectedVersion, actualVersion );
+ assertEquals( expectedPackagesSet, actualPackagesSet );
+ }
+
+ /*
+ * Returns true if the current version of Java does support modules.
+ */
+ private boolean modulesAreSupported()
+ {
+ try
+ {
+ Class.forName( "java.lang.module.ModuleDescriptor" );
+ }
+ catch ( ClassNotFoundException e )
+ {
+ return false;
+ }
+
+ return true;
+ }
+
+}
diff --git a/src/test/resources/java-classes/com/example/app/Main.class b/src/test/resources/java-classes/com/example/app/Main.class
new file mode 100644
index 000000000..5deea9cf3
Binary files /dev/null and b/src/test/resources/java-classes/com/example/app/Main.class differ
diff --git a/src/test/resources/java-classes/com/example/resources/test.properties b/src/test/resources/java-classes/com/example/resources/test.properties
new file mode 100644
index 000000000..2c2dd0fd9
--- /dev/null
+++ b/src/test/resources/java-classes/com/example/resources/test.properties
@@ -0,0 +1 @@
+ley=value
diff --git a/src/test/resources/java-module-descriptor/module-info.class b/src/test/resources/java-module-descriptor/module-info.class
new file mode 100644
index 000000000..c172254ac
Binary files /dev/null and b/src/test/resources/java-module-descriptor/module-info.class differ
diff --git a/src/test/resources/java-src/REAMDE.md b/src/test/resources/java-src/REAMDE.md
new file mode 100644
index 000000000..2e7cb0e79
--- /dev/null
+++ b/src/test/resources/java-src/REAMDE.md
@@ -0,0 +1,2 @@
+This is the source code for the Java classes
+inside `java-classes` and `java-module` directories.
diff --git a/src/test/resources/java-src/com/example/app/Main.java b/src/test/resources/java-src/com/example/app/Main.java
new file mode 100644
index 000000000..5de2c61fa
--- /dev/null
+++ b/src/test/resources/java-src/com/example/app/Main.java
@@ -0,0 +1,10 @@
+package com.example.app;
+
+public class Main
+{
+
+ public static void main( String[] args )
+ {
+ System.out.println( "Hello from Main" );
+ }
+}
diff --git a/src/test/resources/java-src/module-info.java b/src/test/resources/java-src/module-info.java
new file mode 100644
index 000000000..194dd8422
--- /dev/null
+++ b/src/test/resources/java-src/module-info.java
@@ -0,0 +1,4 @@
+module com.example.app
+{
+ exports com.example.app;
+}