Skip to content

Commit

Permalink
Merge pull request #72 from fcamblor/app-archive
Browse files Browse the repository at this point in the history
Packaging restx archives
  • Loading branch information
fcamblor committed Apr 1, 2014
2 parents fbcf3a1 + 4b26b5b commit c33a444
Show file tree
Hide file tree
Showing 6 changed files with 228 additions and 68 deletions.
14 changes: 14 additions & 0 deletions restx-build/src/main/java/restx/build/RestxBuild.java
Expand Up @@ -4,6 +4,7 @@
import java.nio.file.*;
import java.nio.file.attribute.BasicFileAttributes;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;

import static java.nio.file.FileVisitResult.CONTINUE;
Expand Down Expand Up @@ -104,4 +105,17 @@ private static Parser guessParserFor(Path path) {
}
return new RestxJsonSupport();
}

public static List<Path> resolveForeignModuleDescriptorsIn(Path directory) {
List<Path> possibleForeignModuleDescriptors = Arrays.asList(
directory.resolve("pom.xml"), directory.resolve("module.ivy"));

List<Path> existingForeignModuleDescriptors = new ArrayList<>();
for(Path possibleForeignModuleDescriptor : possibleForeignModuleDescriptors){
if(Files.exists(possibleForeignModuleDescriptor)) {
existingForeignModuleDescriptors.add(possibleForeignModuleDescriptor);
}
}
return existingForeignModuleDescriptors;
}
}
Expand Up @@ -71,6 +71,8 @@ protected Optional<? extends ShellCommandRunner> doMatch(String line) {
return Optional.of(new RunAppCommandRunner(args));
case "grab":
return Optional.of(new GrabAppCommandRunner(args));
case "archive":
return Optional.of(new ArchiveAppCommandRunner(args));
}

return Optional.absent();
Expand Down Expand Up @@ -758,7 +760,7 @@ protected void handleSingleFileGrabbing(String coordinates, Path destinationDir,
singleFileGrabber.grabSingleFileTo(coordinates, jarFile, shell);

AppSettings appSettings = shell.getFactory().getComponent(AppSettings.class);
new RestxArchiveUnpacker().unpack(jarFile, destinationDir, appSettings);
new RestxArchive(jarFile).unpack(destinationDir, appSettings);
jarFile.toFile().delete();
}

Expand Down Expand Up @@ -805,4 +807,26 @@ public void run(RestxShell shell) throws Exception {
shell.cd(destinationDirectoy);
}
}

private class ArchiveAppCommandRunner implements ShellCommandRunner {
private final Path jarFile;

public ArchiveAppCommandRunner(List<String> args) {
if(args.size() < 3) {
throw new IllegalArgumentException("app archive : missing jarFile argument");
}

this.jarFile = Paths.get(args.get(2));
}

@Override
public void run(RestxShell shell) throws Exception {
AppSettings appSettings = shell.getFactory().getComponent(AppSettings.class);
new RestxArchive(jarFile).pack(
shell.currentLocation(),
shell.currentLocation().resolve(appSettings.targetClasses()),
Arrays.asList("target", "tmp", "logs")
);
}
}
}
Expand Up @@ -181,13 +181,10 @@ public void run(RestxShell shell) throws Exception {
new RestxJsonSupport().generate(descriptor, w);
}

for (Path mod : asList(shell.currentLocation().resolve("pom.xml"),
shell.currentLocation().resolve("module.ivy"))) {
if (mod.toFile().exists()) {
shell.printIn("updating " + mod, RestxShell.AnsiCodes.ANSI_PURPLE);
shell.println("");
RestxBuild.convert(mdFile.toPath(), mod);
}
for (Path mod : RestxBuild.resolveForeignModuleDescriptorsIn(shell.currentLocation())) {
shell.printIn("updating " + mod, RestxShell.AnsiCodes.ANSI_PURPLE);
shell.println("");
RestxBuild.convert(mdFile.toPath(), mod);
}
}
}
Expand Down
171 changes: 171 additions & 0 deletions restx-core-shell/src/main/java/restx/core/shell/RestxArchive.java
@@ -0,0 +1,171 @@
package restx.core.shell;

import com.google.common.base.Splitter;
import com.google.common.base.Throwables;
import com.google.common.collect.Iterables;
import restx.AppSettings;
import restx.build.RestxBuild;

import java.io.*;
import java.nio.file.*;
import java.nio.file.attribute.BasicFileAttributes;
import java.util.*;
import java.util.jar.JarEntry;
import java.util.jar.JarFile;
import java.util.jar.JarOutputStream;

