Home | History | Annotate | Download | only in tool
      1 /*
      2  * Copyright (C) 2015 The Android Open Source Project
      3  *
      4  * Licensed under the Apache License, Version 2.0 (the "License");
      5  * you may not use this file except in compliance with the License.
      6  * You may obtain a copy of the License at
      7  *
      8  *      http://www.apache.org/licenses/LICENSE-2.0
      9  *
     10  * Unless required by applicable law or agreed to in writing, software
     11  * distributed under the License is distributed on an "AS IS" BASIS,
     12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
     13  * See the License for the specific language governing permissions and
     14  * limitations under the License.
     15  */
     16 package android.databinding.tool;
     17 
     18 import com.google.common.base.Preconditions;
     19 
     20 import com.android.build.gradle.AppExtension;
     21 import com.android.build.gradle.BaseExtension;
     22 import com.android.build.gradle.LibraryExtension;
     23 import com.android.build.gradle.api.ApplicationVariant;
     24 import com.android.build.gradle.api.LibraryVariant;
     25 import com.android.build.gradle.api.TestVariant;
     26 import com.android.build.gradle.internal.api.ApplicationVariantImpl;
     27 import com.android.build.gradle.internal.api.LibraryVariantImpl;
     28 import com.android.build.gradle.internal.api.TestVariantImpl;
     29 import com.android.build.gradle.internal.core.GradleVariantConfiguration;
     30 import com.android.build.gradle.internal.variant.ApplicationVariantData;
     31 import com.android.build.gradle.internal.variant.BaseVariantData;
     32 import com.android.build.gradle.internal.variant.LibraryVariantData;
     33 import com.android.build.gradle.internal.variant.TestVariantData;
     34 import com.android.build.gradle.tasks.ProcessAndroidResources;
     35 import com.android.builder.model.ApiVersion;
     36 
     37 import org.apache.commons.io.IOUtils;
     38 import org.apache.commons.lang3.StringUtils;
     39 import org.apache.commons.lang3.exception.ExceptionUtils;
     40 import org.gradle.api.Action;
     41 import org.gradle.api.Plugin;
     42 import org.gradle.api.Project;
     43 import org.gradle.api.Task;
     44 import org.gradle.api.logging.LogLevel;
     45 import org.gradle.api.logging.Logger;
     46 import org.gradle.api.plugins.ExtraPropertiesExtension;
     47 import org.gradle.api.tasks.bundling.Jar;
     48 import org.gradle.api.tasks.compile.AbstractCompile;
     49 
     50 import android.databinding.tool.processing.ScopedException;
     51 import android.databinding.tool.util.L;
     52 import android.databinding.tool.writer.JavaFileWriter;
     53 
     54 import java.io.File;
     55 import java.io.FileOutputStream;
     56 import java.io.IOException;
     57 import java.io.InputStream;
     58 import java.lang.reflect.Field;
     59 import java.util.Arrays;
     60 import java.util.List;
     61 
     62 import javax.tools.Diagnostic;
     63 import javax.xml.bind.JAXBException;
     64 
     65 public class DataBinderPlugin implements Plugin<Project> {
     66 
     67     private static final String INVOKED_FROM_IDE_PROPERTY = "android.injected.invoked.from.ide";
     68     private static final String PRINT_ENCODED_ERRORS_PROPERTY
     69             = "android.databinding.injected.print.encoded.errors";
     70     private Logger logger;
     71     private boolean printEncodedErrors = false;
     72 
     73     class GradleFileWriter extends JavaFileWriter {
     74 
     75         private final String outputBase;
     76 
     77         public GradleFileWriter(String outputBase) {
     78             this.outputBase = outputBase;
     79         }
     80 
     81         @Override
     82         public void writeToFile(String canonicalName, String contents) {
     83             String asPath = canonicalName.replace('.', '/');
     84             File f = new File(outputBase + "/" + asPath + ".java");
     85             logD("Asked to write to " + canonicalName + ". outputting to:" +
     86                     f.getAbsolutePath());
     87             //noinspection ResultOfMethodCallIgnored
     88             f.getParentFile().mkdirs();
     89             FileOutputStream fos = null;
     90             try {
     91                 fos = new FileOutputStream(f);
     92                 IOUtils.write(contents, fos);
     93             } catch (IOException e) {
     94                 logE(e, "cannot write file " + f.getAbsolutePath());
     95             } finally {
     96                 IOUtils.closeQuietly(fos);
     97             }
     98         }
     99     }
    100 
    101     private boolean safeGetBooleanProperty(Project project, String property) {
    102         boolean hasProperty = project.hasProperty(property);
    103         if (!hasProperty) {
    104             return false;
    105         }
    106         try {
    107             if (Boolean.parseBoolean(String.valueOf(project.getProperties().get(property)))) {
    108                 return true;
    109             }
    110         } catch (Throwable t) {
    111             L.w("unable to read property %s", project);
    112         }
    113         return false;
    114     }
    115 
    116     private boolean resolvePrintEncodedErrors(Project project) {
    117         return safeGetBooleanProperty(project, INVOKED_FROM_IDE_PROPERTY) ||
    118                 safeGetBooleanProperty(project, PRINT_ENCODED_ERRORS_PROPERTY);
    119     }
    120 
    121     @Override
    122     public void apply(Project project) {
    123         if (project == null) {
    124             return;
    125         }
    126         setupLogger(project);
    127 
    128         String myVersion = readMyVersion();
    129         logD("data binding plugin version is %s", myVersion);
    130         if (StringUtils.isEmpty(myVersion)) {
    131             throw new IllegalStateException("cannot read version of the plugin :/");
    132         }
    133         printEncodedErrors = resolvePrintEncodedErrors(project);
    134         ScopedException.encodeOutput(printEncodedErrors);
    135         project.getDependencies().add("compile", "com.android.databinding:library:" + myVersion);
    136         boolean addAdapters = true;
    137         if (project.hasProperty("ext")) {
    138             Object ext = project.getProperties().get("ext");
    139             if (ext instanceof ExtraPropertiesExtension) {
    140                 ExtraPropertiesExtension propExt = (ExtraPropertiesExtension) ext;
    141                 if (propExt.has("addDataBindingAdapters")) {
    142                     addAdapters = Boolean.valueOf(
    143                             String.valueOf(propExt.get("addDataBindingAdapters")));
    144                 }
    145             }
    146         }
    147         if (addAdapters) {
    148             project.getDependencies()
    149                     .add("compile", "com.android.databinding:adapters:" + myVersion);
    150         }
    151         project.getDependencies().add("provided", "com.android.databinding:compiler:" + myVersion);
    152         project.afterEvaluate(new Action<Project>() {
    153             @Override
    154             public void execute(Project project) {
    155                 try {
    156                     createXmlProcessor(project);
    157                 } catch (Throwable t) {
    158                     logE(t, "failed to setup data binding");
    159                 }
    160             }
    161         });
    162     }
    163 
    164     private void setupLogger(Project project) {
    165         logger = project.getLogger();
    166         L.setClient(new L.Client() {
    167             @Override
    168             public void printMessage(Diagnostic.Kind kind, String message) {
    169                 if (kind == Diagnostic.Kind.ERROR) {
    170                     logE(null, message);
    171                 } else {
    172                     logD(message);
    173                 }
    174             }
    175         });
    176     }
    177 
    178     String readMyVersion() {
    179         try {
    180             InputStream stream = getClass().getResourceAsStream("/data_binding_build_info");
    181             try {
    182                 return IOUtils.toString(stream, "utf-8").trim();
    183             } finally {
    184                 IOUtils.closeQuietly(stream);
    185             }
    186         } catch (IOException exception) {
    187             logE(exception, "Cannot read data binding version");
    188         }
    189         return null;
    190     }
    191 
    192     private void createXmlProcessor(Project project)
    193             throws NoSuchFieldException, IllegalAccessException {
    194         L.d("creating xml processor for " + project);
    195         Object androidExt = project.getExtensions().getByName("android");
    196         if (!(androidExt instanceof BaseExtension)) {
    197             return;
    198         }
    199         if (androidExt instanceof AppExtension) {
    200             createXmlProcessorForApp(project, (AppExtension) androidExt);
    201         } else if (androidExt instanceof LibraryExtension) {
    202             createXmlProcessorForLibrary(project, (LibraryExtension) androidExt);
    203         } else {
    204             logE(new UnsupportedOperationException("cannot understand android ext"),
    205                     "unsupported android extension. What is it? %s", androidExt);
    206         }
    207     }
    208 
    209     private void createXmlProcessorForLibrary(Project project, LibraryExtension lib)
    210             throws NoSuchFieldException, IllegalAccessException {
    211         File sdkDir = lib.getSdkDirectory();
    212         L.d("create xml processor for " + lib);
    213         for (TestVariant variant : lib.getTestVariants()) {
    214             logD("test variant %s. dir name %s", variant, variant.getDirName());
    215             BaseVariantData variantData = getVariantData(variant);
    216             attachXmlProcessor(project, variantData, sdkDir, false);//tests extend apk variant
    217         }
    218         for (LibraryVariant variant : lib.getLibraryVariants()) {
    219             logD("library variant %s. dir name %s", variant, variant.getDirName());
    220             BaseVariantData variantData = getVariantData(variant);
    221             attachXmlProcessor(project, variantData, sdkDir, true);
    222         }
    223     }
    224 
    225     private void createXmlProcessorForApp(Project project, AppExtension appExt)
    226             throws NoSuchFieldException, IllegalAccessException {
    227         L.d("create xml processor for " + appExt);
    228         File sdkDir = appExt.getSdkDirectory();
    229         for (TestVariant testVariant : appExt.getTestVariants()) {
    230             TestVariantData variantData = getVariantData(testVariant);
    231             attachXmlProcessor(project, variantData, sdkDir, false);
    232         }
    233         for (ApplicationVariant appVariant : appExt.getApplicationVariants()) {
    234             ApplicationVariantData variantData = getVariantData(appVariant);
    235             attachXmlProcessor(project, variantData, sdkDir, false);
    236         }
    237     }
    238 
    239     private LibraryVariantData getVariantData(LibraryVariant variant)
    240             throws NoSuchFieldException, IllegalAccessException {
    241         Field field = LibraryVariantImpl.class.getDeclaredField("variantData");
    242         field.setAccessible(true);
    243         return (LibraryVariantData) field.get(variant);
    244     }
    245 
    246     private TestVariantData getVariantData(TestVariant variant)
    247             throws IllegalAccessException, NoSuchFieldException {
    248         Field field = TestVariantImpl.class.getDeclaredField("variantData");
    249         field.setAccessible(true);
    250         return (TestVariantData) field.get(variant);
    251     }
    252 
    253     private ApplicationVariantData getVariantData(ApplicationVariant variant)
    254             throws IllegalAccessException, NoSuchFieldException {
    255         Field field = ApplicationVariantImpl.class.getDeclaredField("variantData");
    256         field.setAccessible(true);
    257         return (ApplicationVariantData) field.get(variant);
    258     }
    259 
    260     private void attachXmlProcessor(Project project, final BaseVariantData variantData,
    261             final File sdkDir,
    262             final Boolean isLibrary) {
    263         final GradleVariantConfiguration configuration = variantData.getVariantConfiguration();
    264         final ApiVersion minSdkVersion = configuration.getMinSdkVersion();
    265         ProcessAndroidResources generateRTask = variantData.generateRClassTask;
    266         final String packageName = generateRTask.getPackageForR();
    267         String fullName = configuration.getFullName();
    268         List<File> resourceFolders = Arrays.asList(variantData.mergeResourcesTask.getOutputDir());
    269 
    270         final File codeGenTargetFolder = new File(project.getBuildDir() + "/data-binding-info/" +
    271                 configuration.getDirName());
    272         String writerOutBase = codeGenTargetFolder.getAbsolutePath();
    273         JavaFileWriter fileWriter = new GradleFileWriter(writerOutBase);
    274         final LayoutXmlProcessor xmlProcessor = new LayoutXmlProcessor(packageName, resourceFolders,
    275                 fileWriter, minSdkVersion.getApiLevel(), isLibrary);
    276         final ProcessAndroidResources processResTask = generateRTask;
    277         final File xmlOutDir = new File(project.getBuildDir() + "/layout-info/" +
    278                 configuration.getDirName());
    279         final File generatedClassListOut = isLibrary ? new File(xmlOutDir, "_generated.txt") : null;
    280         logD("xml output for %s is %s", variantData, xmlOutDir);
    281         String layoutTaskName = "dataBindingLayouts" + StringUtils
    282                 .capitalize(processResTask.getName());
    283         String infoClassTaskName = "dataBindingInfoClass" + StringUtils
    284                 .capitalize(processResTask.getName());
    285 
    286         final DataBindingProcessLayoutsTask[] processLayoutsTasks
    287                 = new DataBindingProcessLayoutsTask[1];
    288         project.getTasks().create(layoutTaskName,
    289                 DataBindingProcessLayoutsTask.class,
    290                 new Action<DataBindingProcessLayoutsTask>() {
    291                     @Override
    292                     public void execute(final DataBindingProcessLayoutsTask task) {
    293                         processLayoutsTasks[0] = task;
    294                         task.setXmlProcessor(xmlProcessor);
    295                         task.setSdkDir(sdkDir);
    296                         task.setXmlOutFolder(xmlOutDir);
    297                         task.setMinSdk(minSdkVersion.getApiLevel());
    298 
    299                         logD("TASK adding dependency on %s for %s", task, processResTask);
    300                         processResTask.dependsOn(task);
    301                         processResTask.getInputs().dir(xmlOutDir);
    302                         for (Object dep : processResTask.getDependsOn()) {
    303                             if (dep == task) {
    304                                 continue;
    305                             }
    306                             logD("adding dependency on %s for %s", dep, task);
    307                             task.dependsOn(dep);
    308                         }
    309                         processResTask.doLast(new Action<Task>() {
    310                             @Override
    311                             public void execute(Task unused) {
    312                                 try {
    313                                     task.writeLayoutXmls();
    314                                 } catch (JAXBException e) {
    315                                     // gradle sometimes fails to resolve JAXBException.
    316                                     // We get stack trace manually to ensure we have the log
    317                                     logE(e, "cannot write layout xmls %s",
    318                                             ExceptionUtils.getStackTrace(e));
    319                                 }
    320                             }
    321                         });
    322                     }
    323                 });
    324         final DataBindingProcessLayoutsTask processLayoutsTask = processLayoutsTasks[0];
    325         project.getTasks().create(infoClassTaskName,
    326                 DataBindingExportInfoTask.class,
    327                 new Action<DataBindingExportInfoTask>() {
    328 
    329                     @Override
    330                     public void execute(DataBindingExportInfoTask task) {
    331                         task.dependsOn(processLayoutsTask);
    332                         task.dependsOn(processResTask);
    333                         task.setXmlProcessor(xmlProcessor);
    334                         task.setSdkDir(sdkDir);
    335                         task.setXmlOutFolder(xmlOutDir);
    336                         task.setExportClassListTo(generatedClassListOut);
    337                         task.setPrintEncodedErrors(printEncodedErrors);
    338                         task.setEnableDebugLogs(logger.isEnabled(LogLevel.DEBUG));
    339 
    340                         variantData.registerJavaGeneratingTask(task, codeGenTargetFolder);
    341                     }
    342                 });
    343         String packageJarTaskName = "package" + StringUtils.capitalize(fullName) + "Jar";
    344         final Task packageTask = project.getTasks().findByName(packageJarTaskName);
    345         if (packageTask instanceof Jar) {
    346             String removeGeneratedTaskName = "dataBindingExcludeGeneratedFrom" +
    347                     StringUtils.capitalize(packageTask.getName());
    348             if (project.getTasks().findByName(removeGeneratedTaskName) == null) {
    349                 final AbstractCompile javaCompileTask = variantData.javacTask;
    350                 Preconditions.checkNotNull(javaCompileTask);
    351 
    352                 project.getTasks().create(removeGeneratedTaskName,
    353                         DataBindingExcludeGeneratedTask.class,
    354                         new Action<DataBindingExcludeGeneratedTask>() {
    355                             @Override
    356                             public void execute(DataBindingExcludeGeneratedTask task) {
    357                                 packageTask.dependsOn(task);
    358                                 task.dependsOn(javaCompileTask);
    359                                 task.setAppPackage(packageName);
    360                                 task.setInfoClassQualifiedName(xmlProcessor.getInfoClassFullName());
    361                                 task.setPackageTask((Jar) packageTask);
    362                                 task.setLibrary(isLibrary);
    363                                 task.setGeneratedClassListFile(generatedClassListOut);
    364                             }
    365                         });
    366             }
    367         }
    368     }
    369 
    370     private void logD(String s, Object... args) {
    371         logger.info(formatLog(s, args));
    372     }
    373 
    374     private void logE(Throwable t, String s, Object... args) {
    375         logger.error(formatLog(s, args), t);
    376     }
    377 
    378     private String formatLog(String s, Object... args) {
    379         return "[data binding plugin]: " + String.format(s, args);
    380     }
    381 }
    382