blob: 7a83ed6d7696a1c808af35b981c82c12290b0e36 [file] [log] [blame]
// Copyright 2016 Google Inc. All rights reserved.
//
// 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 com.google.archivepatcher.generator;
import com.google.archivepatcher.generator.DefaultDeflateCompressionDiviner.DivinationResult;
import com.google.archivepatcher.shared.DeltaFriendlyFile;
import com.google.archivepatcher.shared.JreDeflateParameters;
import com.google.archivepatcher.shared.TypedRange;
import java.io.BufferedOutputStream;
import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
/**
* Prepares resources for differencing.
*/
public class PreDiffExecutor {
/** A helper class to build a {@link PreDiffExecutor} with a variety of configurations. */
public static final class Builder {
private File originalOldFile;
private File originalNewFile;
private File deltaFriendlyOldFile;
private File deltaFriendlyNewFile;
private List<RecommendationModifier> recommendationModifiers =
new ArrayList<RecommendationModifier>();
/**
* Sets the original, read-only input files to the patch generation process. This has to be
* called at least once, and both arguments must be non-null.
*
* @param originalOldFile the original old file to read (will not be modified).
* @param originalNewFile the original new file to read (will not be modified).
* @return this builder
*/
public Builder readingOriginalFiles(File originalOldFile, File originalNewFile) {
if (originalOldFile == null || originalNewFile == null) {
throw new IllegalStateException("do not set nul original input files");
}
this.originalOldFile = originalOldFile;
this.originalNewFile = originalNewFile;
return this;
}
/**
* Sets the output files that will hold the delta-friendly intermediate binaries used in patch
* generation. If called, both arguments must be non-null.
*
* @param deltaFriendlyOldFile the intermediate file to write (will be overwritten if it exists)
* @param deltaFriendlyNewFile the intermediate file to write (will be overwritten if it exists)
* @return this builder
*/
public Builder writingDeltaFriendlyFiles(File deltaFriendlyOldFile, File deltaFriendlyNewFile) {
if (deltaFriendlyOldFile == null || deltaFriendlyNewFile == null) {
throw new IllegalStateException("do not set null delta-friendly files");
}
this.deltaFriendlyOldFile = deltaFriendlyOldFile;
this.deltaFriendlyNewFile = deltaFriendlyNewFile;
return this;
}
/**
* Appends an optional {@link RecommendationModifier} to be used during the generation of the
* {@link PreDiffPlan} and/or delta-friendly blobs.
*
* @param recommendationModifier the modifier to set
* @return this builder
*/
public Builder withRecommendationModifier(RecommendationModifier recommendationModifier) {
if (recommendationModifier == null) {
throw new IllegalArgumentException("recommendationModifier cannot be null");
}
this.recommendationModifiers.add(recommendationModifier);
return this;
}
/**
* Builds and returns a {@link PreDiffExecutor} according to the currnet configuration.
*
* @return the executor
*/
public PreDiffExecutor build() {
if (originalOldFile == null) {
// readingOriginalFiles() ensures old and new are non-null when called, so check either.
throw new IllegalStateException("original input files cannot be null");
}
return new PreDiffExecutor(
originalOldFile,
originalNewFile,
deltaFriendlyOldFile,
deltaFriendlyNewFile,
recommendationModifiers);
}
}
/** The original old file to read (will not be modified). */
private final File originalOldFile;
/** The original new file to read (will not be modified). */
private final File originalNewFile;
/**
* Optional file to write the delta-friendly version of the original old file to (will be created,
* overwriting if it already exists). If null, only the read-only planning step can be performed.
*/
private final File deltaFriendlyOldFile;
/**
* Optional file to write the delta-friendly version of the original new file to (will be created,
* overwriting if it already exists). If null, only the read-only planning step can be performed.
*/
private final File deltaFriendlyNewFile;
/**
* Optional {@link RecommendationModifier}s to be used for modifying the patch to be generated.
*/
private final List<RecommendationModifier> recommendationModifiers;
/** Constructs a new PreDiffExecutor to work with the specified configuration. */
private PreDiffExecutor(
File originalOldFile,
File originalNewFile,
File deltaFriendlyOldFile,
File deltaFriendlyNewFile,
List<RecommendationModifier> recommendationModifiers) {
this.originalOldFile = originalOldFile;
this.originalNewFile = originalNewFile;
this.deltaFriendlyOldFile = deltaFriendlyOldFile;
this.deltaFriendlyNewFile = deltaFriendlyNewFile;
this.recommendationModifiers = recommendationModifiers;
}
/**
* Prepare resources for diffing and returns the completed plan.
*
* @return the plan
* @throws IOException if unable to complete the operation due to an I/O error
*/
public PreDiffPlan prepareForDiffing() throws IOException {
PreDiffPlan preDiffPlan = generatePreDiffPlan();
List<TypedRange<JreDeflateParameters>> deltaFriendlyNewFileRecompressionPlan = null;
if (deltaFriendlyOldFile != null) {
// Builder.writingDeltaFriendlyFiles() ensures old and new are non-null when called, so a
// check on either is sufficient.
deltaFriendlyNewFileRecompressionPlan =
Collections.unmodifiableList(generateDeltaFriendlyFiles(preDiffPlan));
}
return new PreDiffPlan(
preDiffPlan.getQualifiedRecommendations(),
preDiffPlan.getOldFileUncompressionPlan(),
preDiffPlan.getNewFileUncompressionPlan(),
deltaFriendlyNewFileRecompressionPlan);
}
/**
* Generate the delta-friendly files and return the plan for recompressing the delta-friendly new
* file back into the original new file.
*
* @param preDiffPlan the plan to execute
* @return as described
* @throws IOException if anything goes wrong
*/
private List<TypedRange<JreDeflateParameters>> generateDeltaFriendlyFiles(PreDiffPlan preDiffPlan)
throws IOException {
try (FileOutputStream out = new FileOutputStream(deltaFriendlyOldFile);
BufferedOutputStream bufferedOut = new BufferedOutputStream(out)) {
DeltaFriendlyFile.generateDeltaFriendlyFile(
preDiffPlan.getOldFileUncompressionPlan(), originalOldFile, bufferedOut);
}
try (FileOutputStream out = new FileOutputStream(deltaFriendlyNewFile);
BufferedOutputStream bufferedOut = new BufferedOutputStream(out)) {
return DeltaFriendlyFile.generateDeltaFriendlyFile(
preDiffPlan.getNewFileUncompressionPlan(), originalNewFile, bufferedOut);
}
}
/**
* Analyze the original old and new files and generate a plan to transform them into their
* delta-friendly equivalents.
*
* @return the plan, which does not yet contain information for recompressing the delta-friendly
* new archive.
* @throws IOException if anything goes wrong
*/
private PreDiffPlan generatePreDiffPlan() throws IOException {
Map<ByteArrayHolder, MinimalZipEntry> originalOldArchiveZipEntriesByPath =
new HashMap<ByteArrayHolder, MinimalZipEntry>();
Map<ByteArrayHolder, MinimalZipEntry> originalNewArchiveZipEntriesByPath =
new HashMap<ByteArrayHolder, MinimalZipEntry>();
Map<ByteArrayHolder, JreDeflateParameters> originalNewArchiveJreDeflateParametersByPath =
new HashMap<ByteArrayHolder, JreDeflateParameters>();
for (MinimalZipEntry zipEntry : MinimalZipArchive.listEntries(originalOldFile)) {
ByteArrayHolder key = new ByteArrayHolder(zipEntry.getFileNameBytes());
originalOldArchiveZipEntriesByPath.put(key, zipEntry);
}
DefaultDeflateCompressionDiviner diviner = new DefaultDeflateCompressionDiviner();
for (DivinationResult divinationResult : diviner.divineDeflateParameters(originalNewFile)) {
ByteArrayHolder key =
new ByteArrayHolder(divinationResult.minimalZipEntry.getFileNameBytes());
originalNewArchiveZipEntriesByPath.put(key, divinationResult.minimalZipEntry);
originalNewArchiveJreDeflateParametersByPath.put(key, divinationResult.divinedParameters);
}
PreDiffPlanner preDiffPlanner =
new PreDiffPlanner(
originalOldFile,
originalOldArchiveZipEntriesByPath,
originalNewFile,
originalNewArchiveZipEntriesByPath,
originalNewArchiveJreDeflateParametersByPath,
recommendationModifiers.toArray(new RecommendationModifier[] {}));
return preDiffPlanner.generatePreDiffPlan();
}
}