blob: ead69cfce031e0abceb23430f623ff7315e36b8d [file] [log] [blame]
/*
* Copyright (C) 2015 The Android Open Source Project
*
* 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.android.build.gradle.internal.transforms;
import static com.android.utils.FileUtils.deleteIfExists;
import static com.android.utils.FileUtils.mkdirs;
import com.android.annotations.NonNull;
import com.android.annotations.Nullable;
import com.android.annotations.VisibleForTesting;
import com.android.build.gradle.internal.dsl.PackagingOptions;
import com.android.build.gradle.internal.packaging.PackagingFileAction;
import com.android.build.gradle.internal.packaging.ParsedPackagingOptions;
import com.android.builder.packaging.ZipAbortException;
import com.android.builder.packaging.ZipEntryFilter;
import com.google.common.base.Joiner;
import com.google.common.collect.ImmutableList;
import com.google.common.io.Files;
import java.io.BufferedOutputStream;
import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.OutputStream;
import java.util.List;
/**
* Defines a file filter contract which will use {@link PackagingOptions} to take appropriate
* action.
*/
@VisibleForTesting
public class FileFilter implements ZipEntryFilter {
public static class SubStream {
private final File folder;
private final String name;
SubStream(File folder, String name) {
this.folder = folder;
this.name = name;
}
public File getFolder() {
return folder;
}
public String getName() {
return name;
}
}
@Nullable
private final ParsedPackagingOptions packagingOptions;
@NonNull
private final List<SubStream> expandedFolders;
public FileFilter(
@NonNull List<SubStream> expandedFolders,
@Nullable PackagingOptions packagingOptions) {
this.expandedFolders = ImmutableList.copyOf(expandedFolders);
this.packagingOptions = new ParsedPackagingOptions(packagingOptions);
}
/**
* Implementation of the {@link ZipEntryFilter} contract which only
* cares about copying or ignoring files since merging is handled differently.
* @param archivePath the archive file path of the entry
* @return true if the archive entry satisfies the filter, false otherwise.
* @throws ZipAbortException
*/
@Override
public boolean checkEntry(@NonNull String archivePath)
throws ZipAbortException {
PackagingFileAction action = getPackagingAction(archivePath);
switch(action) {
case EXCLUDE:
return false;
case PICK_FIRST:
List<File> allFiles = getAllFiles(archivePath);
return allFiles.isEmpty();
case MERGE:
case NONE:
return true;
default:
throw new RuntimeException("Unhandled action " + action);
}
}
/**
* Notification of an incremental file changed since last successful run of the task.
*
* Usually, we just copy the changed file into the merged folder. However, if the user
* specified {@link PackagingFileAction#PICK_FIRST}, the file will only be copied if it the
* first pick. Also, if the user specified {@link PackagingFileAction#MERGE}, all the files
* with the same entry archive path will be re-merged.
*
* @param outputDir merged resources folder.
* @param changedFile changed file located in a temporary expansion folder
* @throws IOException
*/
void handleChanged(@NonNull File outputDir, @NonNull File changedFile)
throws IOException {
String archivePath = getArchivePath(changedFile);
PackagingFileAction action = getPackagingAction(archivePath);
switch (action) {
case EXCLUDE:
return;
case MERGE:
// one of the merged file has changed, re-merge all of them.
mergeAll(outputDir, archivePath);
return;
case PICK_FIRST:
copy(changedFile, outputDir, archivePath);
return;
case NONE:
copy(changedFile, outputDir, archivePath);
}
}
/**
* Notification of a file removal.
*
* file was removed, we need to check that it was not a pickFirst item (since we
* may now need to pick another one) or a merged item since we would need to re-merge
* all remaining items.
*
* @param outputDir expected merged output directory.
* @param removedFilePath removed file path from the temporary resources folders.
* @throws IOException
*/
public void handleRemoved(@NonNull File outputDir, @NonNull String removedFilePath)
throws IOException {
// first delete the output file, it will be eventually replaced.
File outFile = new File(outputDir, removedFilePath);
if (outFile.exists()) {
if (!outFile.delete()) {
throw new IOException("Cannot delete " + outFile.getAbsolutePath());
}
}
PackagingFileAction itemAction = getPackagingAction(removedFilePath);
switch(itemAction) {
case NONE:
case PICK_FIRST:
// this was a picked up item, make sure we copy the first still available
com.google.common.base.Optional<File> firstPick = getFirstPick(removedFilePath);
if (firstPick.isPresent()) {
copy(firstPick.get(), outputDir, removedFilePath);
}
return;
case MERGE:
// re-merge all
mergeAll(outputDir, removedFilePath);
return;
case EXCLUDE:
// do nothing
return;
default:
throw new RuntimeException("Unhandled package option"
+ itemAction);
}
}
private static void copy(@NonNull File inputFile,
@NonNull File outputDir,
@NonNull String archivePath) throws IOException {
File outputFile = new File(outputDir, archivePath);
mkdirs(outputFile.getParentFile());
Files.copy(inputFile, outputFile);
}
private void mergeAll(@NonNull File outputDir, @NonNull String archivePath)
throws IOException {
File outputFile = new File(outputDir, archivePath);
deleteIfExists(outputFile);
mkdirs(outputFile.getParentFile());
List<File> allFiles = getAllFiles(archivePath);
if (!allFiles.isEmpty()) {
OutputStream os = null;
try {
os = new BufferedOutputStream(new FileOutputStream(outputFile));
// take each file in order and merge them.
for (File file : allFiles) {
Files.copy(file, os);
}
} finally {
if (os != null) {
os.close();
}
}
}
}
/**
* Return the first file from the temporary expansion folders that satisfy the archive path.
* @param archivePath the entry archive path.
* @return the first file reference of {@link com.google.common.base.Optional#absent()} if
* none exist in any temporary expansion folders.
*/
@NonNull
private com.google.common.base.Optional<File> getFirstPick(
@NonNull final String archivePath) {
return com.google.common.base.Optional.fromNullable(
forEachExpansionFolder(new FolderAction() {
@Nullable
@Override
public File on(File folder) {
File expandedFile = new File(folder, archivePath);
if (expandedFile.exists()) {
return expandedFile;
}
return null;
}
}));
}
/**
* Returns all files from temporary expansion folders with the same archive path.
* @param archivePath the entry archive path.
* @return a list possibly empty of {@link File}s that satisfy the archive path.
*/
@NonNull
private List<File> getAllFiles(@NonNull final String archivePath) {
final ImmutableList.Builder<File> matchingFiles = ImmutableList.builder();
forEachExpansionFolder(new FolderAction() {
@Nullable
@Override
public File on(File folder) {
File expandedFile = new File(folder, archivePath);
if (expandedFile.exists()) {
matchingFiles.add(expandedFile);
}
return null;
}
});
return matchingFiles.build();
}
/**
* An action on a folder.
*/
private interface FolderAction {
/**
* Perform an action on a folder and stop the processing if something is returned
* @param folder the folder to perform the action on.
* @return a file to stop processing or null to continue to the next expansion folder
* if any.
*/
@Nullable
File on(File folder);
}
/**
* Perform the passed action on each expansion folder.
* @param action the action to perform on each folder.
* @return a file if any action returned a value, or null if none returned a value.
*/
@Nullable
private File forEachExpansionFolder(@NonNull FolderAction action) {
for (SubStream subStream : expandedFolders) {
if (subStream.getFolder().isDirectory()) {
File value = action.on(subStream.getFolder());
if (value != null) {
return value;
}
}
}
return null;
}
/**
* Returns the expansion folder for an expanded file. This represents the location
* where the packaged jar our source directories java resources were extracted into.
* @param expandedFile the java resource file.
* @return the expansion folder used to extract the java resource into.
*/
@NonNull
private File getExpansionFolder(@NonNull final File expandedFile) {
File expansionFolder = forEachExpansionFolder(new FolderAction() {
@Nullable
@Override
public File on(File folder) {
return expandedFile.getAbsolutePath().startsWith(folder.getAbsolutePath())
? folder : null;
}
});
if (expansionFolder == null) {
throw new RuntimeException("Cannot determine expansion folder for " + expandedFile
+ " with folders " + Joiner.on(",").join(expandedFolders));
}
return expansionFolder;
}
/**
* Determines the archive entry path relative to its expansion folder. The archive entry
* path is the path that was used to save the entry in the original .jar file that got
* expanded in the expansion folder.
* @param expandedFile the expanded file to find the relative archive entry from.
* @return the expanded file relative path to its expansion folder.
*/
@NonNull
private String getArchivePath(@NonNull File expandedFile) {
File expansionFolder = getExpansionFolder(expandedFile);
return expandedFile.getAbsolutePath()
.substring(expansionFolder.getAbsolutePath().length() + 1);
}
/**
* Determine the user's intention for a particular archive entry.
* @param archivePath the archive entry
* @return a {@link PackagingFileAction} as provided by the user in the build.gradle
*/
@NonNull
private PackagingFileAction getPackagingAction(@NonNull String archivePath) {
if (packagingOptions != null) {
return packagingOptions.getAction(archivePath);
}
return PackagingFileAction.NONE;
}
}