Jetty with self-extracting WAR from Launch4J EXE

In my previous post I was hacking Tomcat and Jetty with Launch4J – Jetty eventually won as it was willing to read JAR as WAR (not so picky about file extension as Tomcat was) and find WEB-INF/web.xml there without problems.

Yesterday I decided to go on with my quest and populate temporary WAR directory from my classpath programmatically – which would actually get Tomcat (or any other embedded container reading exploded WARs from disk) back into game. We will utilize Java 7 NIO2 classes, so if you’re stuck with Java 6 you have to adjust parts of the solution.

POM simplification

We will stick with Jetty for other reasons, but it will provide some flexibility there as well – now we can wrap JAR into EXE which we carefully avoided previously with plugin options (plugin was com.akathist.maven.plugins.launch4j:launch4j-maven-plugin, see the previous post for the listings):

  • <jar>${project.artifactId}-${project.version}.jar</jar>
  • <dontWrapJar>true</dontWrapJar>

These overrode parent POM’s defaults:

  • <jar>${project.build.directory}/${project.artifactId}-${project.version}.jar</jar>
  • <dontWrapJar>false</dontWrapJar>

So let’s minimise the module’s POM – without changing parent’s one we just mention both plugins we need – launch4j to get EXE and dependency plugin to populate directory with dependencies for us:

  <profiles>
    <profile>
      <id>exe-build</id>
      <build>
        <plugins>
          <plugin>
            <groupId>com.akathist.maven.plugins.launch4j</groupId>
            <artifactId>launch4j-maven-plugin</artifactId>
          </plugin>
          <plugin>
            <groupId>org.apache.maven.plugins</groupId>
            <artifactId>maven-dependency-plugin</artifactId>
          </plugin>
        </plugins>
      </build>
    </profile>
  </profiles>

Neat – my EXE module does not differ from other EXE modules (those without containers) anymore! Let’s code now, yeah… I mean not in XML.

Extracting web.xml from classpath

Using our previous bootstrap code, we just want to check whether our warPath is EXE file or not. If not, we can leave it that way, Jetty will read from JAR/WAR or even target/classes (especially handy during development). If it is EXE, however, we want to get the WAR stuff out. Explode it ourselves:

int port = Integer.parseInt(System.getProperty("port", "8080"));
String contextPath = System.getProperty("context", "app");

Server server = new Server(port);
WebAppContext context = new WebAppContext();
context.setServer(server);
context.setContextPath('/' + contextPath);

ProtectionDomain protectionDomain = RestMain.class.getProtectionDomain();
String warPath = protectionDomain.getCodeSource().getLocation().toExternalForm();
if (warPath.toLowerCase().endsWith(".exe")) {
  warPath = prepareWarPathFromExe(protectionDomain);
} // else we assume dir or jar/war
context.setWar(warPath);
server.setHandler(context);

server.start();

if (!context.isAvailable() || context.getWebInf() == null) {
  server.stop();
}
server.join();

Ok, that was easy – tiny change in the original code. Now the hard part – but first we just extract a single file – web.xml.

  private static String prepareWarPathFromExe(ProtectionDomain protectionDomain) throws IOException {
      String warPath = temporaryWar(protectionDomain.getClassLoader());
      log.debug("Extracting WAR from EXE into: " + warPath);
      final String finalWarPath = warPath;
      Runtime.getRuntime().addShutdownHook(new Thread() {
        @Override
        public void run() {
          try {
            deleteRecursive(Paths.get(finalWarPath));
            log.debug("Temporary WAR directory deleted");
          } catch (IOException e) {
            log.warn("Problems with deleting temporary directory", e);
          }
        }
      });
      return warPath;
    }

    private static String temporaryWar(ClassLoader classLoader) throws IOException {
      Path tmpWarDir = Files.createTempDirectory("restmod");
      Path webInf = tmpWarDir.resolve("WEB-INF");
      Files.createDirectory(webInf);
      Files.copy(classLoader.getResourceAsStream("WEB-INF/web.xml"), webInf.resolve("web.xml"));
      return tmpWarDir.toString();
    }

    private static void deleteRecursive(Path dir) throws IOException {
      if (Files.isDirectory(dir)) {
        try (DirectoryStream<Path> directoryStream = Files.newDirectoryStream(dir)) {
          for (Path path : directoryStream) {
            deleteRecursive(path);
          }
        }
      }
      Files.delete(dir);
    }

I think the code is easy to follow for most of the part. Shutdown hook deletes the temporary directory. We work with ClassLoader to get our resources, etc. If all you need is WEB-INF/web.xml, you can stop here. But this is far from flexible in case you have JSPs and other resources to extract. So let’s try to extract everything starting with specified strings.

Extracting more

I don’t know whether there is some reasonable way to “crawl” through classpath resources, however it is not what we need, actually. We want to check the EXE file and treat it as a JAR. While Jetty does not like context set to this EXE path (not even if you use URL starting with jar:file:/ and ending with !/), it is still perfectly valid JAR file.

First I wanted to use one of my ancient solutions inspired by Stripes framework. This checks JARs on the classpath, and opens their files as JarInputStream. This didn’t seem to work. However I found suggested Spring solution for reading resources using patterns:

