/*
* Copyright 2013 Harald Wellmann
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
* implied.
*
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.ops4j.pax.exam.spi.war;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
import java.util.zip.ZipEntry;
import java.util.zip.ZipOutputStream;
import org.ops4j.io.StreamUtils;
/**
* Builds a ZIP archive from individual files and directories.
*
* @author Harald Wellmann
*
*/
public class ZipBuilder {
private FileOutputStream os;
private ZipOutputStream jarOutputStream;
/**
* Creates a ZIP archive in the given file. Allocates underlying file system resources. The user
* must call {@link #close()} to release these resources.
*
* @param zipFile
* archive file to be created
* @throws IOException
* on I/O error
*/
public ZipBuilder(File zipFile) throws IOException {
this.os = new FileOutputStream(zipFile);
this.jarOutputStream = new ZipOutputStream(os);
}
/**
* Recursively adds a directory tree to the archive. The archive must not be closed.
* <p>
* Example:<br>
*
* <pre>
* sourceDir = /opt/work/classes
* targetDir = WEB-INF/classes
*
* /opt/work/classes/com/acme/Foo.class -> WEB-INF/classes/com/acme/Foo.class
* </pre>
*
* @param sourceDir
* Root directory of tree to be added
* @param targetDir
* Relative path within the archive corresponding to root. Regardless of the OS, this
* path must use slashes ('/') as separators.
*
* @return this for fluent syntax
* @throws IOException
* on I/O error
*/
public ZipBuilder addDirectory(File sourceDir, String targetDir) throws IOException {
addDirectory(sourceDir, sourceDir, targetDir, jarOutputStream);
return this;
}
/**
* Adds a file to the archive. The archive must not be closed.
* <p>
* Example:<br>
*
* <pre>
* sourceFile = C:\opt\work\deps\foo.jar
* targetDir = WEB-INF/lib/foo.jar
*
* </pre>
*
* @param sourceFile
* File to be added
* @param targetFile
* Relative path for the file within the archive. Regardless of the OS, this path
* must use slashes ('/') as separators.
*
* @return this for fluent syntax
* @throws IOException
* on I/O error
*/
public ZipBuilder addFile(File sourceFile, String targetFile) throws IOException {
FileInputStream fis = new FileInputStream(sourceFile);
ZipEntry jarEntry = new ZipEntry(targetFile);
jarOutputStream.putNextEntry(jarEntry);
StreamUtils.copyStream(fis, jarOutputStream, false);
fis.close();
return this;
}
/**
* Closes the archive and releases file system resources. No more files or directories may be
* added after calling this method.
*
* @throws IOException
* on I/O error
*/
public void close() throws IOException {
if (jarOutputStream != null) {
jarOutputStream.close();
}
else if (os != null) {
os.close();
}
}
/**
* Recursively adds the contents of the given directory and all subdirectories to the given ZIP
* output stream.
*
* @param root
* an ancestor of {@code directory}, used to determine the relative path within the
* archive
* @param directory
* current directory to be added
* @param zos
* ZIP output stream
* @throws IOException
*/
private void addDirectory(File root, File directory, String targetPath, ZipOutputStream zos) throws IOException {
String prefix = targetPath;
if (!prefix.isEmpty() && !prefix.endsWith("/")) {
prefix += "/";
}
// directory entries are required, or else bundle classpath may be
// broken
if (!directory.equals(root)) {
String path = normalizePath(root, directory);
ZipEntry jarEntry = new ZipEntry(prefix + path + "/");
jarOutputStream.putNextEntry(jarEntry);
}
File[] children = directory.listFiles();
// loop through dirList, and zip the files
for (File child : children) {
if (child.isDirectory()) {
addDirectory(root, child, prefix, jarOutputStream);
}
else {
addFile(root, child, prefix, jarOutputStream);
}
}
}
/**
* Adds a given file to the given ZIP output stream.
*
* @param root
* an ancestor of {@code file}, used to determine the relative path within the
* archive
* @param file
* file to be added
* @param zos
* ZIP output stream
* @throws IOException
*/
private void addFile(File root, File file, String prefix, ZipOutputStream zos) throws IOException {
FileInputStream fis = new FileInputStream(file);
ZipEntry jarEntry = new ZipEntry(prefix + normalizePath(root, file));
zos.putNextEntry(jarEntry);
StreamUtils.copyStream(fis, zos, false);
fis.close();
}
/**
* Returns the relative path of the given file with respect to the root directory, with all file
* separators replaced by slashes.
*
* Example: For root {@code C:\work} and file {@code C:\work\com\example\Foo.class}, the result
* is {@code com/example/Foo.class}
*
* @param root
* root directory
* @param file
* relative path
* @return normalized file path
*/
private String normalizePath(File root, File file) {
String relativePath = file.getPath().substring(root.getPath().length() + 1);
String path = relativePath.replaceAll("\\" + File.separator, "/");
return path;
}
}