/**
* @author fcamblor
*/
public class RestxArchive {

private static final String CHROOT = "META-INF/restx/app/";
private Path jarFile;

public RestxArchive(Path jarFile) {
this.jarFile = jarFile;
}

private static class JarCopierFileVisitor extends SimpleFileVisitor<Path> {
private final Path startingDirectory;
private final String targetDirPrefix;
private List<String> excludes;
private final JarOutputStream jarOS;

private JarCopierFileVisitor(Path startingDirectory, JarOutputStream jarOS, String targetDirPrefix, List<String> excludes) {
this.startingDirectory = startingDirectory;
this.jarOS = jarOS;
this.targetDirPrefix = targetDirPrefix;
this.excludes = excludes;
}

@Override
public FileVisitResult preVisitDirectory(Path dir, BasicFileAttributes attrs) throws IOException {
super.preVisitDirectory(dir, attrs);

String directoryRelativizedName = startingDirectory.relativize(dir).toString().replace("\\", "/");
if(!directoryRelativizedName.isEmpty()) {
if(!directoryRelativizedName.endsWith("/")) {
directoryRelativizedName += "/";
}

for(String exclude : excludes){
if(directoryRelativizedName.startsWith(exclude)){
return FileVisitResult.SKIP_SUBTREE;
}
}

String targetDirectoryPath = targetDirPrefix + directoryRelativizedName;

JarEntry dirEntry = new JarEntry(targetDirectoryPath);
dirEntry.setTime(Files.getLastModifiedTime(dir).toMillis());
jarOS.putNextEntry(dirEntry);
jarOS.closeEntry();
}

return FileVisitResult.CONTINUE;
}

@Override
public FileVisitResult visitFile(Path file, BasicFileAttributes attrs) throws IOException {
super.visitFile(file, attrs);

copyFileToJar(jarOS, startingDirectory, targetDirPrefix, file);

return FileVisitResult.CONTINUE;
}
}

private static void createJarDirIfNotExists(JarOutputStream jarOS, Path rootDir, String path) throws IOException {
if(!Files.exists(rootDir.resolve(path))){
JarEntry dirEntry = new JarEntry(path+"/");
jarOS.putNextEntry(dirEntry);
jarOS.closeEntry();
}
}

private static void copyFileToJar(JarOutputStream jarOS, Path rootDir, String targetDirPrefix, Path file) throws IOException {
JarEntry fileEntry = new JarEntry(targetDirPrefix + rootDir.relativize(file).toString());

try { fileEntry.setTime(Files.getLastModifiedTime(file).toMillis()); }
catch (IOException e) {}

jarOS.putNextEntry(fileEntry);
Files.copy(file, jarOS);
jarOS.closeEntry();
}

public void pack(Path workingDirectory, final Path targetClassesDirectory, List<String> packagingExcludes) {
try(final JarOutputStream jarOS = new JarOutputStream(new FileOutputStream(jarFile.toFile()))) {

// Generating md.restx.json if it doesn't exist yet
// Because a restx archive without this descriptor won't be considered as valid restx archive
Path md = workingDirectory.resolve("md.restx.json");
boolean generatedMd = false;
if(!Files.exists(md)){
List<Path> foreignModuleDescriptors = RestxBuild.resolveForeignModuleDescriptorsIn(workingDirectory);
if(!foreignModuleDescriptors.isEmpty()) {
Path firstModuleDescriptor = Iterables.getFirst(foreignModuleDescriptors, null);
RestxBuild.convert(firstModuleDescriptor, md);
generatedMd = true;
} else {
throw new RuntimeException("Project descriptor (either a md.restx.json, or a foreign descriptor (pom, ivy, ...) file) is missing !");
}
}

try {
Files.walkFileTree(targetClassesDirectory, new JarCopierFileVisitor(targetClassesDirectory, jarOS, "", Collections.<String>emptyList()));

// Ensuring CHROOT is made available in target jar
String path = "";
for(String chrootChunk : Splitter.on("/").split(CHROOT)){
if(!chrootChunk.isEmpty()) {
path += chrootChunk;
createJarDirIfNotExists(jarOS, targetClassesDirectory, path);
path += "/";
}
}

// Copying everything into CHROOT directory
Files.walkFileTree(workingDirectory, new JarCopierFileVisitor(workingDirectory, jarOS, CHROOT, packagingExcludes));
} finally {
if(generatedMd) {
md.toFile().delete();
}
}
} catch (IOException e) {
Throwables.propagate(e);
}
}

public void unpack(Path destinationDirectory, AppSettings appSettings) {
Path targetClassesDir = destinationDirectory.resolve(appSettings.targetClasses());
try ( JarFile jar = new JarFile(jarFile.toFile()) ) {
if(jar.getJarEntry(CHROOT+"md.restx.json") == null) {
throw new IllegalArgumentException("File "+jarFile+" is not a restx archive (no md.restx.json file found) !");
}
for(Enumeration<JarEntry> jarEntries = jar.entries(); jarEntries.hasMoreElements();){
JarEntry entry = jarEntries.nextElement();
String entryPath = entry.getName();
if(!entry.isDirectory()) {
Path destinationFile;
// Unpacking chrooted files in app root directory..
if(entryPath.startsWith(CHROOT)) {
Path chrootedFile = destinationDirectory.resolve(entryPath.substring(CHROOT.length()));
destinationFile = chrootedFile;
// ... and unpacking other files in classes directory
} else {
destinationFile = targetClassesDir.resolve(entryPath);
}

com.google.common.io.Files.createParentDirs(destinationFile.toFile());
try(InputStream jarInputStream = jar.getInputStream(entry)) {
Files.copy(jarInputStream, destinationFile);
}
}
}
} catch (IOException e) {
throw Throwables.propagate(e);
}
}
}

This file was deleted.

15 changes: 14 additions & 1 deletion restx-core-shell/src/main/resources/restx/core/shell/app.man
Expand Up @@ -74,12 +74,25 @@ Your app will be started in different modes depending on your working directory
- By guessing it from source files


## app archive <jarFile>

Create a restx archive from current working directory.
A restx archive is a standard jar archive, with source (including assets) ressources located in `META-INF/restx/app/`
folder.

Purpose of these archives is to be easily deployable with the `app grab` command.
They embed a restx module descriptor which will be used to provision dependencies before launching the restx app.

`<jarFile>`
Path to generated restx archive vile.

## app grab <coordinates> [<destination>]

Grab and unpack a restx archive/project into a directory, then change restx shell to this directory in order
to easily chain this command with `app run` command.

A restx archive is a standard jar archive, with static ressources located in `META-INF/restx/app/` folder.
A restx archive is a standard jar archive, with sources (including assets) ressources located in `META-INF/restx/app/`
folder.
Note that a restx archive MUST contain a `META-INF/restx/app/md.restx.json` file in order to be considered valid.

`<coordinates>`
Expand Down

0 comments on commit c33a444

Please sign in to comment.