PathMatchingResourcePatternResolver resolver =
  new PathMatchingResourcePatternResolver(protectionDomain.getClassLoader());
Resource[] resources = resolver.getResources("classpath*:WEB-INF/**");
// then copy the resources where necessary

And this worked and found resources in my EXE file too. So I dag a bit deeper, found the Spring’s class PathMatchingResourcePatternResolver and the method doFindPathMatchingJarResources – and I was back on track.

First we will adjust our calling statement from main method, because we’re leaving waters of classpath (taken from ProtectionDomain). And we introduce our prefixes from which we want to extract (META-INF is just an example, choose yours):

warPath = prepareWarPathFromExe(warPath, "WEB-INF", "META");

The rest of the main method stays untouched. Now the new body of the prepareWarPathFromExe method:

    private static String prepareWarPathFromExe(String pathToExe, String... prefixes) throws IOException {
      Path tmpWarDir = Files.createTempDirectory("restmod");
      final String warPath = tmpWarDir.toString();
      log.debug("Extracting WAR from EXE into {}, prefixes {}", warPath, prefixes);
      WarExploder warExploder = new WarExploder(pathToExe, warPath);
      warExploder.explode(prefixes);
      Runtime.getRuntime().addShutdownHook(new Thread() {
        @Override
        public void run() {
          try {
            deleteRecursive(Paths.get(warPath));
            log.debug("Temporary WAR directory deleted");
          } catch (IOException e) {
            log.warn("Problems with deleting temporary directory", e);
          }
        }
      });
      return warPath;
    }

Recursive delete stays the same. I put WAR exploding into separate class so I can put both paths into fields and don’t drag them through parameters. (And I’d hate to put them into static variables in my main class too.) Here it is:

import java.io.File;
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.util.Arrays;
import java.util.List;
import java.util.jar.JarEntry;
import java.util.jar.JarFile;
import java.util.stream.Collectors;

/**
 * Extracts files from JAR to specified directory. Extraction can be called multiple times for various set of prefixes.
 * Prefix matches if the JAR entry name starts with it, that is "META" matches META-INF and anything under.
 */
class WarExploder {
    private final File srcJarFile;
    private final Path explodedDir;

    WarExploder(String jarFilePath, String explodedDir) {
      if (jarFilePath.startsWith("file:/")) {
        jarFilePath = jarFilePath.substring("file:/".length());
      }
      srcJarFile = new File(jarFilePath);
      this.explodedDir = Paths.get(explodedDir);
    }

    void explode(String... prefixes) throws IOException {
      try (JarFile jarFile = new JarFile(srcJarFile)) {
        List<JarEntry> entriesToExtract = jarFile.stream()
          .filter(e -> Arrays.stream(prefixes).anyMatch(e.getName()::startsWith))
          .collect(Collectors.toList());

        // throws checked exception, hence the for-each
        for (JarEntry jarEntry : entriesToExtract) {
          extractIfMatching(jarFile, jarEntry);
        }
      }
    }

    private void extractIfMatching(JarFile jarFile, JarEntry jarEntry) throws IOException {
      String name = jarEntry.getName();
      Path targetPath = explodedDir.resolve(name);
      if (jarEntry.isDirectory()) {
        Files.createDirectory(targetPath);
      } else {
        Files.copy(jarFile.getInputStream(jarEntry), targetPath);
      }
    }
}

This time I used a bit of streams too, but there should be no problem to switch it to Java 7, maybe a bit more problem to use Java 6 – but if you’re that far in the past, then you rather use some Apache libs to copy files too.

Exploder doesn’t log, everything is delegated (and exception propagated) to the main class.

Conclusion

It wasn’t that hard after all. Although the solution is pure JDK after all, I have to thank Spring developers for their incredible work and inspiration (not just here and now). In cases like these you also can feel the pain of developers still stuck with pre-7 Java, as NIO 2 does miracles with Files and Paths (finally!). It also wouldn’t be possible without JDK working with JARs so easily. Actually if I had headed into getResourcesAsStream in URLClassLoader, I’d have found the proper way of working with JAR.

Originally I thought JarURLConnection was the solution, but the real key was using JarFile instead of JarInputStream (that works perfectly OK in other circumstances). You can create JarFile using JarURLConnection or just plain File. Having the file lying around on the disk, I felt that File should suffice – and it did.

There is also related StackOverflow answer (and probably more).

Next time we will talk about “hardening” our Jetty application, so that it produces our error messages instead of the default ones (not to mention that directory browsing is on by default too!) – but that will be much shorter than today’s post. 🙂

Advertisements

About virgo47
Java Developer by profession in the first place. Gamer and amateur musician. And father too. Naive believer in brighter future. Step by step.

One Response to Jetty with self-extracting WAR from Launch4J EXE

  1. Pingback: Jetty hardening | Virgo's Naive Stories

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out / Change )

Twitter picture

You are commenting using your Twitter account. Log Out / Change )

Facebook photo

You are commenting using your Facebook account. Log Out / Change )

Google+ photo

You are commenting using your Google+ account. Log Out / Change )

Connecting to %s