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