/* * 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.tasks; import com.android.annotations.NonNull; import com.android.build.gradle.internal.scope.ConventionMappingHelper; import com.android.build.gradle.internal.scope.TaskConfigAction; import com.android.build.gradle.internal.scope.VariantScope; import com.android.ide.common.packaging.PackagingUtils; import com.android.utils.FileUtils; import com.google.common.io.ByteStreams; import org.gradle.api.tasks.InputFiles; import org.gradle.api.tasks.OutputDirectory; import org.gradle.api.tasks.TaskAction; import org.gradle.api.tasks.incremental.IncrementalTaskInputs; import org.gradle.api.tasks.incremental.InputFileDetails; import java.io.BufferedOutputStream; import java.io.File; import java.io.FileOutputStream; import java.io.IOException; import java.io.InputStream; import java.io.OutputStream; import java.util.Enumeration; import java.util.Set; import java.util.concurrent.Callable; import java.util.jar.JarEntry; import java.util.jar.JarFile; /** * Extract all packaged jar files java resources into a directory. Each jar file will be extracted * in a jar specific folder, and only java resources are extracted. */ public class ExtractJavaResourcesTask extends DefaultAndroidTask { // the fact we use a SET is not right, we should have an ordered list of jars... // VariantConfiguration.getPackaged|ProvidedJars should use List<> @InputFiles public Set<File> jarInputFiles; @OutputDirectory public File outputDir; @InputFiles public Set<File> getJarInputFiles() { return jarInputFiles; } @TaskAction public void extractJavaResources(final IncrementalTaskInputs incrementalTaskInputs) { incrementalTaskInputs.outOfDate(new org.gradle.api.Action<InputFileDetails>() { @Override public void execute(InputFileDetails inputFileDetails) { File inputJar = inputFileDetails.getFile(); String folderName = inputJar.getName() + inputJar.getPath().hashCode(); File outputFolder = new File(outputDir, folderName); if (outputFolder.exists()) { try { FileUtils.deleteFolder(outputFolder); } catch (IOException e) { throw new RuntimeException(e); } } if (!outputFolder.mkdirs()) { throw new RuntimeException( "Cannot create folder to extract java resources in for " + inputJar.getAbsolutePath()); } // create the jar file visitor that will check for out-dated resources. JarFile jarFile = null; try { jarFile = new JarFile(inputJar); Enumeration<JarEntry> entries = jarFile.entries(); while (entries.hasMoreElements()) { JarEntry jarEntry = entries.nextElement(); if (!jarEntry.isDirectory()) { processJarEntry(jarFile, jarEntry, outputFolder); } } } catch (IOException e) { throw new RuntimeException(e); } finally { if (jarFile != null) { try { jarFile.close(); } catch (IOException e) { // ignore. } } } } }); incrementalTaskInputs.removed(new org.gradle.api.Action<InputFileDetails>() { @Override public void execute(InputFileDetails inputFileDetails) { File deletedJar = inputFileDetails.getFile(); String folderName = deletedJar.getName() + deletedJar.getPath().hashCode(); File outputFolder = new File(outputDir, folderName); if (outputFolder.exists()) { try { FileUtils.deleteFolder(outputFolder); } catch (IOException e) { throw new RuntimeException(e); } } } }); } /** * process one jar entry in an input jar file and optionally stores the entry in the output * folder. * @param jarFile the input jar file * @param jarEntry the jar entry in the jarFile to process * @param outputDir the output folder to use to copy/merge the entry in. * @throws IOException */ private static void processJarEntry(JarFile jarFile, JarEntry jarEntry, File outputDir) throws IOException { File outputFile = new File(outputDir, jarEntry.getName()); Action action = getAction(jarEntry.getName()); if (action == Action.COPY) { if (!outputFile.getParentFile().exists() && !outputFile.getParentFile().mkdirs()) { throw new RuntimeException("Cannot create directory " + outputFile.getParent()); } if (!outputFile.exists() || outputFile.lastModified() < jarEntry.getTime()) { InputStream inputStream = null; OutputStream outputStream = null; try { inputStream = jarFile.getInputStream(jarEntry); if (inputStream != null) { outputStream = new BufferedOutputStream( new FileOutputStream(outputFile)); ByteStreams.copy(inputStream, outputStream); outputStream.flush(); } else { throw new RuntimeException("Cannot copy " + jarEntry.getName()); } } finally { try { if (outputStream != null) { outputStream.close(); } } finally { if (inputStream != null) { inputStream.close(); } } } } } } /** * Define all possible actions for a Jar file entry. */ enum Action { /** * Copy the file to the output destination. */ COPY, /** * Ignore the file. */ IGNORE } /** * Provides an {@link Action} for the archive entry. * @param archivePath the archive entry path in the archive. * @return the action to implement. */ @NonNull public static Action getAction(@NonNull String archivePath) { // Manifest files are never merged. if (JarFile.MANIFEST_NAME.equals(archivePath)) { return Action.IGNORE; } // split the path into segments. String[] segments = archivePath.split("/"); // empty path? skip to next entry. if (segments.length == 0) { return Action.IGNORE; } // Check each folders to make sure they should be included. // Folders like CVS, .svn, etc.. should already have been excluded from the // jar file, but we need to exclude some other folder (like /META-INF) so // we check anyway. for (int i = 0 ; i < segments.length - 1; i++) { if (!PackagingUtils.checkFolderForPackaging(segments[i])) { return Action.IGNORE; } } // get the file name from the path String fileName = segments[segments.length-1]; return PackagingUtils.checkFileForPackaging(fileName) ? Action.COPY : Action.IGNORE; } public static class Config implements TaskConfigAction<ExtractJavaResourcesTask> { private final VariantScope scope; public Config(VariantScope scope) { this.scope = scope; } @Override public String getName() { return scope.getTaskName("extract", "PackagedLibrariesJavaResources"); } @Override public Class<ExtractJavaResourcesTask> getType() { return ExtractJavaResourcesTask.class; } @Override public void execute(ExtractJavaResourcesTask extractJavaResourcesTask) { ConventionMappingHelper.map(extractJavaResourcesTask, "jarInputFiles", new Callable<Set<File>>() { @Override public Set<File> call() throws Exception { return scope.getVariantConfiguration().getPackagedJars(); } }); extractJavaResourcesTask.outputDir = scope.getPackagedJarsJavaResDestinationDir(); extractJavaResourcesTask.setVariantName(scope.getVariantConfiguration().getFullName()); } } }