/* * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. The ASF licenses this file * to you 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.apache.brooklyn.util.core.file; import static com.google.common.base.Preconditions.checkArgument; import static com.google.common.base.Preconditions.checkNotNull; import java.io.File; import java.io.FileOutputStream; import java.io.IOException; import java.io.OutputStream; import java.util.Collections; import java.util.Map; import java.util.jar.Attributes; import java.util.jar.JarEntry; import java.util.jar.JarOutputStream; import java.util.jar.Manifest; import java.util.zip.ZipOutputStream; import org.apache.brooklyn.util.core.file.ArchiveUtils.ArchiveType; import org.apache.brooklyn.util.exceptions.Exceptions; import org.apache.brooklyn.util.os.Os; import org.apache.brooklyn.util.text.Strings; import com.google.common.annotations.Beta; import com.google.common.collect.Iterables; import com.google.common.collect.LinkedHashMultimap; import com.google.common.collect.Multimap; import com.google.common.io.Files; /** * Build a Zip or Jar archive. * <p> * Supports creating temporary archives that will be deleted on exit, if no name is * specified. The created file must be a Java archive type, with the extension {@code .zip}, * {@code .jar}, {@code .war} or {@code .ear}. * <p> * Example: * <pre> File zip = ArchiveBuilder.archive("data/archive.zip") * .addAt(new File("./pom.xml"), "") * .addDirContentsAt(new File("./src"), "src/") * .addAt(new File("/tmp/Extra.java"), "src/main/java/") * .addDirContentsAt(new File("/tmp/overlay/"), "") * .create(); * </pre> * <p> */ @Beta public class ArchiveBuilder { /** * Create an {@link ArchiveBuilder} for an archive with the given name. */ public static ArchiveBuilder archive(String archive) { return new ArchiveBuilder(archive); } /** * Create an {@link ArchiveBuilder} for a {@link ArchiveType#ZIP Zip} format archive. */ public static ArchiveBuilder zip() { return new ArchiveBuilder(ArchiveType.ZIP); } /** * Create an {@link ArchiveBuilder} for a {@link ArchiveType#JAR Jar} format archive. */ public static ArchiveBuilder jar() { return new ArchiveBuilder(ArchiveType.JAR); } // TODO would be nice to support TAR and TGZ // e.g. using commons-compress // TarArchiveOutputStream out = new TarArchiveOutputStream(new GZIPOutputStream(bytes)); // but I think the way entries are done is slightly different so we'd need a bit of refactoring private final ArchiveType type; private File archive; private Manifest manifest; private Multimap<String, File> entries = LinkedHashMultimap.create(); private ArchiveBuilder() { this(ArchiveType.ZIP); } private ArchiveBuilder(String filename) { this(ArchiveType.of(filename)); named(filename); } private ArchiveBuilder(ArchiveType type) { checkNotNull(type); checkArgument(ArchiveType.ZIP_ARCHIVES.contains(type)); this.type = type; this.manifest = new Manifest(); } /** * Set the location of the generated archive file. */ public ArchiveBuilder named(String name) { checkNotNull(name); String ext = Files.getFileExtension(name); if (ext.isEmpty()) { name = name + "." + type.toString(); } else if (type != ArchiveType.of(name)) { throw new IllegalArgumentException(String.format("Extension for '%s' did not match archive type of %s", ext, type)); } this.archive = new File(Os.tidyPath(name)); return this; } /** * @see #named(String) */ public ArchiveBuilder named(File file) { checkNotNull(file); return named(file.getPath()); } /** * Add a manifest entry with the given {@code key} and {@code value}. */ public ArchiveBuilder manifest(Object key, Object value) { checkNotNull(key, "key"); checkNotNull(value, "value"); manifest.getMainAttributes().put(key, value); return this; } /** * Add the file located at the {@code filePath} to the archive, * with some complicated base-name strategies. * * @deprecated since 0.7.0 use one of the other add methods which makes the strategy explicit */ @Deprecated public ArchiveBuilder add(String filePath) { checkNotNull(filePath, "filePath"); return add(new File(Os.tidyPath(filePath))); } /** * Add the {@code file} to the archive. * <p> * If the file path is absolute, or points to a file above the current directory, * the file is added to the archive as a top-level entry, using the file name only. * For relative {@code filePath}s below the current directory, the file is added * using the path given and is assumed to be located relative to the current * working directory. * <p> * No checks for file existence are made at this stage. * * @see #entry(String, File) * @deprecated since 0.7.0 use one of the other add methods which makes the strategy explicit */ @Deprecated public ArchiveBuilder add(File file) { checkNotNull(file, "file"); String filePath = Os.tidyPath(file.getPath()); if (file.isAbsolute() || filePath.startsWith("../")) { return entry(Os.mergePaths(".", file.getName()), file); } else { return entry(Os.mergePaths(".", filePath), file); } } /** * Add a file located at the {@code fileSubPath}, relative to the {@code baseDir} on the local system, * as {@code fileSubPath} in the archive. For most archives directories are supported. * <p> * Uses the {@code fileSubPath} as the name of the file in the archive. Note that the * file is found by concatenating the two path components using {@link Os#mergePaths(String...)}, * thus {@code fileSubPath} should not be absolute or point to a location above the current directory. * <p> * For a simpler addition mechanism, use {@link #addAt(File, String)}. * <p> * For complete control over file locations and names in the archive. * use {@link #entry(String, String)} directly or {@link #entries(Map)} for complete * * @see #entry(String, String) */ public ArchiveBuilder addFromLocalBaseDir(File baseDir, String fileSubPath) { checkNotNull(baseDir, "baseDir"); checkNotNull(fileSubPath, "filePath"); return entry(fileSubPath, Os.mergePaths(baseDir.getPath(), fileSubPath)); } /** @deprecated since 0.7.0 use {@link #addFromLocalBaseDir(File, String)}, or * one of the other add methods if adding relative to baseDir was not intended */ @Deprecated public ArchiveBuilder addFromLocalBaseDir(String baseDir, String fileSubPath) { return addFromLocalBaseDir(new File(baseDir), fileSubPath); } /** @deprecated since 0.7.0 use {@link #addFromLocalBaseDir(File, String)}, or * one of the other add methods if adding relative to baseDir was not intended */ @Deprecated public ArchiveBuilder add(String baseDir, String fileSubPath) { return addFromLocalBaseDir(baseDir, fileSubPath); } /** * Adds the given file or directory to the archive, preserving its name but putting under the given directory in the archive (may be <code>""</code> or <code>"./"</code>). * See also {@link #addDirContentsAt(File, String)} and {@link #addFromLocalBaseDir(File, String)}. */ public ArchiveBuilder addAt(File file, String archiveParentDir) { checkNotNull(archiveParentDir, "archiveParentDir"); checkNotNull(file, "file"); return entry(Os.mergePaths(archiveParentDir, file.getName()), file); } /** * Adds the given file or directory to the root of the archive, preserving its name. * To add the contents of a directory without the dir name, use {@link #addDirContentsAtRoot(File)}. * See also {@link #addAt(File, String)}. */ public ArchiveBuilder addAtRoot(File file) { return addAt(file, ""); } /** * Add the contents of the directory named {@code dirName} to the archive. * * @see #addDir(File) * @deprecated since 0.7.0 use {@link #addDirContentsAt(File, String) */ @Deprecated public ArchiveBuilder addDir(String dirName) { checkNotNull(dirName, "dirName"); return addDir(new File(Os.tidyPath(dirName))); } /** * Add the contents of the directory {@code dir} to the archive. * The directory's name is not included; use {@link #addAt(File, String)} with <code>""</code> as the second argument if you want that behavior. * * @see #entry(String, File) */ public ArchiveBuilder addDirContentsAt(File dir, String archiveParentDir) { checkNotNull(dir, "dir"); if (!dir.isDirectory()) throw new IllegalArgumentException(dir+" is not a directory; cannot add contents to archive"); return entry(archiveParentDir, dir); } /** See {@link #addDirContentsAt(File, String)} and {@link #addAtRoot(File)}. */ public ArchiveBuilder addDirContentsAtRoot(File dir) { return addDirContentsAt(dir, ""); } /** * As {@link #addDirContentsAt(File, String)}, * using {@literal .} as the parent directory name for the contents. * * @deprecated since 0.7.0 use {@link #addDirContentsAt(File, String) * to clarify API, argument types, and be explicit about where it should be installed, * because JARs seem to require <code>""<code> whereas ZIPs might want <code>"./"</code>. */ @Deprecated public ArchiveBuilder addDir(File dir) { return addDirContentsAt(dir, "."); } /** * Add the collection of {@code files} to the archive. * * @see #add(String) * @deprecated since 0.7.0 use one of the other add methods if keeping this file's path was not intended */ @Deprecated public ArchiveBuilder add(Iterable<String> files) { checkNotNull(files, "files"); for (String filePath : files) { add(filePath); } return this; } /** * Add the collection of {@code files}, relative to the {@code baseDir}, to * the archive. * * @see #add(String, String) * @deprecated since 0.7.0 use one of the other add methods if keeping this file's path was not intended */ @Deprecated public ArchiveBuilder add(String baseDir, Iterable<String> files) { checkNotNull(baseDir, "baseDir"); checkNotNull(files, "files"); for (String filePath : files) { add(baseDir, filePath); } return this; } /** * Add the {@code file} to the archive with the path {@code entryPath}. * * @see #entry(String, File) */ public ArchiveBuilder entry(String entryPath, String filePath) { checkNotNull(entryPath, "entryPath"); checkNotNull(filePath, "filePath"); return entry(entryPath, new File(filePath)); } /** * Add the {@code file} to the archive with the path {@code entryPath}. */ public ArchiveBuilder entry(String entryPath, File file) { checkNotNull(entryPath, "entryPath"); checkNotNull(file, "file"); this.entries.put(entryPath, file); return this; } /** * Add a {@link Map} of entries to the archive. * <p> * The keys should be the names of the file entries to be added to the archive and * the value should point to the actual {@link File} to be added. * <p> * This allows complete control over the directory structure of the eventual archive, * as the entry names do not need to bear any relationship to the name or location * of the files on the filesystem. */ public ArchiveBuilder entries(Map<String, File> entries) { checkNotNull(entries, "entries"); for (Map.Entry<String, File> entry: entries.entrySet()) this.entries.put(entry.getKey(), entry.getValue()); return this; } /** * Generates the archive and outputs it to the given stream, ignoring any file name. * <p> * This will add a manifest file if the type is a Jar archive. */ public void stream(OutputStream output) { try { ZipOutputStream target; if (type == ArchiveType.ZIP) { target = new ZipOutputStream(output); } else { manifest(Attributes.Name.MANIFEST_VERSION, "1.0"); target = new JarOutputStream(output, manifest); } for (String entry : entries.keySet()) { addToArchive(entry, entries.get(entry), target); } target.close(); } catch (IOException ioe) { throw Exceptions.propagate(ioe); } } /** * Generates the archive, saving it with the given name. */ public File create(String archiveFile) { return named(archiveFile).create(); } /** * Generates the archive. * <p> * If no name has been specified, the archive will be created as a temporary file with * a unique name, that is deleted on exit. Otherwise, the given name will be used. */ public File create() { if (archive == null) { File temp = Os.newTempFile("brooklyn-archive", type.toString()); temp.deleteOnExit(); named(temp); } try { OutputStream output = new FileOutputStream(archive); stream(output); output.close(); } catch (IOException ioe) { throw Exceptions.propagate(ioe); } return archive; } /** * Recursively add files to the archive. * <p> * Code adapted from this <a href="http://stackoverflow.com/questions/1281229/how-to-use-jaroutputstream-to-create-a-jar-file">example</a> * <p> * <strong>Note</strong> {@link File} provides no support for symbolic links, and as such there is * no way to ensure that a symbolic link to a directory is not followed when traversing the * tree. In this case, iterables created by this traverser could contain files that are * outside of the given directory or even be infinite if there is a symbolic link loop. */ private void addToArchive(String path, Iterable<File> sources, ZipOutputStream target) throws IOException { int size = Iterables.size(sources); if (size==0) return; boolean isDirectory; if (size>1) { // it must be directories if we are putting multiple things here isDirectory = true; } else { isDirectory = Iterables.getOnlyElement(sources).isDirectory(); } String name = path.replace("\\", "/"); if (isDirectory) { name = Strings.removeAllFromEnd(name, "/"); if (name.length()>0) { name += "/"; JarEntry entry = new JarEntry(name); long lastModified=-1; for (File source: sources) if (source.lastModified()>lastModified) lastModified = source.lastModified(); entry.setTime(lastModified); target.putNextEntry(entry); target.closeEntry(); } for (File source: sources) { if (!source.isDirectory()) { throw new IllegalStateException("Cannot add multiple items at a path in archive unless they are directories: "+sources+" at "+path+" is not valid."); } Iterable<File> children = Files.fileTreeTraverser().children(source); for (File child : children) { addToArchive(Os.mergePaths(name, child.getName()), Collections.singleton(child), target); } } return; } File source = Iterables.getOnlyElement(sources); JarEntry entry = new JarEntry(name); entry.setTime(source.lastModified()); target.putNextEntry(entry); Files.asByteSource(source).copyTo(target); target.closeEntry(); } }