diff --git a/README.md b/README.md index 9efa949d..5b0cc28e 100644 --- a/README.md +++ b/README.md @@ -72,7 +72,7 @@ The following Ant task can be used with either the `maven-antrun-plugin` or an A io.smallrye jandex-maven-plugin - 3.0.0-SNAPSHOT + ${version.jandex} make-index @@ -100,7 +100,7 @@ If you need to process more than one directory of classes, you can specify multi io.smallrye jandex-maven-plugin - 3.0.0-SNAPSHOT + ${version.jandex} make-index @@ -168,6 +168,55 @@ A `groupId` and `artifactId` are mandatory, a `classifier` is optional: ``` +### Usage with Shading + +The Jandex Maven plugin has an additional goal `jandex-jar` that can be used to create an index inside an existing JAR. +This goal is not bound to any phase by default, so you have to configure that manually. + +It is useful together with shading, where the Maven Shade plugin creates a JAR from multiple previously existing JARs. +A shaded JAR may already contain a Jandex index, if at least one of the constituent JARs contains one, but that index is most likely _not_ what you want. +First, it is an unmodified index originating in one of the constituent JARs. +If multiple constituent JARs contain an index, only one of them makes it to the shaded JAR; the others are lost. +Second, during shading, classes may be relocated, so the index data may become stale. + +There is no support for merging existing index files from the original JARs. +Likewise, there is no support for applying class relocations over an existing index. + +In short, if you want a shaded JAR to have a Jandex index, you have to reindex it. + +```xml + + io.smallrye + jandex-maven-plugin + ${version.jandex} + + + uberjar-index + package + + jandex-jar + + + ${project.build.directory}/${project.build.finalName}.jar + + com/example/my/project/**/*.class + + + com/example/**/_private/*.class + + + + + +``` + +If we want to reindex a shaded JAR, we have to make sure that the `jandex-jar` goal of the Jandex Maven plugin executes _later_ than the `shade` goal of the Maven Shade plugin. +In this example, we bind the `jandex-jar` goal to the `package` phase, which is also the phase in which the `shade` goal of the Maven Shade plugin executes by default. +In such case, we have to put the `` element of the Jandex Maven plugin _after_ the `` element of the Maven Shade plugin. + +Remember that if the Jandex Maven plugin operates on a JAR that was produced by the Maven Shade plugin, and if the Maven Shade plugin is configured to perform class relocations, the Jandex Maven plugin operates on already relocated classes. +This is important for configuring includes and excludes correctly. + ## Adding the Jandex API to your Maven project Just add the following to your POM: diff --git a/maven-plugin/src/it/jar/pom.xml b/maven-plugin/src/it/jar/pom.xml new file mode 100644 index 00000000..a65e6536 --- /dev/null +++ b/maven-plugin/src/it/jar/pom.xml @@ -0,0 +1,73 @@ + + + 4.0.0 + + + io.smallrye + smallrye-build-parent + 31 + + + org.jboss.jandex + jandex-maven-plugin-jar + 1.0-SNAPSHOT + + + + org.jboss + jandex + 2.4.0.Final + + + + io.smallrye.common + smallrye-common-annotation + 1.11.0 + + + + + + + org.apache.maven.plugins + maven-shade-plugin + 3.3.0 + + + uberjar + + shade + + + + + + + @project.groupId@ + @project.artifactId@ + @project.version@ + + + uberjar-index + package + + jandex-jar + + + ${project.build.directory}/${project.build.finalName}.jar + + org/jboss/jandex/maven/**/*.class + org/jboss/jandex/MethodParameter*.class + io/smallrye/common/annotation/Experimental.class + + + org/jboss/jandex/MethodParameterTypeTarget.class + + + + + + + + diff --git a/maven-plugin/src/it/jar/src/main/java/org/jboss/jandex/maven/jar/SomeClass.java b/maven-plugin/src/it/jar/src/main/java/org/jboss/jandex/maven/jar/SomeClass.java new file mode 100644 index 00000000..3d6f033b --- /dev/null +++ b/maven-plugin/src/it/jar/src/main/java/org/jboss/jandex/maven/jar/SomeClass.java @@ -0,0 +1,4 @@ +package org.jboss.jandex.maven.jar; + +public class SomeClass { +} diff --git a/maven-plugin/src/it/jar/verify.groovy b/maven-plugin/src/it/jar/verify.groovy new file mode 100644 index 00000000..d34762fc --- /dev/null +++ b/maven-plugin/src/it/jar/verify.groovy @@ -0,0 +1,17 @@ +import java.util.zip.ZipFile +import org.jboss.jandex.IndexReader + +def jarFile = new File(basedir, 'target/jandex-maven-plugin-jar-1.0-SNAPSHOT.jar') +assert jarFile.exists() : "File ${jarFile} does not exist" +assert jarFile.length() > 0 : "File ${jarFile} is empty" + +def jar = new ZipFile(jarFile) +def indexEntry = jar.getEntry("META-INF/jandex.idx") +assert indexEntry != null : "JAR ${jarFile} doesn't contain an index" + +def index = new IndexReader(jar.getInputStream(indexEntry)).read() +assert index.getKnownClasses().size() == 3 : "Index in ${jarFile} does not contain exactly 3 classes" + +assert index.getClassByName("org.jboss.jandex.maven.jar.SomeClass") != null +assert index.getClassByName("org.jboss.jandex.MethodParameterInfo") != null +assert index.getClassByName("io.smallrye.common.annotation.Experimental") != null diff --git a/maven-plugin/src/main/java/org/jboss/jandex/maven/JandexGoal.java b/maven-plugin/src/main/java/org/jboss/jandex/maven/JandexGoal.java index ada39b3e..5173d706 100644 --- a/maven-plugin/src/main/java/org/jboss/jandex/maven/JandexGoal.java +++ b/maven-plugin/src/main/java/org/jboss/jandex/maven/JandexGoal.java @@ -94,7 +94,7 @@ public class JandexGoal extends AbstractMojo { private boolean processDefaultFileSet; /** - * Print verbose output (debug output without needing to enable -X for the whole build) + * Print verbose output (debug output without needing to enable -X for the whole build). */ @Parameter(defaultValue = "false") private boolean verbose; @@ -116,7 +116,7 @@ public class JandexGoal extends AbstractMojo { * Skip execution if set. */ @Parameter(property = "jandex.skip", defaultValue = "false") - private boolean skip = true; + private boolean skip; public void execute() throws MojoExecutionException { if (skip) { diff --git a/maven-plugin/src/main/java/org/jboss/jandex/maven/JandexJarGoal.java b/maven-plugin/src/main/java/org/jboss/jandex/maven/JandexJarGoal.java new file mode 100644 index 00000000..d01beab2 --- /dev/null +++ b/maven-plugin/src/main/java/org/jboss/jandex/maven/JandexJarGoal.java @@ -0,0 +1,181 @@ +package org.jboss.jandex.maven; + +import java.io.File; +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.StandardCopyOption; +import java.util.Enumeration; +import java.util.List; +import java.util.zip.ZipEntry; +import java.util.zip.ZipFile; +import java.util.zip.ZipOutputStream; + +import org.apache.maven.plugin.AbstractMojo; +import org.apache.maven.plugin.MojoExecutionException; +import org.apache.maven.plugins.annotations.Mojo; +import org.apache.maven.plugins.annotations.Parameter; +import org.jboss.jandex.ClassSummary; +import org.jboss.jandex.Index; +import org.jboss.jandex.IndexWriter; +import org.jboss.jandex.Indexer; + +/** + * Generate a Jandex index inside a given JAR. + */ +@Mojo(name = "jandex-jar", threadSafe = true) +public class JandexJarGoal extends AbstractMojo { + /** + * The JAR that should be indexed and inside which the index should be stored. + */ + @Parameter(required = true) + private File jar; + + /** + * Path to the index inside the JAR. Defaults to META-INF/jandex.idx. + */ + @Parameter(defaultValue = "META-INF/jandex.idx") + private String indexName; + + /** + * Names or glob patterns of files in the JAR that should be indexed. + */ + @Parameter + private List includes; + + /** + * Names or glob patterns of files in the JAR that should not be indexed. + * Excludes have priority over includes. + */ + @Parameter + private List excludes; + + @Parameter(defaultValue = "true") + private boolean useDefaultExcludes; + + /** + * Print verbose output (debug output without needing to enable -X for the whole build). + */ + @Parameter(defaultValue = "false") + private boolean verbose; + + /** + * Skip execution if set. + */ + @Parameter(property = "jandex.skip", defaultValue = "false") + private boolean skip; + + public void execute() throws MojoExecutionException { + if (skip) { + getLog().info("Jandex execution skipped"); + return; + } + + if (!jar.isFile()) { + getLog().warn("Skipping, expected JAR does not exist or is not a file: " + jar); + return; + } + + Index index = indexJar(); + + getLog().info("Saving Jandex index into JAR: " + jar); + Path tmp = createTempFile("jandextmp"); + try (ZipFile zip = new ZipFile(jar); + ZipOutputStream out = new ZipOutputStream(Files.newOutputStream(tmp.toFile().toPath()))) { + Enumeration entries = zip.entries(); + while (entries.hasMoreElements()) { + ZipEntry entry = entries.nextElement(); + if (entry.isDirectory() || entry.getName().equals(indexName)) { + continue; + } + + ZipEntry newEntry = new ZipEntry(entry); + // Compression level and format can vary across implementations + if (newEntry.getMethod() != ZipEntry.STORED) { + newEntry.setCompressedSize(-1); + } + out.putNextEntry(newEntry); + try (InputStream in = zip.getInputStream(entry)) { + copy(in, out); + } + } + + out.putNextEntry(new ZipEntry(indexName)); + new IndexWriter(out).write(index); + } catch (IOException e) { + try { + Files.deleteIfExists(tmp); + } catch (IOException e1) { + e.addSuppressed(e1); + } + throw new MojoExecutionException(e.getMessage(), e); + } + + Path originalJar = jar.toPath(); + Path backupJar = createTempFile("jandexbackup"); + + try { + Files.move(originalJar, backupJar, StandardCopyOption.REPLACE_EXISTING); + Files.move(tmp, originalJar); + Files.delete(backupJar); + } catch (IOException e) { + throw new MojoExecutionException(e.getMessage(), e); + } + } + + private Index indexJar() throws MojoExecutionException { + ArchiveScanner scanner = new ArchiveScanner(jar); + scanner.setFilenameComparator(String::compareTo); + if (useDefaultExcludes) { + scanner.addDefaultExcludes(); + } + if (includes != null) { + scanner.setIncludes(includes.toArray(new String[0])); + } + if (excludes != null) { + scanner.setExcludes(excludes.toArray(new String[0])); + } + scanner.scan(); + String[] filesInJar = scanner.getIncludedFiles(); + + Indexer indexer = new Indexer(); + try (ZipFile zip = new ZipFile(jar)) { + for (String file : filesInJar) { + if (file.endsWith(".class")) { + try (InputStream in = zip.getInputStream(zip.getEntry(file))) { + ClassSummary info = indexer.indexWithSummary(in); + if (isVerbose() && info != null) { + getLog().info("Indexed " + info.name() + " (" + info.annotationsCount() + " annotations)"); + } + } + } + } + } catch (IOException e) { + throw new MojoExecutionException(e.getMessage(), e); + } + return indexer.complete(); + } + + private Path createTempFile(String suffix) throws MojoExecutionException { + try { + return Files.createTempFile(jar.toPath().getParent(), jar.getName(), suffix); + } catch (IOException e) { + throw new MojoExecutionException(e.getMessage(), e); + } + } + + private static void copy(InputStream in, OutputStream out) throws IOException { + byte[] buf = new byte[8192]; + int len; + while ((len = in.read(buf)) > 0) { + out.write(buf, 0, len); + } + out.flush(); + } + + private boolean isVerbose() { + return verbose || getLog().isDebugEnabled(); + } +}