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 extends JarEntry> 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 extends ZipEntry> 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
+