blob: 584d84c06c28fa75a74004bafb728e245d9d5368 [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 android.databinding.tool;
import com.google.common.base.Preconditions;
import com.android.build.gradle.AppExtension;
import com.android.build.gradle.BaseExtension;
import com.android.build.gradle.LibraryExtension;
import com.android.build.gradle.api.ApplicationVariant;
import com.android.build.gradle.api.LibraryVariant;
import com.android.build.gradle.api.TestVariant;
import com.android.build.gradle.internal.api.ApplicationVariantImpl;
import com.android.build.gradle.internal.api.LibraryVariantImpl;
import com.android.build.gradle.internal.api.TestVariantImpl;
import com.android.build.gradle.internal.core.GradleVariantConfiguration;
import com.android.build.gradle.internal.variant.ApplicationVariantData;
import com.android.build.gradle.internal.variant.BaseVariantData;
import com.android.build.gradle.internal.variant.LibraryVariantData;
import com.android.build.gradle.internal.variant.TestVariantData;
import com.android.build.gradle.tasks.ProcessAndroidResources;
import com.android.builder.model.ApiVersion;
import org.apache.commons.io.IOUtils;
import org.apache.commons.lang3.StringUtils;
import org.apache.commons.lang3.exception.ExceptionUtils;
import org.gradle.api.Action;
import org.gradle.api.Plugin;
import org.gradle.api.Project;
import org.gradle.api.Task;
import org.gradle.api.logging.LogLevel;
import org.gradle.api.logging.Logger;
import org.gradle.api.plugins.ExtraPropertiesExtension;
import org.gradle.api.tasks.bundling.Jar;
import org.gradle.api.tasks.compile.AbstractCompile;
import android.databinding.tool.processing.ScopedException;
import android.databinding.tool.util.L;
import android.databinding.tool.writer.JavaFileWriter;
import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.lang.reflect.Field;
import java.util.Arrays;
import java.util.List;
import javax.tools.Diagnostic;
import javax.xml.bind.JAXBException;
public class DataBinderPlugin implements Plugin<Project> {
private static final String INVOKED_FROM_IDE_PROPERTY = "android.injected.invoked.from.ide";
private static final String PRINT_ENCODED_ERRORS_PROPERTY
= "android.databinding.injected.print.encoded.errors";
private Logger logger;
private boolean printEncodedErrors = false;
class GradleFileWriter extends JavaFileWriter {
private final String outputBase;
public GradleFileWriter(String outputBase) {
this.outputBase = outputBase;
}
@Override
public void writeToFile(String canonicalName, String contents) {
String asPath = canonicalName.replace('.', '/');
File f = new File(outputBase + "/" + asPath + ".java");
logD("Asked to write to " + canonicalName + ". outputting to:" +
f.getAbsolutePath());
//noinspection ResultOfMethodCallIgnored
f.getParentFile().mkdirs();
FileOutputStream fos = null;
try {
fos = new FileOutputStream(f);
IOUtils.write(contents, fos);
} catch (IOException e) {
logE(e, "cannot write file " + f.getAbsolutePath());
} finally {
IOUtils.closeQuietly(fos);
}
}
}
private boolean safeGetBooleanProperty(Project project, String property) {
boolean hasProperty = project.hasProperty(property);
if (!hasProperty) {
return false;
}
try {
if (Boolean.parseBoolean(String.valueOf(project.getProperties().get(property)))) {
return true;
}
} catch (Throwable t) {
L.w("unable to read property %s", project);
}
return false;
}
private boolean resolvePrintEncodedErrors(Project project) {
return safeGetBooleanProperty(project, INVOKED_FROM_IDE_PROPERTY) ||
safeGetBooleanProperty(project, PRINT_ENCODED_ERRORS_PROPERTY);
}
@Override
public void apply(Project project) {
if (project == null) {
return;
}
setupLogger(project);
String myVersion = readMyVersion();
logD("data binding plugin version is %s", myVersion);
if (StringUtils.isEmpty(myVersion)) {
throw new IllegalStateException("cannot read version of the plugin :/");
}
printEncodedErrors = resolvePrintEncodedErrors(project);
ScopedException.encodeOutput(printEncodedErrors);
project.getDependencies().add("compile", "com.android.databinding:library:" + myVersion);
boolean addAdapters = true;
if (project.hasProperty("ext")) {
Object ext = project.getProperties().get("ext");
if (ext instanceof ExtraPropertiesExtension) {
ExtraPropertiesExtension propExt = (ExtraPropertiesExtension) ext;
if (propExt.has("addDataBindingAdapters")) {
addAdapters = Boolean.parseBoolean(
String.valueOf(propExt.get("addDataBindingAdapters")));
}
}
}
if (addAdapters) {
project.getDependencies()
.add("compile", "com.android.databinding:adapters:" + myVersion);
}
project.getDependencies().add("provided", "com.android.databinding:compiler:" + myVersion);
project.afterEvaluate(new Action<Project>() {
@Override
public void execute(Project project) {
try {
createXmlProcessor(project);
} catch (Throwable t) {
logE(t, "failed to setup data binding");
}
}
});
}
private void setupLogger(Project project) {
logger = project.getLogger();
L.setClient(new L.Client() {
@Override
public void printMessage(Diagnostic.Kind kind, String message) {
if (kind == Diagnostic.Kind.ERROR) {
logE(null, message);
} else {
logD(message);
}
}
});
}
String readMyVersion() {
try {
InputStream stream = getClass().getResourceAsStream("/data_binding_build_info");
try {
return IOUtils.toString(stream, "utf-8").trim();
} finally {
IOUtils.closeQuietly(stream);
}
} catch (IOException exception) {
logE(exception, "Cannot read data binding version");
}
return null;
}
private void createXmlProcessor(Project project)
throws NoSuchFieldException, IllegalAccessException {
L.d("creating xml processor for " + project);
Object androidExt = project.getExtensions().getByName("android");
if (!(androidExt instanceof BaseExtension)) {
return;
}
if (androidExt instanceof AppExtension) {
createXmlProcessorForApp(project, (AppExtension) androidExt);
} else if (androidExt instanceof LibraryExtension) {
createXmlProcessorForLibrary(project, (LibraryExtension) androidExt);
} else {
logE(new UnsupportedOperationException("cannot understand android ext"),
"unsupported android extension. What is it? %s", androidExt);
}
}
private void createXmlProcessorForLibrary(Project project, LibraryExtension lib)
throws NoSuchFieldException, IllegalAccessException {
File sdkDir = lib.getSdkDirectory();
L.d("create xml processor for " + lib);
for (TestVariant variant : lib.getTestVariants()) {
logD("test variant %s. dir name %s", variant, variant.getDirName());
BaseVariantData variantData = getVariantData(variant);
attachXmlProcessor(project, variantData, sdkDir, false);//tests extend apk variant
}
for (LibraryVariant variant : lib.getLibraryVariants()) {
logD("library variant %s. dir name %s", variant, variant.getDirName());
BaseVariantData variantData = getVariantData(variant);
attachXmlProcessor(project, variantData, sdkDir, true);
}
}
private void createXmlProcessorForApp(Project project, AppExtension appExt)
throws NoSuchFieldException, IllegalAccessException {
L.d("create xml processor for " + appExt);
File sdkDir = appExt.getSdkDirectory();
for (TestVariant testVariant : appExt.getTestVariants()) {
TestVariantData variantData = getVariantData(testVariant);
attachXmlProcessor(project, variantData, sdkDir, false);
}
for (ApplicationVariant appVariant : appExt.getApplicationVariants()) {
ApplicationVariantData variantData = getVariantData(appVariant);
attachXmlProcessor(project, variantData, sdkDir, false);
}
}
private LibraryVariantData getVariantData(LibraryVariant variant)
throws NoSuchFieldException, IllegalAccessException {
Field field = LibraryVariantImpl.class.getDeclaredField("variantData");
field.setAccessible(true);
return (LibraryVariantData) field.get(variant);
}
private TestVariantData getVariantData(TestVariant variant)
throws IllegalAccessException, NoSuchFieldException {
Field field = TestVariantImpl.class.getDeclaredField("variantData");
field.setAccessible(true);
return (TestVariantData) field.get(variant);
}
private ApplicationVariantData getVariantData(ApplicationVariant variant)
throws IllegalAccessException, NoSuchFieldException {
Field field = ApplicationVariantImpl.class.getDeclaredField("variantData");
field.setAccessible(true);
return (ApplicationVariantData) field.get(variant);
}
private void attachXmlProcessor(Project project, final BaseVariantData variantData,
final File sdkDir,
final Boolean isLibrary) {
final GradleVariantConfiguration configuration = variantData.getVariantConfiguration();
final ApiVersion minSdkVersion = configuration.getMinSdkVersion();
ProcessAndroidResources generateRTask = variantData.generateRClassTask;
final String packageName = generateRTask.getPackageForR();
String fullName = configuration.getFullName();
List<File> resourceFolders = Arrays.asList(variantData.mergeResourcesTask.getOutputDir());
final File codeGenTargetFolder = new File(project.getBuildDir() + "/data-binding-info/" +
configuration.getDirName());
String writerOutBase = codeGenTargetFolder.getAbsolutePath();
JavaFileWriter fileWriter = new GradleFileWriter(writerOutBase);
final LayoutXmlProcessor xmlProcessor = new LayoutXmlProcessor(packageName, resourceFolders,
fileWriter, minSdkVersion.getApiLevel(), isLibrary);
final ProcessAndroidResources processResTask = generateRTask;
final File xmlOutDir = new File(project.getBuildDir() + "/layout-info/" +
configuration.getDirName());
final File generatedClassListOut = isLibrary ? new File(xmlOutDir, "_generated.txt") : null;
logD("xml output for %s is %s", variantData, xmlOutDir);
String layoutTaskName = "dataBindingLayouts" + StringUtils
.capitalize(processResTask.getName());
String infoClassTaskName = "dataBindingInfoClass" + StringUtils
.capitalize(processResTask.getName());
final DataBindingProcessLayoutsTask[] processLayoutsTasks
= new DataBindingProcessLayoutsTask[1];
project.getTasks().create(layoutTaskName,
DataBindingProcessLayoutsTask.class,
new Action<DataBindingProcessLayoutsTask>() {
@Override
public void execute(final DataBindingProcessLayoutsTask task) {
processLayoutsTasks[0] = task;
task.setXmlProcessor(xmlProcessor);
task.setSdkDir(sdkDir);
task.setXmlOutFolder(xmlOutDir);
task.setMinSdk(minSdkVersion.getApiLevel());
logD("TASK adding dependency on %s for %s", task, processResTask);
processResTask.dependsOn(task);
processResTask.getInputs().dir(xmlOutDir);
for (Object dep : processResTask.getDependsOn()) {
if (dep == task) {
continue;
}
logD("adding dependency on %s for %s", dep, task);
task.dependsOn(dep);
}
processResTask.doLast(new Action<Task>() {
@Override
public void execute(Task unused) {
try {
task.writeLayoutXmls();
} catch (JAXBException e) {
// gradle sometimes fails to resolve JAXBException.
// We get stack trace manually to ensure we have the log
logE(e, "cannot write layout xmls %s",
ExceptionUtils.getStackTrace(e));
}
}
});
}
});
final DataBindingProcessLayoutsTask processLayoutsTask = processLayoutsTasks[0];
project.getTasks().create(infoClassTaskName,
DataBindingExportInfoTask.class,
new Action<DataBindingExportInfoTask>() {
@Override
public void execute(DataBindingExportInfoTask task) {
task.dependsOn(processLayoutsTask);
task.dependsOn(processResTask);
task.setXmlProcessor(xmlProcessor);
task.setSdkDir(sdkDir);
task.setXmlOutFolder(xmlOutDir);
task.setExportClassListTo(generatedClassListOut);
task.setPrintEncodedErrors(printEncodedErrors);
task.setEnableDebugLogs(logger.isEnabled(LogLevel.DEBUG));
variantData.registerJavaGeneratingTask(task, codeGenTargetFolder);
}
});
String packageJarTaskName = "package" + StringUtils.capitalize(fullName) + "Jar";
final Task packageTask = project.getTasks().findByName(packageJarTaskName);
if (packageTask instanceof Jar) {
String removeGeneratedTaskName = "dataBindingExcludeGeneratedFrom" +
StringUtils.capitalize(packageTask.getName());
if (project.getTasks().findByName(removeGeneratedTaskName) == null) {
final AbstractCompile javaCompileTask = variantData.javacTask;
Preconditions.checkNotNull(javaCompileTask);
project.getTasks().create(removeGeneratedTaskName,
DataBindingExcludeGeneratedTask.class,
new Action<DataBindingExcludeGeneratedTask>() {
@Override
public void execute(DataBindingExcludeGeneratedTask task) {
packageTask.dependsOn(task);
task.dependsOn(javaCompileTask);
task.setAppPackage(packageName);
task.setInfoClassQualifiedName(xmlProcessor.getInfoClassFullName());
task.setPackageTask((Jar) packageTask);
task.setLibrary(isLibrary);
task.setGeneratedClassListFile(generatedClassListOut);
}
});
}
}
}
private void logD(String s, Object... args) {
logger.info(formatLog(s, args));
}
private void logE(Throwable t, String s, Object... args) {
logger.error(formatLog(s, args), t);
}
private String formatLog(String s, Object... args) {
return "[data binding plugin]: " + String.format(s, args);
}
}