diff --git a/README.md b/README.md index adc48f1..707f05e 100644 --- a/README.md +++ b/README.md @@ -12,3 +12,5 @@ The list of components currently reads as follows: * [logging-context-gelf](logging-context-gelf) Context-aware GELF log handler * [logging-context-json](logging-context-json) Context-aware JSON/logstash log formatter * [pb-json](pb-json) Hassle-free conversion from Protobuf to JSON and back +* [emjar](emjar) Class loader and supporting cast for using jar-in-jar embedded archives as part of classpath +* [emjar-maven-plugin](emjar-maven-plugin) Generate EmJar-enabled bundle archives from Maven diff --git a/emjar-maven-plugin/README.md b/emjar-maven-plugin/README.md new file mode 100644 index 0000000..f7d7f97 --- /dev/null +++ b/emjar-maven-plugin/README.md @@ -0,0 +1,82 @@ +emjar-maven-plugin +================== + +Maven Plugin Mojo for building bundling jars that contain dependency artifact jars verbatim. The bundling jars are built to include the [EmJar](../emjar) class loader for allowing use of the embedded jars as parts of the classpath for the main application code. + +### Available parameters + +* finalName + + Final name for generated Java Archive file. + +* explicitOrderings + + Set of explicit orderings for dependency artifacts that contain conflicting entries. + +* mainJar + + Jar file containing main application code. + +* manifestEntries + + Additional Manifest entries to include in top-level jar. + +* outputDirectory + + Output directory for generated jar file. + +* ignoreConflicts + + Ingore jar content conflicts. + + +### Minimal usage example + +```xml + + ... + + com.comoyo.commons + emjar-maven-plugin + 1.0.0 + + + + run + + + + + ... + +``` + +### With explicit ordering constraints + +```xml + + ... + + com.comoyo.commons + emjar-maven-plugin + 1.0.0 + + + + run + + + ${project.artifactId}-${project.version}-${git.commit.id.abbrev} + + + org.slf4j:log4j-over-slf4j + log4j:log4j + + + + + + + ... + +``` diff --git a/emjar-maven-plugin/pom.xml b/emjar-maven-plugin/pom.xml new file mode 100644 index 0000000..e032218 --- /dev/null +++ b/emjar-maven-plugin/pom.xml @@ -0,0 +1,89 @@ + + + + 4.0.0 + + + commons + com.comoyo + 1.0-SNAPSHOT + + + emjar-maven-plugin -- for building executable embedded jar files + com.comoyo.commons + emjar-maven-plugin + + https://github.com/comoyo/commons/emjar-maven-plugin + maven-plugin + + + 3 + + + + + com.google.guava + guava + ${google.guava.version} + + + + org.apache.maven + maven-core + 3.0.5 + + + + com.comoyo.commons + emjar + ${project.version} + + + + junit + junit + 4.11 + test + + + + + + + org.apache.maven.plugins + maven-plugin-plugin + 3.2 + + emjar + true + + + + mojo-descriptor + + descriptor + + + + help-goal + + helpmojo + + + + + + + org.apache.maven.plugins + maven-compiler-plugin + 2.3.2 + + 1.7 + 1.7 + + + + + diff --git a/emjar-maven-plugin/src/main/java/com/comoyo/maven/plugins/emjar/EmJarMojo.java b/emjar-maven-plugin/src/main/java/com/comoyo/maven/plugins/emjar/EmJarMojo.java new file mode 100644 index 0000000..69b9109 --- /dev/null +++ b/emjar-maven-plugin/src/main/java/com/comoyo/maven/plugins/emjar/EmJarMojo.java @@ -0,0 +1,495 @@ +package com.comoyo.maven.plugins.emjar; + +import com.comoyo.emjar.Boot; +import com.google.common.io.ByteStreams; + +import org.apache.maven.artifact.Artifact; +import org.apache.maven.artifact.repository.ArtifactRepository; +import org.apache.maven.artifact.resolver.ArtifactResolutionRequest; +import org.apache.maven.artifact.resolver.ArtifactResolutionResult; +import org.apache.maven.plugin.AbstractMojo; +import org.apache.maven.plugin.MojoExecutionException; +import org.apache.maven.plugin.descriptor.PluginDescriptor; +import org.apache.maven.repository.RepositorySystem; + +import java.io.File; +import java.io.FileInputStream; +import java.io.FileOutputStream; +import java.io.IOException; +import java.io.InputStream; +import java.nio.channels.FileChannel; +import java.util.Arrays; +import java.util.Collection; +import java.util.Enumeration; +import java.util.HashMap; +import java.util.HashSet; +import java.util.Iterator; +import java.util.LinkedList; +import java.util.List; +import java.util.ListIterator; +import java.util.Map; +import java.util.Set; +import java.util.jar.Attributes; +import java.util.jar.JarEntry; +import java.util.jar.JarFile; +import java.util.jar.JarOutputStream; +import java.util.jar.Manifest; +import java.util.zip.CRC32; +import java.util.zip.ZipEntry; + +import static java.util.jar.Attributes.Name; + +/** + * Maven Plugin Mojo for building dependencies-included bundle jars + * that embed nested application and library jars. + * + * @goal run + * @phase package + * @requiresProject + * @requiresDependencyResolution runtime + */ +public class EmJarMojo + extends AbstractMojo +{ + /** + * Plugin descriptor. + * + * @component role="org.apache.maven.plugin.descriptor.PluginDescriptor" + * @required + * @readonly + */ + private PluginDescriptor pluginDescriptor; + + /** + * Used to look up Artifacts in the remote repository. + * + * @component role="org.apache.maven.repository.RepositorySystem" + * @required + * @readonly + */ + protected RepositorySystem repositorySystem; + + /** + * List of Remote Repositories used by the resolver + * + * @parameter property="project.remoteArtifactRepositories" + * @readonly + * @required + */ + protected List remoteRepositories; + + /** + * @parameter default-value="${project.artifacts}" + * @required + * @readonly + */ + private Collection artifacts; + + + /** + * Jar containing main application code. + * + * @parameter + * property="mainJar" + * default-value="${project.build.directory}/${project.build.finalName}.jar" + * @required + */ + private File mainJar; + + /** + * Output directory for generated jar file. + * + * @parameter + * property="outputDirectory" + * default-value="${project.build.directory}" + */ + private File outputDirectory; + + /** + * Final name for generated jar file. + * + * @parameter + * property="finalName" + * default-value="${project.artifactId}-${project.version}" + */ + private String finalName; + + /** + * Set of explicit orderings for dependency artifacts that contain + * conflicting entries. + * + * @parameter + * property="explicitOrderings" + */ + private Ordering[] explicitOrderings; + + /** + * Ingore JAR content conflicts. + * + * @parameter + * property="ignoreConflicts" + * default-value="false" + */ + private boolean ignoreConflicts; + + /** + * Additional Manifest entries to include in top-level JAR. + * + * @parameter + * property="manifestEntries" + */ + private Map manifestEntries; + + + private static final String CREATED_BY = "Created-By"; + private static final int CHUNK_SIZE = 16 * 1024; + + private final Map>> conflicts = new HashMap<>(); + private final Map> seen = new HashMap<>(); + + private static String desc( + final Artifact artifact) + { + return artifact.getGroupId() + ":" + + artifact.getArtifactId() + ":" + + artifact.getVersion(); + } + + private static boolean matches( + final String id, + final Artifact artifact) + { + final String[] parts = id.split(":"); + return parts.length == 2 + && artifact.getGroupId().equals(parts[0]) + && artifact.getArtifactId().equals(parts[1]); + } + + /** + * Record that artifacts {@code one} and {@code two} both contain + * the entry {@code name}. + */ + protected void addConflict( + final Artifact one, + final Artifact two, + final String name) + { + Map> oneConflicts = conflicts.get(one); + if (oneConflicts == null) { + oneConflicts = new HashMap<>(); + conflicts.put(one, oneConflicts); + } + Set files = oneConflicts.get(two); + if (files == null) { + files = new HashSet<>(); + oneConflicts.put(two, files); + } + files.add(name); + } + + /** + * Record that all artifacts in {@code list} contain the entry + * {@code name}. + */ + protected void addConflicts( + final Artifact[] list, + final String name) + { + for (int i = 0; i < list.length; i++) { + for (int j = i+1; j < list.length; j++) { + addConflict(list[i], list[j], name); + addConflict(list[j], list[i], name); + } + } + } + + /** + * Return any entries common to artifacts {@code one} and {@code two}. + */ + protected Set getConflicts( + final Artifact one, + final Artifact two) + { + Map> oneConflicts = conflicts.get(one); + if (oneConflicts == null) { + return null; + } + return oneConflicts.get(two); + } + + /** + * Warn user that artifacts {@code one} and {@code two} both + * contain the entries {@code files}. + */ + protected void reportConflict( + final Artifact one, + final Artifact two, + final Set files) + { + getLog().warn( + desc(one) + " and " + desc(two) + " contain " + files.size() + + " common entries, no explicit resolution was given"); + int shown = 0; + for (String file : files) { + if (shown >= 10) { + getLog().warn(" ..."); + break; + } + getLog().warn(" " + file); + shown++; + } + } + + /** + * Get artifact containing the EmJar class loader itself. + */ + private Artifact getEmJarArtifact() + throws MojoExecutionException + { + final Artifact artifact + = repositorySystem.createArtifact( + pluginDescriptor.getGroupId(), + "emjar", + pluginDescriptor.getVersion(), + "jar"); + + getLog().info("Using emjar " + artifact); + ArtifactResolutionRequest request = new ArtifactResolutionRequest() + .setArtifact(artifact) + .setRemoteRepositories(remoteRepositories); + ArtifactResolutionResult result = repositorySystem.resolve(request); + if (!result.isSuccess()) { + throw new MojoExecutionException( + "Unable to resolve dependency on EmJar loader artifact, sorry: " + + result.toString()); + } + + Set artifacts = result.getArtifacts(); + if (artifacts.size() != 1) { + throw new MojoExecutionException( + "Unexpected number of artifacts returned when resolving EmJar loader (" + artifacts.size() + ")"); + } + return artifacts.iterator().next(); + } + + /** + * Add the (jar) file {@code inner} to the {@code jar} archive, + * under the directory {@code dirPrefix}. + */ + private void addJarToJarStream( + final JarOutputStream jar, + final File inner, + final String dirPrefix) + throws IOException + { + final FileInputStream is = new FileInputStream(inner); + final FileChannel ch = is.getChannel(); + final CRC32 crc = new CRC32(); + final byte[] buf = new byte[CHUNK_SIZE]; + while (true) { + final int read = is.read(buf, 0, CHUNK_SIZE); + if (read <= 0) { + break; + } + crc.update(buf, 0, read); + } + + final ZipEntry entry = new ZipEntry(dirPrefix + "/" + inner.getName()); + entry.setMethod(ZipEntry.STORED); + entry.setSize(ch.size()); + entry.setCompressedSize(ch.size()); + entry.setCrc(crc.getValue()); + + ch.position(0); + jar.putNextEntry(entry); + ByteStreams.copy(is, jar); + jar.closeEntry(); + } + + /** + * Return version of {@code artifacts} ordered according to the + * constraints given in {@code orderings}. Report any conflicts + * that are not unambiguously resolved by given ordering + * constraints. + */ + protected List orderArtifacts( + final Collection artifacts, + final Ordering[] orderings) + throws MojoExecutionException + { + final LinkedList ordered = new LinkedList<>(); + final LinkedList toOrder = new LinkedList<>(artifacts); + final LinkedList toApply = new LinkedList<>(Arrays.asList(orderings)); + + while (!toOrder.isEmpty()) { + final Iterator remaining = toOrder.descendingIterator(); + boolean progress = false; + artifact_insert: + while (remaining.hasNext()) { + final Artifact toPlace = remaining.next(); + int placeAt = 0; + for (Ordering order : toApply) { + if (!matches(order.getOver(), toPlace)) { + continue; + } + int placeAfter = -1; + final ListIterator placed = ordered.listIterator(); + while (placed.hasNext()) { + final Artifact candidate = placed.next(); + if (matches(order.getPrefer(), candidate)) { + placeAfter = placed.previousIndex(); + break; + } + } + if (placeAfter < 0) { + // preferred artifact not inserted yet + continue artifact_insert; + } + placeAt = Math.max(placeAfter + 1, placeAt); + } + ordered.add(placeAt, toPlace); + final Iterator constraint = toApply.iterator(); + while (constraint.hasNext()) { + if (matches(constraint.next().getOver(), toPlace)) { + constraint.remove(); + } + } + remaining.remove(); + progress = true; + + // Artifacts are placed as early as possible after constrains + // have been applied; i.e there are no constraints regulating + // the relationship between placed artifacts and others + // occurring later in the list. Report all conflicts in this + // space. + if (!ignoreConflicts) { + final ListIterator allAfter + = ordered.listIterator(placeAt + 1); + while (allAfter.hasNext()) { + final Artifact after = allAfter.next(); + Set files = getConflicts(toPlace, after); + if (files != null && !files.isEmpty()) { + reportConflict(toPlace, after, files); + } + } + } + } + if (!progress) { + String remains = ""; + for (Artifact artifact : toOrder) { + remains = remains + " " + desc(artifact); + } + throw new MojoExecutionException( + "Unable to order remaining artifacts:" + remains + + ", conflicting ordering directives?"); + } + } + if (!toApply.isEmpty()) { + getLog().warn("Unused ordering directives:"); + for (Ordering ordering : toApply) { + getLog().warn(" prefer " + ordering.getPrefer() + + " over " + ordering.getOver()); + } + } + return ordered; + } + + /** + * Scan an artifact (jar) file for entries; record all entries + * that conflict with the contents of already scanned artifacts. + */ + private void scanArtifact( + final Artifact artifact) + throws IOException + { + final File file = artifact.getFile(); + final JarFile jar = new JarFile(file); + final Enumeration entries = jar.entries(); + while (entries.hasMoreElements()) { + final JarEntry entry = entries.nextElement(); + final String name = entry.getName(); + if (entry.isDirectory() + || name.startsWith("META-INF") + || !name.contains("/")) + { + continue; + } + Set others = seen.get(name); + if (others == null) { + others = new HashSet<>(); + seen.put(name, others); + } + else { + for (Artifact other : others) { + addConflict(artifact, other, name); + addConflict(other, artifact, name); + } + } + others.add(artifact); + } + jar.close(); + } + + /** + * Plugin invocation point. + */ + @Override + public void execute() + throws MojoExecutionException + { + try { + final File outFile = new File(outputDirectory, finalName + "-emjar.jar"); + final JarFile main = new JarFile(mainJar); + final Attributes mainAttrs = main.getManifest().getMainAttributes(); + final Manifest manifest = new Manifest(); + final Attributes bootAttrs = manifest.getMainAttributes(); + bootAttrs.put(Name.MANIFEST_VERSION, "1.0"); + bootAttrs.putValue(CREATED_BY, + "EmJar Maven Plugin " + pluginDescriptor.getVersion()); + bootAttrs.put(Name.MAIN_CLASS, Boot.class.getName()); + if (mainAttrs.containsKey(Name.MAIN_CLASS)) { + bootAttrs.putValue(Boot.EMJAR_MAIN_CLASS_ATTR, + mainAttrs.getValue(Name.MAIN_CLASS)); + } + main.close(); + if (manifestEntries != null) { + for (Map.Entry entry : manifestEntries.entrySet()) { + bootAttrs.putValue(entry.getKey(), entry.getValue()); + } + } + final JarOutputStream jar + = new JarOutputStream(new FileOutputStream(outFile, false), manifest); + + final Artifact emjar = getEmJarArtifact(); + final JarFile loader = new JarFile(emjar.getFile()); + final Enumeration entries = loader.entries(); + while (entries.hasMoreElements()) { + ZipEntry entry = entries.nextElement(); + if (!entry.getName().endsWith(".class")) { + continue; + } + final InputStream is = loader.getInputStream(entry); + jar.putNextEntry(entry); + ByteStreams.copy(is, jar); + jar.closeEntry(); + } + loader.close(); + + addJarToJarStream(jar, mainJar, "main"); + seen.clear(); + if (!ignoreConflicts) { + for (Artifact artifact : artifacts) { + scanArtifact(artifact); + } + } + final List ordered = orderArtifacts(artifacts, explicitOrderings); + for (Artifact artifact : ordered) { + addJarToJarStream(jar, artifact.getFile(), "lib"); + } + jar.close(); + } + catch (IOException e) { + throw new MojoExecutionException("Unable to generate EmJar archive", e); + } + } +} diff --git a/emjar-maven-plugin/src/main/java/com/comoyo/maven/plugins/emjar/Ordering.java b/emjar-maven-plugin/src/main/java/com/comoyo/maven/plugins/emjar/Ordering.java new file mode 100644 index 0000000..e8f0de4 --- /dev/null +++ b/emjar-maven-plugin/src/main/java/com/comoyo/maven/plugins/emjar/Ordering.java @@ -0,0 +1,25 @@ +package com.comoyo.maven.plugins.emjar; + +public class Ordering +{ + private String prefer; + private String over; + + public Ordering() {} + + public Ordering(String prefer, String over) + { + this.prefer = prefer; + this.over = over; + } + + public String getPrefer() + { + return prefer; + } + + public String getOver() + { + return over; + } +} diff --git a/emjar-maven-plugin/src/test/java/com/comoyo/maven/plugins/emjar/EmJarMojoTest.java b/emjar-maven-plugin/src/test/java/com/comoyo/maven/plugins/emjar/EmJarMojoTest.java new file mode 100644 index 0000000..deb7112 --- /dev/null +++ b/emjar-maven-plugin/src/test/java/com/comoyo/maven/plugins/emjar/EmJarMojoTest.java @@ -0,0 +1,174 @@ +package com.comoyo.maven.plugins.emjar; + +import org.apache.maven.artifact.Artifact; +import org.apache.maven.artifact.DefaultArtifact; +import org.apache.maven.plugin.MojoExecutionException; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.junit.runners.JUnit4; + +import static com.google.common.collect.Collections2.*; +import static org.junit.Assert.*; + +import java.util.ArrayList; +import java.util.List; +import java.util.Set; +import java.util.concurrent.atomic.AtomicBoolean; + +@RunWith(JUnit4.class) +public class EmJarMojoTest +{ + private static final Artifact ART_A = testArtifact("idA"); + private static final Artifact ART_B = testArtifact("idB"); + private static final Artifact ART_C = testArtifact("idC"); + private static final Artifact ART_D = testArtifact("idD"); + private static final Artifact ART_E = testArtifact("idE"); + + private static final List ALL_ARTS = new ArrayList() {{ + add(ART_A); + add(ART_B); + add(ART_C); + add(ART_D); + add(ART_E); + }}; + + private static Artifact testArtifact(String id) + { + return new DefaultArtifact("group", id, "0.0", "compile", "jar", "", null); + } + + private static String toSpec(Artifact artifact) + { + return artifact.getGroupId() + ":" + artifact.getArtifactId(); + } + + @Test + public void testMissingOrdering() + throws Exception + { + final AtomicBoolean seen1 = new AtomicBoolean(false); + final AtomicBoolean seen2 = new AtomicBoolean(false); + final EmJarMojo mojo = new EmJarMojo() + { + @Override + protected void reportConflict(Artifact one, Artifact two, Set files) + { + if (one.equals(ART_A) && two.equals(ART_E) && files.contains("a")) { + seen1.set(true); + } + else if (one.equals(ART_D) && two.equals(ART_E) && files.contains("a")) { + seen2.set(true); + } + else { + fail("Unexpected conflict between " + toSpec(one) + " and " + + toSpec(two) + " on " + files); + } + } + }; + mojo.addConflicts(new Artifact[]{ART_A, ART_D, ART_E}, "a"); + + final Ordering[] orderings = new Ordering[]{ + new Ordering(toSpec(ART_A), toSpec(ART_B)), + new Ordering(toSpec(ART_B), toSpec(ART_C)), + new Ordering(toSpec(ART_C), toSpec(ART_D)) + }; + + final List ordered = mojo.orderArtifacts(ALL_ARTS, orderings); + + final List knownOrdered = new ArrayList() {{ + add(ART_A); + add(ART_B); + add(ART_C); + add(ART_D); + add(ART_E); + }}; + + assertEquals("Returned artifact list was not ordered as expected", knownOrdered, ordered); + assertTrue("Conflict between " + toSpec(ART_A) + " and " + toSpec(ART_E) + " on `a` not seen", + seen1.get()); + assertTrue("Conflict between " + toSpec(ART_D) + " and " + toSpec(ART_E) + " on `a` not seen", + seen2.get()); + } + + @Test + public void testPartialOrdering() + throws Exception + { + for (List perm : permutations(ALL_ARTS)) { + final EmJarMojo mojo = new EmJarMojo(); + final Ordering[] orderings = new Ordering[]{ + new Ordering(toSpec(ART_A), toSpec(ART_B)) + }; + + final List ordered = mojo.orderArtifacts(perm, orderings); + assertTrue(toSpec(ART_A) + " was not ordered before " + toSpec(ART_B) + + " for input " + perm, + ordered.indexOf(ART_A) < ordered.indexOf(ART_B)); + } + } + + @Test + public void testFullOrdering() + throws Exception + { + for (List perm : permutations(ALL_ARTS)) { + final EmJarMojo mojo = new EmJarMojo() + { + @Override + protected void reportConflict(Artifact one, Artifact two, Set files) + { + fail("Unexpected conflict between " + toSpec(one) + " and " + toSpec(two) + + " on " + files); + } + }; + mojo.addConflicts(new Artifact[]{ART_A, ART_B}, "b"); + mojo.addConflicts(new Artifact[]{ART_B, ART_C}, "c"); + mojo.addConflicts(new Artifact[]{ART_C, ART_D}, "d"); + mojo.addConflicts(new Artifact[]{ART_A, ART_D}, "a"); + mojo.addConflicts(new Artifact[]{ART_A, ART_E}, "a"); + + final Ordering[] orderings = new Ordering[]{ + new Ordering(toSpec(ART_A), toSpec(ART_B)), + new Ordering(toSpec(ART_B), toSpec(ART_C)), + new Ordering(toSpec(ART_C), toSpec(ART_D)), + new Ordering(toSpec(ART_A), toSpec(ART_E)), + new Ordering(toSpec(ART_E), toSpec(ART_B)) + }; + + final List ordered = mojo.orderArtifacts(perm, orderings); + final List knownOrdered = new ArrayList() {{ + add(ART_A); + add(ART_E); + add(ART_B); + add(ART_C); + add(ART_D); + }}; + assertEquals( + "Returned artifact list was not ordered as expected for input " + + perm, knownOrdered, ordered); + } + } + + @Test + public void testIllegalOrdering() + throws Exception + { + for (List perm : permutations(ALL_ARTS)) { + final EmJarMojo mojo = new EmJarMojo(); + final Ordering[] orderings = new Ordering[]{ + new Ordering(toSpec(ART_A), toSpec(ART_B)), + new Ordering(toSpec(ART_B), toSpec(ART_A)) + }; + + try { + mojo.orderArtifacts(perm, orderings); + } + catch (MojoExecutionException e) { + if (e.getMessage().contains("Unable to order")) { + return; + } + } + fail("Illegal ordering was not caught"); + } + } +} diff --git a/emjar/pom.xml b/emjar/pom.xml new file mode 100644 index 0000000..d6b1c52 --- /dev/null +++ b/emjar/pom.xml @@ -0,0 +1,51 @@ + + + + 4.0.0 + + + commons + com.comoyo + 1.0-SNAPSHOT + + + emjar -- for loading dependencies from embedded jar files + com.comoyo.commons + emjar + + https://github.com/comoyo/commons/emjar + jar + + + + + org.apache.maven.plugins + maven-compiler-plugin + 2.3.2 + + 1.7 + 1.7 + + + + org.apache.maven.plugins + maven-surefire-plugin + 2.12 + + false + + + + + + + + junit + junit + 4.11 + test + + + diff --git a/emjar/src/main/java/com/comoyo/emjar/Boot.java b/emjar/src/main/java/com/comoyo/emjar/Boot.java new file mode 100644 index 0000000..13587a6 --- /dev/null +++ b/emjar/src/main/java/com/comoyo/emjar/Boot.java @@ -0,0 +1,105 @@ +package com.comoyo.emjar; + +import java.io.IOException; +import java.io.InputStream; +import java.io.InputStreamReader; +import java.lang.reflect.Field; +import java.lang.reflect.Method; +import java.net.URL; +import java.util.Enumeration; +import java.util.Properties; +import java.util.jar.Attributes; +import java.util.jar.Manifest; + +/** + * Dispatching main class that allows for java -jar + * bundle.jar invocation of bundled jars. To use: + * + *
    + *
  • Configure the bundle jar file's manifest attribute + * Main-Class as + * com.comoyo.emjar.Boot.
  • + * + *
  • Specify the actual main class using the + * EmJar-Main-Class manifest attribute + * or the emjar.main.class + * property.
  • + *
