| /* |
| * 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; |
| } |
| } |