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 
GradleFileWriter(String outputBase)77         public GradleFileWriter(String outputBase) {
78             this.outputBase = outputBase;
79         }
80 
81         @Override
writeToFile(String canonicalName, String contents)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 
safeGetBooleanProperty(Project project, String property)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 
resolvePrintEncodedErrors(Project project)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
apply(Project project)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 
setupLogger(Project project)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 
readMyVersion()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 
createXmlProcessor(Project project)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 
createXmlProcessorForLibrary(Project project, LibraryExtension lib)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 
createXmlProcessorForApp(Project project, AppExtension appExt)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 
getVariantData(LibraryVariant variant)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 
getVariantData(TestVariant variant)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 
getVariantData(ApplicationVariant variant)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 
attachXmlProcessor(Project project, final BaseVariantData variantData, final File sdkDir, final Boolean isLibrary)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 
logD(String s, Object... args)370     private void logD(String s, Object... args) {
371         logger.info(formatLog(s, args));
372     }
373 
logE(Throwable t, String s, Object... args)374     private void logE(Throwable t, String s, Object... args) {
375         logger.error(formatLog(s, args), t);
376     }
377 
formatLog(String s, Object... args)378     private String formatLog(String s, Object... args) {
379         return "[data binding plugin]: " + String.format(s, args);
380     }
381 }
382