
at.spardat.xma.xdelta.JarDelta Maven / Gradle / Ivy
Show all versions of javaxdelta Show documentation
/*
* Copyright (c) 2003, 2007 s IT Solutions AT Spardat GmbH.
*
* Permission is hereby granted, free of charge, to any person obtaining a
* copy of this software and associated documentation files (the "Software"),
* to deal in the Software without restriction, including without limitation
* the rights to use, copy, modify, merge, publish, distribute, sublicense,
* and/or sell copies of the Software, and to permit persons to whom the
* Software is furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in
* all copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
* THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
* FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS
* IN THE SOFTWARE.
*
*/
package at.spardat.xma.xdelta;
import java.io.ByteArrayOutputStream;
import java.io.DataOutputStream;
import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStreamWriter;
import java.io.PrintWriter;
import java.util.ArrayList;
import java.util.Enumeration;
import java.util.regex.Pattern;
import java.util.zip.ZipException;
import org.apache.commons.compress.archivers.zip.ExtraFieldUtils;
import org.apache.commons.compress.archivers.zip.ZipArchiveEntry;
import org.apache.commons.compress.archivers.zip.ZipArchiveOutputStream;
import org.apache.commons.compress.archivers.zip.ZipFile;
import com.nothome.delta.Delta;
import com.nothome.delta.DiffWriter;
import com.nothome.delta.GDiffWriter;
/**
* This class calculates the binary difference of two zip files by applying {@link com.nothome.delta.Delta}
* to all files contained in both zip files. All these binary differences are stored in the output zip file.
* New files are simply copied to the output zip file. Additionally all files contained in the target zip
* file are listed in META-INF/file.list
.
* Use {@link JarPatcher} to apply the output zip file.
*
* @author gruber
*/
public class JarDelta {
/** The Constant zipFilesPattern. */
public static final Pattern zipFilesPattern = Pattern.compile(".*?\\.zip$|.*?\\.jar$|.*?\\.war$|.*?\\.ear$", Pattern.CASE_INSENSITIVE);
/** The Constant BUFFER_LEN. */
private static final int BUFFER_LEN = 8 * 1024;
/** The buffer. */
private final byte[] buffer = new byte[BUFFER_LEN];
/** The calculated delta. */
private byte[] calculatedDelta = null;
/**
* Computes the binary differences of two zip files. For all files contained in source and target which
* are not equal, the binary difference is caluclated by using
* {@link com.nothome.delta.Delta#compute(byte[], InputStream, DiffWriter)}.
* If the files are equal, nothing is written to the output for them.
* Files contained only in target and files to small for {@link com.nothome.delta.Delta} are copied to output.
* Files contained only in source are ignored.
* At last a list of all files contained in target is written to META-INF/file.list
in output.
*
* @param sourceName the original zip file
* @param targetName a modification of the original zip file
* @param source the original zip file
* @param target a modification of the original zip file
* @param output the zip file where the patches have to be written to
* @throws IOException if an error occurs reading or writing any entry in a zip file
*/
public void computeDelta(String sourceName, String targetName, ZipFile source, ZipFile target, ZipArchiveOutputStream output) throws IOException {
ByteArrayOutputStream listBytes = new ByteArrayOutputStream();
PrintWriter list = new PrintWriter(new OutputStreamWriter(listBytes));
list.println(sourceName);
list.println(targetName);
computeDelta(source, target, output, list, "");
list.close();
ZipArchiveEntry listEntry = new ZipArchiveEntry("META-INF/file.list");
output.putArchiveEntry(listEntry);
output.write(listBytes.toByteArray());
output.closeArchiveEntry();
output.finish();
output.flush();
}
/**
* Compute delta.
*
* @param source the source
* @param target the target
* @param output the output
* @param list the list
* @param prefix the prefix
* @throws IOException Signals that an I/O exception has occurred.
*/
public void computeDelta(ZipFile source, ZipFile target, ZipArchiveOutputStream output, PrintWriter list, String prefix) throws IOException {
try {
for(Enumeration enumer=target.getEntries();enumer.hasMoreElements();) {
calculatedDelta = null;
ZipArchiveEntry targetEntry = enumer.nextElement();
ZipArchiveEntry sourceEntry = findBestSource(source, target, targetEntry);
String nextEntryName = prefix + targetEntry.getName();
if (sourceEntry != null && targetEntry != null && zipFilesPattern.matcher(sourceEntry.getName()).matches() && !equal(sourceEntry, targetEntry)) {
nextEntryName += "!";
}
nextEntryName += "|" + Long.toHexString(targetEntry.getCrc());
if (sourceEntry != null) {
nextEntryName += ":" + Long.toHexString(sourceEntry.getCrc());
} else {
nextEntryName += ":0";
}
list.println(nextEntryName);
if(targetEntry.isDirectory()) {
if(sourceEntry==null) {
ZipArchiveEntry outputEntry = entryToNewName(targetEntry, prefix + targetEntry.getName());
output.putArchiveEntry(outputEntry);
output.closeArchiveEntry();
}
} else {
if(sourceEntry==null
|| sourceEntry.getSize() <= Delta.DEFAULT_CHUNK_SIZE
|| targetEntry.getSize() <= Delta.DEFAULT_CHUNK_SIZE) { // new Entry od. alter Eintrag od. neuer Eintrag leer
ZipArchiveEntry outputEntry = entryToNewName(targetEntry, prefix + targetEntry.getName());
output.putArchiveEntry(outputEntry);
try (InputStream in = target.getInputStream(targetEntry)) {
int read = 0;
while (-1 < (read = in.read(buffer))) {
output.write(buffer, 0, read);
}
output.flush();
}
output.closeArchiveEntry();
} else {
if(!equal(sourceEntry,targetEntry)) {
if (zipFilesPattern.matcher(sourceEntry.getName()).matches()) {
File embeddedSource = File.createTempFile("jardelta-tmp", ".zip");
try (FileOutputStream out = new FileOutputStream(embeddedSource);
InputStream in = source.getInputStream(sourceEntry)) {
int read = 0;
while (-1 < (read = in.read(buffer))) {
out.write(buffer, 0, read);
}
out.flush();
}
File embeddedTarget = File.createTempFile("jardelta-tmp", ".zip");
try (FileOutputStream out = new FileOutputStream(embeddedTarget);
InputStream in = target.getInputStream(targetEntry)) {
int read = 0;
while (-1 < (read = in.read(buffer))) {
out.write(buffer, 0, read);
}
out.flush();
}
computeDelta(new ZipFile(embeddedSource), new ZipFile(embeddedTarget), output, list, prefix + sourceEntry.getName() + "!");
embeddedSource.delete();
embeddedTarget.delete();
} else {
ZipArchiveEntry outputEntry = new ZipArchiveEntry(prefix + targetEntry.getName()+".gdiff");
outputEntry.setTime(targetEntry.getTime());
outputEntry.setComment("" + targetEntry.getCrc());
output.putArchiveEntry(outputEntry);
if (calculatedDelta != null) {
output.write(calculatedDelta);
output.flush();
} else {
try (ByteArrayOutputStream outbytes = new ByteArrayOutputStream()) {
Delta d = new Delta();
DiffWriter diffWriter = new GDiffWriter(new DataOutputStream(outbytes));
int sourceSize = (int)sourceEntry.getSize();
byte[] sourceBytes = new byte[sourceSize];
try (InputStream sourceStream = source.getInputStream(sourceEntry)) {
for(int erg=sourceStream.read(sourceBytes);erg ret = new ArrayList<>();
for (ZipArchiveEntry next : source.getEntries(targetEntry.getName())) {
if (next.getCrc() == targetEntry.getCrc()) return next;
ret.add(next);
}
if (ret.size() == 0) return null;
if (ret.size() == 1 || targetEntry.isDirectory()) return ret.get(0);
//More than one and no matching crc --- need to calculate xdeltas and pick the shortest
ZipArchiveEntry retEntry = null;
for (ZipArchiveEntry sourceEntry : ret) {
try (ByteArrayOutputStream outbytes = new ByteArrayOutputStream()) {
Delta d = new Delta();
DiffWriter diffWriter = new GDiffWriter(new DataOutputStream(outbytes));
int sourceSize = (int)sourceEntry.getSize();
byte[] sourceBytes = new byte[sourceSize];
try (InputStream sourceStream = source.getInputStream(sourceEntry)) {
for(int erg=sourceStream.read(sourceBytes);erg nextDiff.length) {
retEntry = sourceEntry;
calculatedDelta = nextDiff;
}
}
}
return retEntry;
}
/**
* Main method to make {@link #computeDelta(String, String, ZipFile, ZipFile, ZipArchiveOutputStream)} available at
* the command line.
* usage JarDelta source target output
*
* @param args the arguments
* @throws IOException Signals that an I/O exception has occurred.
*/
public static void main(String[] args) throws IOException {
if (args.length != 3) {
System.err.println("usage JarDelta source target output");
return;
}
try (ZipArchiveOutputStream output = new ZipArchiveOutputStream(new FileOutputStream(args[2]))) {
new JarDelta().computeDelta(args[0], args[1], new ZipFile(args[0]), new ZipFile(args[1]), output);
}
}
/**
* Entry to new name.
*
* @param source the source
* @param name the name
* @return the zip archive entry
* @throws ZipException the zip exception
*/
public static ZipArchiveEntry entryToNewName(ZipArchiveEntry source, String name) throws ZipException {
if (source.getName().equals(name)) return new ZipArchiveEntry(source);
ZipArchiveEntry ret = new ZipArchiveEntry(name);
byte[] extra = source.getExtra();
if (extra != null) {
ret.setExtraFields(ExtraFieldUtils.parse(extra, true,
ExtraFieldUtils
.UnparseableExtraField.READ));
} else {
ret.setExtra(ExtraFieldUtils.mergeLocalFileDataData(source.getExtraFields(true)));
}
ret.setInternalAttributes(source.getInternalAttributes());
ret.setExternalAttributes(source.getExternalAttributes());
ret.setExtraFields(source.getExtraFields(true));
ret.setCrc(source.getCrc());
ret.setMethod(source.getMethod());
ret.setSize(source.getSize());
return ret;
}
}