blob: 8ec9ac629a40e33af3fe15630c24f3d3df4212c1 [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.delete;
import static com.android.utils.FileUtils.emptyFolder;
import static com.android.utils.FileUtils.mkdirs;
import static com.google.common.base.Preconditions.checkNotNull;
import com.android.annotations.NonNull;
import com.android.build.api.transform.DirectoryInput;
import com.android.build.api.transform.Format;
import com.android.build.api.transform.JarInput;
import com.android.build.api.transform.QualifiedContent.ContentType;
import com.android.build.api.transform.QualifiedContent.DefaultContentType;
import com.android.build.api.transform.QualifiedContent.Scope;
import com.android.build.api.transform.Transform;
import com.android.build.api.transform.TransformException;
import com.android.build.api.transform.TransformInput;
import com.android.build.api.transform.TransformInvocation;
import com.android.build.api.transform.TransformOutputProvider;
import com.android.ide.common.internal.WaitableExecutor;
import com.android.ide.common.packaging.PackagingUtils;
import com.android.utils.FileUtils;
import com.google.common.io.ByteStreams;
import com.google.common.io.Closer;
import org.gradle.api.logging.Logger;
import org.gradle.api.logging.Logging;
import java.io.BufferedOutputStream;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
import java.util.Set;
import java.util.concurrent.Callable;
import java.util.jar.JarFile;
import java.util.zip.ZipEntry;
import java.util.zip.ZipInputStream;
/**
* Transform to extract jars.
*
*/
public class ExtractJarsTransform extends Transform {
@NonNull
private final Set<ContentType> contentTypes;
@NonNull
private final Set<Scope> scopes;
public ExtractJarsTransform(
@NonNull Set<ContentType> contentTypes,
@NonNull Set<Scope> scopes) {
this.contentTypes = contentTypes;
this.scopes = scopes;
}
@NonNull
@Override
public String getName() {
return "extractJars";
}
@NonNull
@Override
public Set<ContentType> getInputTypes() {
return contentTypes;
}
@NonNull
@Override
public Set<Scope> getScopes() {
return scopes;
}
@Override
public boolean isIncremental() {
return true;
}
@Override
public void transform(TransformInvocation transformInvocation)
throws IOException, TransformException, InterruptedException {
TransformOutputProvider outputProvider = transformInvocation.getOutputProvider();
boolean isIncremental = transformInvocation.isIncremental();
checkNotNull(outputProvider, "Missing output object for transform " + getName());
// as_input transform and no referenced scopes, all the inputs will in InputOutputStreams.
final boolean extractCode = contentTypes.contains(DefaultContentType.CLASSES);
Logger logger = Logging.getLogger(ExtractJarsTransform.class);
if (!isIncremental) {
outputProvider.deleteAll();
}
try {
WaitableExecutor<Void> executor = new WaitableExecutor<Void>();
for (TransformInput input : transformInvocation.getInputs()) {
for (DirectoryInput dirInput : input.getDirectoryInputs()) {
File dirOutput = outputProvider.getContentLocation(dirInput.getName()
+ "-" + dirInput.getFile().getAbsolutePath().hashCode(),
dirInput.getContentTypes(),
dirInput.getScopes(),
Format.DIRECTORY);
org.apache.commons.io.FileUtils.copyDirectory(dirInput.getFile(), dirOutput);
}
for (JarInput jarInput : input.getJarInputs()) {
final File jarFile = jarInput.getFile();
// create an output folder for this jar, keeping its type and scopes.
final File outJarFolder = outputProvider.getContentLocation(
jarFile.getName() + "-" + jarFile.getPath().hashCode(),
jarInput.getContentTypes(),
jarInput.getScopes(),
Format.DIRECTORY);
FileUtils.mkdirs(outJarFolder);
if (!isIncremental) {
executor.execute(new Callable<Void>() {
@Override
public Void call() throws Exception {
extractJar(outJarFolder, jarFile, extractCode);
return null;
}
});
} else {
switch (jarInput.getStatus()) {
case CHANGED:
executor.execute(new Callable<Void>() {
@Override
public Void call() throws Exception {
emptyFolder(outJarFolder);
extractJar(outJarFolder, jarFile, extractCode);
return null;
}
});
break;
case ADDED:
executor.execute(new Callable<Void>() {
@Override
public Void call() throws Exception {
extractJar(outJarFolder, jarFile, extractCode);
return null;
}
});
break;
case REMOVED:
executor.execute(new Callable<Void>() {
@Override
public Void call() throws Exception {
delete(outJarFolder);
return null;
}
});
break;
}
}
}
}
executor.waitForTasksWithQuickFail(true);
} catch (InterruptedException e) {
throw e;
} catch (Exception e) {
throw new TransformException(e);
}
}
private static void extractJar(
@NonNull File outJarFolder,
@NonNull File jarFile,
boolean extractCode) throws IOException {
mkdirs(outJarFolder);
Closer closer = Closer.create();
try {
FileInputStream fis = closer.register(new FileInputStream(jarFile));
ZipInputStream zis = closer.register(new ZipInputStream(fis));
// loop on the entries of the intermediary package and put them in the final package.
ZipEntry entry;
while ((entry = zis.getNextEntry()) != null) {
try {
String name = entry.getName();
// do not take directories
if (entry.isDirectory()) {
continue;
}
Action action = getAction(name, extractCode);
if (action == Action.COPY) {
File outputFile = new File(outJarFolder,
name.replace('/', File.separatorChar));
mkdirs(outputFile.getParentFile());
Closer closer2 = Closer.create();
try {
java.io.OutputStream outputStream = closer2.register(
new BufferedOutputStream(new FileOutputStream(outputFile)));
ByteStreams.copy(zis, outputStream);
outputStream.flush();
} finally {
closer2.close();
}
}
} finally {
zis.closeEntry();
}
}
} finally {
closer.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.
* @param extractCode whether to extractCode
* @return the action to implement.
*/
@NonNull
public static Action getAction(@NonNull String archivePath, boolean extractCode) {
// 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, extractCode)
? Action.COPY
: Action.IGNORE;
}
@NonNull
private static File getFolder(
@NonNull File outFolder,
@NonNull File jarFile) {
return new File(outFolder, jarFile.getName() + "-" + jarFile.getPath().hashCode());
}
}