/* * Copyright © 2014 Cask Data, Inc. * * 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 co.cask.cdap.archive; import com.google.common.base.Preconditions; import com.google.common.base.Predicate; import com.google.common.base.Predicates; import com.google.common.io.ByteStreams; import com.google.common.io.InputSupplier; import org.apache.twill.filesystem.Location; import java.io.BufferedOutputStream; import java.io.IOException; import java.io.InputStream; import java.util.Map; import java.util.jar.Attributes; import java.util.jar.JarEntry; import java.util.jar.JarInputStream; import java.util.jar.JarOutputStream; import java.util.jar.Manifest; import javax.annotation.Nullable; /** * ArchiveBundler is a utlity class that allows to clone a JAR file with modifications * to the manifest file and also adds few additional files to the cloned JAR. */ public final class ArchiveBundler { /** * Default location of additional files in cloned archive. */ private static final String DEFAULT_LOC = "META-INF/specification/"; /** * Main archive that needs to be cloned. */ private final Location archive; /** * Base location where the files in cloned archive will be stored. */ private final String jarEntryPrefix; /** * Constructor with archive to be cloned. * * @param archive to be cloned. */ public ArchiveBundler(Location archive) { this(archive, DEFAULT_LOC); } /** * Constructor that takes in the archive to be cloned. * * @param archive to be cloned * @param jarEntryPrefix within cloned archive for additional files. */ public ArchiveBundler(Location archive, String jarEntryPrefix) { Preconditions.checkNotNull(jarEntryPrefix, "Entry prefix of additional files in JAR is null"); this.archive = archive; if (jarEntryPrefix.charAt(jarEntryPrefix.length() - 1) != '/') { jarEntryPrefix += "/"; } this.jarEntryPrefix = jarEntryPrefix; } /** * Clones the input <code>archive</code> file with MANIFEST file and also adds addition * Files to the cloned archive. * * @param output Cloned output archive * @param manifest New manifest file to be added to the cloned archive * @param files Additional files to be added to cloned archive. Pairs are: entry_name, input supplier * @throws IOException thrown when issue with handling of files. */ public void clone(Location output, Manifest manifest, Map<String, ? extends InputSupplier<? extends InputStream>> files) throws IOException { clone(output, manifest, files, Predicates.<JarEntry>alwaysFalse()); } /** * Clones the input <code>archive</code> file with MANIFEST file and also adds addition * Files to the cloned archive. * * @param output Cloned output archive * @param manifest New manifest file to be added to the cloned archive * @param files Additional files to be added to cloned archive. * Pairs are: entry_name, input supplier * @param ignoreFilter Filter applied on ZipEntry, if true file is ignored, otherwise will be accepted. * @throws IOException thrown when issue with handling of files. */ public void clone(Location output, Manifest manifest, Map<String, ? extends InputSupplier<? extends InputStream>> files, Predicate<JarEntry> ignoreFilter) throws IOException { Preconditions.checkNotNull(manifest, "Null manifest"); Preconditions.checkNotNull(files); // Create a input stream based on the original archive file. // Create a new output JAR file with new MANIFEST. try ( JarInputStream zin = new JarInputStream(archive.getInputStream()); JarOutputStream zout = new JarOutputStream(new BufferedOutputStream(output.getOutputStream()), combineManifest(zin.getManifest(), manifest)) ) { // Iterates through the input zip entry and make sure, the new files // being added are not already present. If not, they are added to the // output zip. JarEntry entry = zin.getNextJarEntry(); while (entry != null) { // Invoke the predicate to see if the entry needs to be filtered. // If the ignoreFilter returns true, then it needs to be filtered; false keep it. if (ignoreFilter.apply(entry)) { entry = zin.getNextJarEntry(); continue; } final String name = entry.getName(); // adding entries missing in jar if (!files.containsKey(name)) { zout.putNextEntry(new JarEntry(entry)); ByteStreams.copy(zin, zout); } entry = zin.getNextJarEntry(); } // Add the new files. for (Map.Entry<String, ? extends InputSupplier<? extends InputStream>> toAdd : files.entrySet()) { zout.putNextEntry(new JarEntry(jarEntryPrefix + toAdd.getKey())); try { ByteStreams.copy(toAdd.getValue(), zout); } finally { zout.closeEntry(); } } } } /** * Combines two manifests into one. Same attributes in the second manifest will overwrite the first one. * If the first one is null, the second Manifest is returned. */ private Manifest combineManifest(@Nullable Manifest first, Manifest second) { if (first == null) { return second; } Manifest manifest = new Manifest(first); manifest.getMainAttributes().putAll(second.getMainAttributes()); for (Map.Entry<String, Attributes> entry : second.getEntries().entrySet()) { manifest.getAttributes(entry.getKey()).putAll(entry.getValue()); } return manifest; } }