+ * + *

+ * (This class relies on reflection and some slightly questionable + * practices to hook the EmJar class loader into the system. See also + * {@link EmJarClassLoader} for an approach using explicit + * configuration that avoids this.) + * + */ +public class Boot +{ + public final static String EMJAR_MAIN_CLASS_PROP = "emjar.main.class"; + public final static String EMJAR_MAIN_CLASS_ATTR = "EmJar-Main-Class"; + public final static String EMJAR_SYSTEM_PROPS_PROP = "emjar.system.properties"; + public final static String EMJAR_SYSTEM_PROPS_ATTR = "EmJar-System-Properties"; + + public static void main(String[] args) + throws Exception + { + final EmJarClassLoader loader = new EmJarClassLoader(ClassLoader.getSystemClassLoader()); + Thread.currentThread().setContextClassLoader(loader); + + try { + // Make a best-effort attempt to update the system class + // loader. This is required to allow e.g LogManager to + // find classes inside embedded jars when parsing files + // given using the java.util.logging.config.file property. + final Field systemClassLoader = ClassLoader.class.getDeclaredField("scl"); + systemClassLoader.setAccessible(true); + systemClassLoader.set(null, loader); + } + catch (NoSuchFieldException e) { + // Unable to set system class loader; continuing anyway + } + + final String systemPropsName + = System.getProperty(EMJAR_SYSTEM_PROPS_PROP, + getManifestAttribute(EMJAR_SYSTEM_PROPS_ATTR)); + if (systemPropsName != null) { + final Properties props = System.getProperties(); + final InputStream is = loader.getResourceAsStream(systemPropsName); + if (is != null) { + props.load(new InputStreamReader(is)); + is.close(); + } + } + final String mainClassName + = System.getProperty(EMJAR_MAIN_CLASS_PROP, + getManifestAttribute(EMJAR_MAIN_CLASS_ATTR)); + if (mainClassName == null) { + throw new RuntimeException( + "No main class specified using " + + EMJAR_MAIN_CLASS_PROP + " or " + EMJAR_MAIN_CLASS_ATTR); + } + + final Class mainClass + = Class.forName(mainClassName, false, loader); + final Method mainMethod + = mainClass.getDeclaredMethod("main", new Class[]{String[].class}); + mainMethod.invoke(null, new Object[]{args}); + } + + private static String getManifestAttribute(String key) + throws IOException + { + final Enumeration manifests + = Boot.class.getClassLoader().getResources("META-INF/MANIFEST.MF"); + while (manifests.hasMoreElements()) { + final URL url = manifests.nextElement(); + final Manifest manifest = new Manifest(url.openStream()); + final Attributes attributes = manifest.getMainAttributes(); + final String value = attributes.getValue(key); + if (value != null) { + return value; + } + } + return null; + } +} diff --git a/emjar/src/main/java/com/comoyo/emjar/ByteBufferBackedInputStream.java b/emjar/src/main/java/com/comoyo/emjar/ByteBufferBackedInputStream.java new file mode 100644 index 0000000..0137f18 --- /dev/null +++ b/emjar/src/main/java/com/comoyo/emjar/ByteBufferBackedInputStream.java @@ -0,0 +1,37 @@ +package com.comoyo.emjar; + +import java.io.IOException; +import java.io.InputStream; +import java.nio.ByteBuffer; + +public class ByteBufferBackedInputStream + extends InputStream +{ + private final ByteBuffer buf; + + public ByteBufferBackedInputStream(ByteBuffer buf) + { + this.buf = buf; + } + + public int read() + throws IOException + { + if (!buf.hasRemaining()) { + return -1; + } + return buf.get() & 0xff; + } + + public int read(byte[] bytes, int off, int len) + throws IOException + { + if (!buf.hasRemaining()) { + return -1; + } + + len = Math.min(len, buf.remaining()); + buf.get(bytes, off, len); + return len; + } +} diff --git a/emjar/src/main/java/com/comoyo/emjar/EmJarClassLoader.java b/emjar/src/main/java/com/comoyo/emjar/EmJarClassLoader.java new file mode 100644 index 0000000..e2a0e28 --- /dev/null +++ b/emjar/src/main/java/com/comoyo/emjar/EmJarClassLoader.java @@ -0,0 +1,186 @@ +package com.comoyo.emjar; + +import java.net.JarURLConnection; +import java.net.URL; +import java.net.URLClassLoader; +import java.net.URLConnection; +import java.net.URLStreamHandler; +import java.net.URLStreamHandlerFactory; +import java.util.ArrayList; +import java.util.Enumeration; +import java.util.Map; +import java.util.Properties; +import java.util.concurrent.ConcurrentHashMap; +import java.util.jar.JarEntry; +import java.util.jar.JarFile; +import java.io.File; +import java.io.IOException; + +/** + * Class loader able to use embedded jars as part of the classpath. + * All jars specified in the original classpath are opened and + * inspected, any jar files found within will be added to the + * classpath. If the embedded jar files are stored using the ZIP + * archiving method stored (i.e no compression), they will be + * mapped directly and individual classes/elements can be loaded + * on-demand. For compressed embedded jars, initial access will + * preload the contents of all contained elements. + * + *

+ * The EmJar class loader can be invoked by setting the system + * property java.system.class.loader to + * the value + * com.comoyo.emjar.EmJarClassLoader, + * e.g by using the -D flag to the + * java executable. (The emjar classes + * must for obvious reasons be stored directly inside the bundle jar; + * i.e not within an embedded jar.) + * + *

+ * For a less manual approach that embeds all configuration in the + * bundled jar, see {@link Boot}. + * + *

+ * (To ensure that embedded jars are stored as-is and not compressed, + * use e.g maven-assembly-plugin version + * 2.4 or higher, and keep the + * recompressZippedFiles configuration + * option set to false (the default as of version 2.4)). + * + */ +public class EmJarClassLoader + extends URLClassLoader +{ + private final static HandlerFactory factory = new HandlerFactory(); + + static { + try { + ClassLoader.registerAsParallelCapable(); + } + catch (Throwable ignored) { + } + } + + public EmJarClassLoader() + { + super(getClassPath(), null, factory); + } + + public EmJarClassLoader(ClassLoader parent) + { + super(getClassPath(), parent, factory); + } + + private static URL[] getClassPath() + { + final Properties props = System.getProperties(); + final String classPath = props.getProperty("java.class.path"); + final String pathSep = props.getProperty("path.separator"); + final String fileSep = props.getProperty("file.separator"); + final String userDir = props.getProperty("user.dir"); + + final ArrayList urls = new ArrayList<>(); + for (String elem : classPath.split(pathSep)) { + if (!elem.endsWith(".jar")) { + continue; + } + final String full = elem.startsWith(fileSep) ? elem : userDir + fileSep + elem; + try { + urls.add(new URL("file:" + full)); + final JarFile jar = new JarFile(elem); + Enumeration embedded = jar.entries(); + while (embedded.hasMoreElements()) { + final JarEntry entry = embedded.nextElement(); + if (entry.getName().endsWith(".jar")) { + urls.add(new URL("jar:file:" + full + "!/" + entry.getName())); + } + } + jar.close(); + } + catch (IOException e) { + e.printStackTrace(); + } + } + return urls.toArray(new URL[0]); + } + + @Override + public Class loadClass(String name) + throws ClassNotFoundException + { + return super.loadClass(name); + } + + private static class HandlerFactory + implements URLStreamHandlerFactory + { + private final Handler handler = new Handler(); + + @Override + public URLStreamHandler createURLStreamHandler(String protocol) + { + return "jar".equals(protocol) ? handler : null; + } + } + + private static class Handler + extends URLStreamHandler + { + private final Map connections + = new ConcurrentHashMap<>(); + private final Map>> rootJars + = new ConcurrentHashMap<>(); + + @Override + protected URLConnection openConnection(URL url) + throws IOException + { + final String spec = url.getFile(); + if (!spec.startsWith("jar:file:")) { + throw new IOException("Unable to handle " + spec); + } + JarURLConnection conn = connections.get(spec); + if (conn == null) { + synchronized (connections) { + conn = connections.get(spec); + if (conn == null) { + final int i = spec.indexOf("!/"); + final int j = spec.indexOf("!/", i + 1); + if (i < 0 || j < 0) { + throw new IOException("Unable to parse " + spec); + } + final String root = spec.substring(9, i); + final String nested = spec.substring(i + 2, j); + final String entry = spec.substring(j + 2); + + Map> rootJar + = rootJars.get(root); + if (rootJar == null) { + synchronized (rootJars) { + rootJar = rootJars.get(root); + if (rootJar == null) { + final ZipScanner scanner + = new ZipScanner(new File(root)); + rootJar = scanner.scan(); + rootJars.put(root, rootJar); + } + } + } + final Map descriptors + = rootJar.get(nested); + if (descriptors != null) { + conn = new OndemandEmbeddedJar.Connection( + new URL(spec), root, descriptors, entry); + } + else { + conn = new PreloadedEmbeddedJar.Connection( + new URL(spec), root, nested, entry); + } + connections.put(spec, conn); + } + } + } + return conn; + } + } +} diff --git a/emjar/src/main/java/com/comoyo/emjar/IteratorBackedEnumeration.java b/emjar/src/main/java/com/comoyo/emjar/IteratorBackedEnumeration.java new file mode 100644 index 0000000..1f5ca1c --- /dev/null +++ b/emjar/src/main/java/com/comoyo/emjar/IteratorBackedEnumeration.java @@ -0,0 +1,25 @@ +package com.comoyo.emjar; + +import java.util.Enumeration; +import java.util.Iterator; + +public class IteratorBackedEnumeration + implements Enumeration +{ + private final Iterator it; + + public IteratorBackedEnumeration(Iterator it) + { + this.it = it; + } + + public boolean hasMoreElements() + { + return it.hasNext(); + } + + public E nextElement() + { + return it.next(); + } +} diff --git a/emjar/src/main/java/com/comoyo/emjar/OndemandEmbeddedJar.java b/emjar/src/main/java/com/comoyo/emjar/OndemandEmbeddedJar.java new file mode 100644 index 0000000..bf2c038 --- /dev/null +++ b/emjar/src/main/java/com/comoyo/emjar/OndemandEmbeddedJar.java @@ -0,0 +1,205 @@ +package com.comoyo.emjar; + +import java.io.ByteArrayInputStream; +import java.io.IOException; +import java.io.InputStream; +import java.net.JarURLConnection; +import java.net.MalformedURLException; +import java.net.URL; +import java.nio.ByteBuffer; +import java.util.Enumeration; +import java.util.HashMap; +import java.util.Map; +import java.util.jar.JarEntry; +import java.util.jar.JarFile; +import java.util.jar.Manifest; +import java.util.zip.ZipEntry; +import java.util.zip.ZipInputStream; + +/** + * Representation of nested jar that uses on-demand loading of inner + * jar contents. The inner jar must be stored uncompressed inside the + * outer bundle; the individual inner jar entries may use any + * compression method. + * + */ +public class OndemandEmbeddedJar +{ + public static class Descriptor + { + private final ByteBuffer map; + private final String name; + private final int offset; + private final int size; + + public Descriptor(String name, ByteBuffer map, int offset, int size) + { + this.name = name; + this.map = map; + this.offset = offset; + this.size = size; + } + + public String getName() + { + return name; + } + + public ByteBuffer getMap() + { + final ByteBuffer constrained = map.duplicate(); + constrained.position(offset); + return constrained.slice(); + } + + public int getSize() + { + return size; + } + + public String toString() + { + return "{name:" + name + + ", map:" + map + + ", offset:" + offset + + ", size:" + size + "}"; + } + } + + public static class Connection + extends JarURLConnection + { + private final String root; + private final Map descriptors; + private final String entry; + private JarFile jarFile = null; + + public Connection( + final URL url, + final String root, + final Map descriptors, + final String entry) + throws MalformedURLException + { + super(url); + this.root = root; + this.descriptors = descriptors; + this.entry = entry; + } + + @Override + public void connect() + { + } + + @Override + public synchronized JarFile getJarFile() + throws IOException + { + if (jarFile == null) { + jarFile = new FileEntry(root, descriptors); + } + return jarFile; + } + + @Override + public InputStream getInputStream() + throws IOException + { + final JarFile jarFile = getJarFile(); + return jarFile.getInputStream(new ZipEntry(entry)); + } + } + + private static class FileEntry + extends JarFile + { + private Manifest manifest = null; + private final Map entries; + private final Map descriptors; + private final Map contents; + + public FileEntry(String root, Map descriptors) + throws IOException + { + super(root); + this.descriptors = descriptors; + entries = new HashMap<>(descriptors.size()); + contents = new HashMap<>(descriptors.size()); + + for (Map.Entry entry : descriptors.entrySet()) { + entries.put(entry.getKey(), new JarEntry(entry.getValue().getName())); + } + } + + @Override + public Enumeration entries() + { + return new IteratorBackedEnumeration(entries.values().iterator()); + } + + @Override + public ZipEntry getEntry(String name) + { + return getJarEntry(name); + } + + @Override + public InputStream getInputStream(ZipEntry ze) + throws IOException + { + final String name = ze.getName(); + byte[] cont = contents.get(name); + if (cont == null) { + synchronized (contents) { + cont = contents.get(name); + if (cont == null) { + final Descriptor desc = descriptors.get(name); + if (desc == null) { + throw new IOException("Entry does not exist"); + } + final ByteBuffer map = desc.getMap(); + final InputStream raw = new ByteBufferBackedInputStream(map); + final ZipInputStream unzipped = new ZipInputStream(raw); + + unzipped.getNextEntry(); + final int len = desc.getSize(); + int read = 0; + cont = new byte[len]; + while (read < len) { + read += unzipped.read(cont, read, len - read); + } + unzipped.close(); + contents.put(name, cont); + } + } + } + return new ByteArrayInputStream(cont); + } + + @Override + public JarEntry getJarEntry(String name) + { + return entries.get(name); + } + + public Manifest getManifest() { + if (manifest == null) { + try { + final InputStream is + = getInputStream(new ZipEntry("META-INF/MANIFEST.MF")); + manifest = new Manifest(is); + } + catch (IOException e) { + manifest = new Manifest(); + } + } + return manifest; + } + + public int size() + { + return entries.size(); + } + } +} diff --git a/emjar/src/main/java/com/comoyo/emjar/PreloadedEmbeddedJar.java b/emjar/src/main/java/com/comoyo/emjar/PreloadedEmbeddedJar.java new file mode 100644 index 0000000..9e67d4f --- /dev/null +++ b/emjar/src/main/java/com/comoyo/emjar/PreloadedEmbeddedJar.java @@ -0,0 +1,179 @@ +package com.comoyo.emjar; + +import java.io.ByteArrayInputStream; +import java.io.IOException; +import java.io.InputStream; +import java.net.JarURLConnection; +import java.net.MalformedURLException; +import java.net.URL; +import java.util.Arrays; +import java.util.Enumeration; +import java.util.HashMap; +import java.util.Map; +import java.util.jar.JarEntry; +import java.util.jar.JarFile; +import java.util.jar.JarInputStream; +import java.util.jar.Manifest; +import java.util.zip.ZipEntry; + +/** + * Representation of nested jar that preloads all inner jar entries on + * creation. + * + */ +public class PreloadedEmbeddedJar +{ + public static class Connection + extends JarURLConnection + { + private final String root; + private final String nested; + private final String entry; + private JarFile jarFile = null; + + public Connection( + final URL url, + final String root, + final String nested, + final String entry) + throws MalformedURLException + { + super(url); + this.root = root; + this.nested = nested; + this.entry = entry; + } + + @Override + public void connect() + { + } + + @Override + public synchronized JarFile getJarFile() + throws IOException + { + if (jarFile == null) { + jarFile = new FileEntry(root, nested); + } + return jarFile; + } + + @Override + public InputStream getInputStream() + throws IOException + { + final JarFile jarFile = getJarFile(); + return jarFile.getInputStream(new ZipEntry(entry)); + } + } + + private static class FileEntry + extends JarFile + { + private static final int CHUNK_SIZE = 1024 * 16; + private static final int MAX_FILE_SIZE = 1024 * 1024 * 128; + + private final Manifest manifest; + private final Map entries; + private final Map contents; + + public FileEntry(String root, String nested) + throws IOException + { + super(root); + final ZipEntry embedded = super.getEntry(nested); + final InputStream stream = super.getInputStream(embedded); + final JarInputStream jar = new JarInputStream(stream); + + entries = new HashMap<>(); + contents = new HashMap<>(); + manifest = jar.getManifest(); + + byte[] buf = null; + + process_entries: + while (true) { + final JarEntry entry = jar.getNextJarEntry(); + if (entry == null) { + break; + } + final String name = entry.getName(); + final int len = (int) entry.getSize(); + if (len >= 0) { + // Size known in advance, we can allocate contents + // buffer directly. + if (len > MAX_FILE_SIZE) { + jar.closeEntry(); + continue; + } + final byte[] cont = new byte[len]; + int read = 0; + while (read < len) { + read += jar.read(cont, read, len - read); + } + contents.put(name, cont); + } + else { + int size = 0; + if (buf == null) { + buf = new byte[CHUNK_SIZE * 2]; + } + while (true) { + if (size + CHUNK_SIZE > buf.length) { + buf = Arrays.copyOf(buf, buf.length * 2); + } + final int read = jar.read(buf, size, CHUNK_SIZE); + if (read <= 0) { + break; + } + size += read; + if (size > MAX_FILE_SIZE) { + jar.closeEntry(); + continue process_entries; + } + } + contents.put(name, Arrays.copyOf(buf, size)); + } + entries.put(name, entry); + jar.closeEntry(); + } + jar.close(); + } + + @Override + public Enumeration entries() + { + return new IteratorBackedEnumeration<>(entries.values().iterator()); + } + + @Override + public ZipEntry getEntry(String name) { + return getJarEntry(name); + } + + @Override + public InputStream getInputStream(ZipEntry ze) + throws IOException + { + final byte[] cont = contents.get(ze.getName()); + return cont == null ? null : new ByteArrayInputStream(cont); + } + + @Override + public JarEntry getJarEntry(String name) { + return entries.get(name); + } + + @Override + public Manifest getManifest() { + return manifest; + } + + @Override + public int size() + { + return entries.size(); + } + } +} diff --git a/emjar/src/main/java/com/comoyo/emjar/ZipScanner.java b/emjar/src/main/java/com/comoyo/emjar/ZipScanner.java new file mode 100644 index 0000000..e814a0c --- /dev/null +++ b/emjar/src/main/java/com/comoyo/emjar/ZipScanner.java @@ -0,0 +1,219 @@ +package com.comoyo.emjar; + +import java.io.File; +import java.io.IOException; +import java.io.RandomAccessFile; +import java.nio.ByteBuffer; +import java.nio.ByteOrder; +import java.nio.MappedByteBuffer; +import java.nio.channels.FileChannel; +import java.nio.charset.StandardCharsets; +import java.util.HashMap; +import java.util.Map; +import java.util.zip.ZipFile; + +public class ZipScanner +{ + static final int METHOD_STORED = 0; + + static final int ENDCUR = 4; + static final int ENDSTA = 6; + + static final long ZIP64_ENDSIG = 0x06064b50L; // "PK\006\006" + static final long ZIP64_LOCSIG = 0x07064b50L; // "PK\006\007" + static final int ZIP64_ENDHDR = 56; // ZIP64 end header size + static final int ZIP64_LOCHDR = 20; // ZIP64 end loc header size + static final int ZIP64_EXTHDR = 24; // EXT header size + static final int ZIP64_EXTID = 0x0001; // Extra field Zip64 header ID + + static final int ZIP64_ENDLEN = 4; // size of zip64 end of central dir + static final int ZIP64_ENDVEM = 12; // version made by + static final int ZIP64_ENDVER = 14; // version needed to extract + static final int ZIP64_ENDNMD = 16; // number of this disk + static final int ZIP64_ENDDSK = 20; // disk number of start + static final int ZIP64_ENDTOD = 24; // total number of entries on this disk + static final int ZIP64_ENDTOT = 32; // total number of entries + static final int ZIP64_ENDSIZ = 40; // central directory size in bytes + static final int ZIP64_ENDOFF = 48; // offset of first CEN header + static final int ZIP64_ENDEXT = 56; // zip64 extensible data sector + + static final int ZIP64_LOCDSK = 4; // disk number start + static final int ZIP64_LOCOFF = 8; // offset of zip64 end + static final int ZIP64_LOCTOT = 16; // total number of disks + + + private final File file; + private final Map> nestedDescriptors; + + public ZipScanner(File file) + { + this.file = file; + nestedDescriptors = new HashMap<>(); + } + + public Map> scan() + throws IOException + { + final RandomAccessFile raf = new RandomAccessFile(file, "r"); + final FileChannel chan = raf.getChannel(); + if (raf.length() <= Integer.MAX_VALUE) { + final MappedByteBuffer map + = chan.map(FileChannel.MapMode.READ_ONLY, 0, raf.length()); + recurse(map, null); + } + raf.close(); + return nestedDescriptors; + } + + public void recurse( + final ByteBuffer map, + final Map context) + throws IOException + { + final ByteBuffer loc64 = findEocd(map, ZIP64_LOCSIG, ZIP64_LOCHDR); + if (loc64 != null) { + loc64.order(ByteOrder.LITTLE_ENDIAN); + final int pos = loc64.position(); + final int locDsk = loc64.getInt(pos + ZIP64_LOCDSK); + final long locOff = loc64.getLong(pos + ZIP64_LOCOFF); + final int locTot = loc64.getInt(pos + ZIP64_LOCTOT); + if (locDsk != 0 || locTot != 1) { + throw new IOException("Split archives not supported"); + } + if (locOff > Integer.MAX_VALUE) { + throw new IOException("Unexpected oversize offset value"); + } + map.position((int) locOff); + final ByteBuffer eocd64 = map.slice(); + eocd64.order(ByteOrder.LITTLE_ENDIAN); + final int eocdSig = eocd64.getInt(0); + if (eocdSig != ZIP64_ENDSIG) { + throw new IOException("Zip64 EOCD locator did not point to EOCD structure"); + } + final int endNmd = eocd64.getInt(ZIP64_ENDNMD); + final int endDsk = eocd64.getInt(ZIP64_ENDDSK); + final long endTod = eocd64.getLong(ZIP64_ENDTOD); + final long endTot = eocd64.getLong(ZIP64_ENDTOT); + final long endSiz = eocd64.getLong(ZIP64_ENDSIZ); + final long endOff = eocd64.getLong(ZIP64_ENDOFF); + if (endNmd != 0 || endDsk != 0 || endTod != endTot) { + throw new IOException("Split archives not supported"); + } + if (endOff > Integer.MAX_VALUE) { + throw new IOException("Unexpected oversize offset value"); + } + parseDirectory(map, (int) endOff, (int) endSiz, context); + return; + } + final ByteBuffer eocd = findEocd(map, ZipFile.ENDSIG, ZipFile.ENDHDR); + if (eocd != null) { + eocd.order(ByteOrder.LITTLE_ENDIAN); + final int pos = eocd.position(); + final int curDiskNum = eocd.getShort(pos + ENDCUR); + final int cdStartDisk = eocd.getShort(pos + ENDSTA); + final int cdRecsHere = eocd.getShort(pos + ZipFile.ENDSUB); + final int cdRecsTotal = eocd.getShort(pos + ZipFile.ENDTOT); + final int cdSize = eocd.getInt(pos + ZipFile.ENDSIZ); + final int cdOffs = eocd.getInt(pos + ZipFile.ENDOFF); + if (curDiskNum != 0 || cdStartDisk != 0 || cdRecsHere != cdRecsTotal) { + throw new IOException("Split archives not supported"); + } + parseDirectory(map, cdOffs, cdSize, context); + return; + } + throw new IOException("EOCD signature not found"); + } + + private void parseDirectory( + final ByteBuffer map, + final int offset, + final int size, + final Map context) + throws IOException + { + map.position(offset); + final ByteBuffer dir = map.slice(); + dir.limit(size); + dir.order(ByteOrder.LITTLE_ENDIAN); + int pos = 0; + byte[] buf = new byte[256]; + while (pos < size) { + final int sig = dir.getInt(pos); + if (sig != ZipFile.CENSIG) { + break; + } + final int method = dir.getShort(pos + ZipFile.CENHOW); + final int compressedSize = dir.getInt(pos + ZipFile.CENSIZ); + final int originalSize = dir.getInt(pos + ZipFile.CENLEN); + final int nameLen = dir.getShort(pos + ZipFile.CENNAM); + final int extraLen = dir.getShort(pos + ZipFile.CENEXT); + final int commentLen = dir.getShort(pos + ZipFile.CENCOM); + final int startDiskNum = dir.getShort(pos + ZipFile.CENDSK); + final int headerOffs = dir.getInt(pos + ZipFile.CENOFF); + if (nameLen > buf.length) { + buf = new byte[buf.length * 2]; + } + dir.position(pos + ZipFile.CENHDR); + dir.get(buf, 0, nameLen); + final String name = new String(buf, 0, nameLen, StandardCharsets.UTF_8); + pos += ZipFile.CENHDR + nameLen + extraLen + commentLen; + + if (startDiskNum != 0) { + continue; + } + map.position(headerOffs); + if (method == METHOD_STORED && name.endsWith(".jar")) { + final Map descriptors + = new HashMap<>(16); + parseFile(map.slice(), descriptors, compressedSize); + nestedDescriptors.put(name, descriptors); + } + if (context != null) { + context.put(name, new OndemandEmbeddedJar.Descriptor(name, map, headerOffs, originalSize)); + } + } + } + + private void parseFile( + final ByteBuffer map, + final Map context, + final int compressedSize) + throws IOException + { + map.order(ByteOrder.LITTLE_ENDIAN); + final int pos = map.position(); + final int sig = map.getInt(pos); + if (sig != ZipFile.LOCSIG) { + return; + } + final int nameLen = map.getShort(pos + ZipFile.LOCNAM); + final int extraLen = map.getShort(pos + ZipFile.LOCEXT); + map.position(pos + ZipFile.LOCHDR + nameLen + extraLen); + final ByteBuffer nested = map.slice(); + nested.limit(compressedSize); + recurse(nested, context); + } + + private ByteBuffer findEocd( + final ByteBuffer map, + final long eocdSig, + final int eocdLen) + throws IOException + { + map.order(ByteOrder.LITTLE_ENDIAN); + final int length = map.limit(); + int eocdPos = length - eocdLen; + while (eocdPos > 0) { + final long sig = map.getInt(eocdPos); + if (sig == eocdSig) { + map.position(eocdPos); + return map.slice(); + } + eocdPos--; + if (length - eocdPos + eocdLen > Short.MAX_VALUE) { + break; + } + } + return null; + } +} diff --git a/emjar/src/test/java/com/comoyo/emjar/EmJarTest.java b/emjar/src/test/java/com/comoyo/emjar/EmJarTest.java new file mode 100644 index 0000000..cc01e14 --- /dev/null +++ b/emjar/src/test/java/com/comoyo/emjar/EmJarTest.java @@ -0,0 +1,18 @@ +package com.comoyo.emjar; + +import java.io.File; +import java.net.URL; +import java.net.URLDecoder; + +public abstract class EmJarTest +{ + protected File getResourceFile(String name) { + final ClassLoader cl = getClass().getClassLoader(); + final URL url = cl.getResource("com/comoyo/emjar/" + name); + if (!"file".equals(url.getProtocol())) { + throw new IllegalArgumentException( + "Resource " + name + " not present as file (" + url + ")"); + } + return new File(URLDecoder.decode(url.getPath())); + } +} diff --git a/emjar/src/test/java/com/comoyo/emjar/JarTest.java b/emjar/src/test/java/com/comoyo/emjar/JarTest.java new file mode 100644 index 0000000..a0e0e62 --- /dev/null +++ b/emjar/src/test/java/com/comoyo/emjar/JarTest.java @@ -0,0 +1,57 @@ +package com.comoyo.emjar; + +import java.io.BufferedReader; +import java.io.File; +import java.io.InputStreamReader; +import java.net.JarURLConnection; +import java.util.jar.JarEntry; +import java.util.jar.JarFile; + +import org.junit.Test; +import org.junit.runner.RunWith; +import org.junit.runners.JUnit4; + +import static org.junit.Assert.*; + +@RunWith(JUnit4.class) +public abstract class JarTest extends EmJarTest +{ + public abstract JarURLConnection getJarUrlConnection(File root, String jarName, String entryName) + throws Exception; + + protected JarFile testJarBundle(String name) + throws Exception + { + final File bundle = getResourceFile("bundle-" + name + ".jar"); + final JarURLConnection conn = getJarUrlConnection(bundle, "lib-" + name + ".jar", ""); + final JarFile jar = conn.getJarFile(); + final BufferedReader entry + = new BufferedReader(new InputStreamReader( + jar.getInputStream(new JarEntry("entry-" + name + ".txt")))); + assertEquals("Contents mismatch for " + name, name, entry.readLine()); + return jar; + } + + @Test + public void testAllBundles() + throws Exception + { + for (String m : new String[]{"m", "M"}) { + for (String s : new String[]{"s", "S"}) { + for (String l : new String[]{"l", "L"}) { + for (String c : new String[]{"c", "C"}) { + if (this instanceof PreloadedEmbeddedJarTest + && s.equals("S") && l.equals("L")) + { + // The combination of "streamed" and "zip64" breaks JarInputStream + continue; + } + + final String name = m + s + l + c; + testJarBundle(name); + } + } + } + } + } +} diff --git a/emjar/src/test/java/com/comoyo/emjar/OndemandEmbeddedJarTest.java b/emjar/src/test/java/com/comoyo/emjar/OndemandEmbeddedJarTest.java new file mode 100644 index 0000000..a6d1053 --- /dev/null +++ b/emjar/src/test/java/com/comoyo/emjar/OndemandEmbeddedJarTest.java @@ -0,0 +1,45 @@ +package com.comoyo.emjar; + +import java.io.File; +import java.io.InputStream; +import java.net.JarURLConnection; +import java.net.URL; +import java.util.Map; +import java.util.jar.JarEntry; +import java.util.jar.JarFile; + +import org.junit.Test; +import org.junit.runner.RunWith; +import org.junit.runners.JUnit4; + +import static org.junit.Assert.*; + +@RunWith(JUnit4.class) +public class OndemandEmbeddedJarTest + extends JarTest +{ + public JarURLConnection getJarUrlConnection(File root, String jarName, String entryName) + throws Exception + { + final ZipScanner scanner = new ZipScanner(root); + final Map> desc + = scanner.scan(); + assertNotNull("Descriptor returned from ZipScanner was null", desc); + return new OndemandEmbeddedJar.Connection( + new URL("jar:file:" + root.getPath() + "!/" + jarName + "!/"), + root.getPath(), + desc.get(jarName), + entryName); + } + + @Test + public void testLargeBundle() + throws Exception + { + for (String s : new String[]{"s", "S"}) { + final JarFile jar = testJarBundle(s + "-large"); + final InputStream is = jar.getInputStream(new JarEntry("oversize")); + assertNotNull("oversize entry was unexpectedly filtered out from " + s + " results", is); + } + } +} diff --git a/emjar/src/test/java/com/comoyo/emjar/PreloadedEmbeddedJarTest.java b/emjar/src/test/java/com/comoyo/emjar/PreloadedEmbeddedJarTest.java new file mode 100644 index 0000000..517e2b5 --- /dev/null +++ b/emjar/src/test/java/com/comoyo/emjar/PreloadedEmbeddedJarTest.java @@ -0,0 +1,40 @@ +package com.comoyo.emjar; + +import java.io.File; +import java.io.InputStream; +import java.net.JarURLConnection; +import java.net.URL; +import java.util.jar.JarEntry; +import java.util.jar.JarFile; + +import org.junit.Test; +import org.junit.runner.RunWith; +import org.junit.runners.JUnit4; + +import static org.junit.Assert.*; + +@RunWith(JUnit4.class) +public class PreloadedEmbeddedJarTest + extends JarTest +{ + public JarURLConnection getJarUrlConnection(File root, String jarName, String entryName) + throws Exception + { + return new PreloadedEmbeddedJar.Connection( + new URL("jar:file:" + root.getPath() + "!/" + jarName + "!/"), + root.getPath(), + jarName, + entryName); + } + + @Test + public void testLargeBundle() + throws Exception + { + for (String s : new String[]{"s", "S"}) { + final JarFile jar = testJarBundle(s + "-large"); + final InputStream is = jar.getInputStream(new JarEntry("oversize")); + assertNull("oversize entry was not filtered out from " + s + " results", is); + } + } +} diff --git a/emjar/src/test/java/com/comoyo/emjar/ZipScannerTest.java b/emjar/src/test/java/com/comoyo/emjar/ZipScannerTest.java new file mode 100644 index 0000000..91f8a37 --- /dev/null +++ b/emjar/src/test/java/com/comoyo/emjar/ZipScannerTest.java @@ -0,0 +1,48 @@ +package com.comoyo.emjar; + +import java.io.File; +import java.util.Map; + +import org.junit.Test; +import org.junit.runner.RunWith; +import org.junit.runners.JUnit4; + +import static org.junit.Assert.*; + +@RunWith(JUnit4.class) +public class ZipScannerTest + extends EmJarTest +{ + @Test + public void testCompressedBundle() + throws Exception + { + final File large = getResourceFile("bundle-Z-large.jar"); + final ZipScanner scanner = new ZipScanner(large); + final Map> desc + = scanner.scan(); + assertEquals("ZipScanner did not return empty set for compressed bundle", + 0, desc.entrySet().size()); + } + + private void testBundle(String name) + throws Exception + { + final File plain = getResourceFile("bundle-" + name + ".jar"); + final ZipScanner scanner = new ZipScanner(plain); + final Map> desc + = scanner.scan(); + final Map embedded + = desc.get("lib-" + name + ".jar"); + assertNotNull("Descriptor entry for lib-" + name + ".jar was null", embedded); + assertNotNull("File entry for entry-" + name + ".txt", embedded.get("entry-" + name + ".txt")); + } + + @Test + public void testPlainBundle() + throws Exception + { + testBundle("s-large"); + testBundle("S-large"); + } +} diff --git a/emjar/src/test/resources/com/comoyo/emjar/bundle-MSLC.jar b/emjar/src/test/resources/com/comoyo/emjar/bundle-MSLC.jar new file mode 100644 index 0000000..7659bd0 Binary files /dev/null and b/emjar/src/test/resources/com/comoyo/emjar/bundle-MSLC.jar differ diff --git a/emjar/src/test/resources/com/comoyo/emjar/bundle-MSLc.jar b/emjar/src/test/resources/com/comoyo/emjar/bundle-MSLc.jar new file mode 100644 index 0000000..5fa3154 Binary files /dev/null and b/emjar/src/test/resources/com/comoyo/emjar/bundle-MSLc.jar differ diff --git a/emjar/src/test/resources/com/comoyo/emjar/bundle-MSlC.jar b/emjar/src/test/resources/com/comoyo/emjar/bundle-MSlC.jar new file mode 100644 index 0000000..4175458 Binary files /dev/null and b/emjar/src/test/resources/com/comoyo/emjar/bundle-MSlC.jar differ diff --git a/emjar/src/test/resources/com/comoyo/emjar/bundle-MSlc.jar b/emjar/src/test/resources/com/comoyo/emjar/bundle-MSlc.jar new file mode 100644 index 0000000..4857880 Binary files /dev/null and b/emjar/src/test/resources/com/comoyo/emjar/bundle-MSlc.jar differ diff --git a/emjar/src/test/resources/com/comoyo/emjar/bundle-MsLC.jar b/emjar/src/test/resources/com/comoyo/emjar/bundle-MsLC.jar new file mode 100644 index 0000000..64a3fb2 Binary files /dev/null and b/emjar/src/test/resources/com/comoyo/emjar/bundle-MsLC.jar differ diff --git a/emjar/src/test/resources/com/comoyo/emjar/bundle-MsLc.jar b/emjar/src/test/resources/com/comoyo/emjar/bundle-MsLc.jar new file mode 100644 index 0000000..194696e Binary files /dev/null and b/emjar/src/test/resources/com/comoyo/emjar/bundle-MsLc.jar differ diff --git a/emjar/src/test/resources/com/comoyo/emjar/bundle-MslC.jar b/emjar/src/test/resources/com/comoyo/emjar/bundle-MslC.jar new file mode 100644 index 0000000..602e856 Binary files /dev/null and b/emjar/src/test/resources/com/comoyo/emjar/bundle-MslC.jar differ diff --git a/emjar/src/test/resources/com/comoyo/emjar/bundle-Mslc.jar b/emjar/src/test/resources/com/comoyo/emjar/bundle-Mslc.jar new file mode 100644 index 0000000..f08adbf Binary files /dev/null and b/emjar/src/test/resources/com/comoyo/emjar/bundle-Mslc.jar differ diff --git a/emjar/src/test/resources/com/comoyo/emjar/bundle-S-large.jar b/emjar/src/test/resources/com/comoyo/emjar/bundle-S-large.jar new file mode 100644 index 0000000..1b39fb7 Binary files /dev/null and b/emjar/src/test/resources/com/comoyo/emjar/bundle-S-large.jar differ diff --git a/emjar/src/test/resources/com/comoyo/emjar/bundle-Z-large.jar b/emjar/src/test/resources/com/comoyo/emjar/bundle-Z-large.jar new file mode 100644 index 0000000..252fa74 Binary files /dev/null and b/emjar/src/test/resources/com/comoyo/emjar/bundle-Z-large.jar differ diff --git a/emjar/src/test/resources/com/comoyo/emjar/bundle-mSLC.jar b/emjar/src/test/resources/com/comoyo/emjar/bundle-mSLC.jar new file mode 100644 index 0000000..b09f8bc Binary files /dev/null and b/emjar/src/test/resources/com/comoyo/emjar/bundle-mSLC.jar differ diff --git a/emjar/src/test/resources/com/comoyo/emjar/bundle-mSLc.jar b/emjar/src/test/resources/com/comoyo/emjar/bundle-mSLc.jar new file mode 100644 index 0000000..d2d6c39 Binary files /dev/null and b/emjar/src/test/resources/com/comoyo/emjar/bundle-mSLc.jar differ diff --git a/emjar/src/test/resources/com/comoyo/emjar/bundle-mSlC.jar b/emjar/src/test/resources/com/comoyo/emjar/bundle-mSlC.jar new file mode 100644 index 0000000..4424886 Binary files /dev/null and b/emjar/src/test/resources/com/comoyo/emjar/bundle-mSlC.jar differ diff --git a/emjar/src/test/resources/com/comoyo/emjar/bundle-mSlc.jar b/emjar/src/test/resources/com/comoyo/emjar/bundle-mSlc.jar new file mode 100644 index 0000000..08ed9ca Binary files /dev/null and b/emjar/src/test/resources/com/comoyo/emjar/bundle-mSlc.jar differ diff --git a/emjar/src/test/resources/com/comoyo/emjar/bundle-msLC.jar b/emjar/src/test/resources/com/comoyo/emjar/bundle-msLC.jar new file mode 100644 index 0000000..fc2823e Binary files /dev/null and b/emjar/src/test/resources/com/comoyo/emjar/bundle-msLC.jar differ diff --git a/emjar/src/test/resources/com/comoyo/emjar/bundle-msLc.jar b/emjar/src/test/resources/com/comoyo/emjar/bundle-msLc.jar new file mode 100644 index 0000000..49be56a Binary files /dev/null and b/emjar/src/test/resources/com/comoyo/emjar/bundle-msLc.jar differ diff --git a/emjar/src/test/resources/com/comoyo/emjar/bundle-mslC.jar b/emjar/src/test/resources/com/comoyo/emjar/bundle-mslC.jar new file mode 100644 index 0000000..a895b83 Binary files /dev/null and b/emjar/src/test/resources/com/comoyo/emjar/bundle-mslC.jar differ diff --git a/emjar/src/test/resources/com/comoyo/emjar/bundle-mslc.jar b/emjar/src/test/resources/com/comoyo/emjar/bundle-mslc.jar new file mode 100644 index 0000000..1fcb310 Binary files /dev/null and b/emjar/src/test/resources/com/comoyo/emjar/bundle-mslc.jar differ diff --git a/emjar/src/test/resources/com/comoyo/emjar/bundle-s-large.jar b/emjar/src/test/resources/com/comoyo/emjar/bundle-s-large.jar new file mode 100644 index 0000000..6551cd9 Binary files /dev/null and b/emjar/src/test/resources/com/comoyo/emjar/bundle-s-large.jar differ diff --git a/emjar/src/test/resources/com/comoyo/emjar/create-zips.pl b/emjar/src/test/resources/com/comoyo/emjar/create-zips.pl new file mode 100755 index 0000000..c4f8da9 --- /dev/null +++ b/emjar/src/test/resources/com/comoyo/emjar/create-zips.pl @@ -0,0 +1,57 @@ +#!/usr/bin/perl + +use strict; +use Archive::Zip::SimpleZip qw(:zip_method); + +our $DEFAULT_MANIFEST = "Manifest-Version: 1.0\r\n\r\n"; +our $TIMESTAMP = 1400000000; + +$Archive::Zip::SimpleZip::PARAMS{"time"} ||= [IO::Compress::Base::Common::Parse_any, undef]; + +sub createZip { + my ($name, $initargs, $entryargs, $oversize, $method) = @_; + my $buf = ""; + my $inner = Archive::Zip::SimpleZip->new(\$buf, %$initargs) + or die "Unable to create Zip"; + $inner->addString($DEFAULT_MANIFEST, + Name => "META-INF/MANIFEST.MF", + Time => $TIMESTAMP + 1); + if ($oversize) { + $inner->addString("\0" x (128 * 1024 * 1024 + 1), + Name => "oversize", + Time => $TIMESTAMP + 2); + } + $inner->addString($name, + Name => "entry-$name.txt", + Time => $TIMESTAMP + 3, + %$entryargs); + $inner->close(); + + my $outer = Archive::Zip::SimpleZip->new("bundle-$name.jar", %$initargs) + or die "Unable to create Zip"; + $outer->addString($DEFAULT_MANIFEST, + Name => "META-INF/MANIFEST.MF", + Time => $TIMESTAMP + 4); + $outer->addString($buf, + Name => "lib-$name.jar", + Method => ($method || ZIP_CM_STORE), + Time => $TIMESTAMP + 5); + $outer->close(); +} + +for my $m (0, 1) { + for my $s (0, 1) { + for my $l (0, 1) { + for my $c (0, 1) { + my $name = ($m ? "M" : "m") + .($s ? "S" : "s") + .($l ? "L" : "l") + .($c ? "C" : "c"); + createZip($name, {Minimal => $m, Stream => $s, Zip64 => $l}, {Comment => $c}); + } + } + } +} +createZip("s-large", {Stream => 0}, {}, 1); +createZip("S-large", {Stream => 1}, {}, 1); +createZip("Z-large", {}, {}, 1, ZIP_CM_DEFLATE); diff --git a/javadoc/maven.apache.org/maven-artifact/package-list b/javadoc/maven.apache.org/maven-artifact/package-list new file mode 100644 index 0000000..951c857 --- /dev/null +++ b/javadoc/maven.apache.org/maven-artifact/package-list @@ -0,0 +1,11 @@ +org.apache.maven.artifact +org.apache.maven.artifact.handler +org.apache.maven.artifact.metadata +org.apache.maven.artifact.repository +org.apache.maven.artifact.repository.layout +org.apache.maven.artifact.repository.metadata +org.apache.maven.artifact.resolver +org.apache.maven.artifact.resolver.filter +org.apache.maven.artifact.versioning +org.apache.maven.repository +org.apache.maven.repository.legacy.metadata diff --git a/pom.xml b/pom.xml index d169c6b..b1dfbd4 100644 --- a/pom.xml +++ b/pom.xml @@ -36,6 +36,8 @@ logging-context logging-context-gelf logging-context-json + emjar + emjar-maven-plugin @@ -142,6 +144,10 @@ http://logging.paluch.biz/apidocs ../../../javadoc/logging.paluch.biz + + http://maven.apache.org/ref/3.0.5/maven-artifact/apidocs + ../../../javadoc/maven.apache.org/maven-artifact +