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 */
16package android.databinding.tool;
17
18import com.google.common.base.Preconditions;
19
20import com.android.build.gradle.AppExtension;
21import com.android.build.gradle.BaseExtension;
22import com.android.build.gradle.LibraryExtension;
23import com.android.build.gradle.api.ApplicationVariant;
24import com.android.build.gradle.api.LibraryVariant;
25import com.android.build.gradle.api.TestVariant;
26import com.android.build.gradle.internal.api.ApplicationVariantImpl;
27import com.android.build.gradle.internal.api.LibraryVariantImpl;
28import com.android.build.gradle.internal.api.TestVariantImpl;
29import com.android.build.gradle.internal.core.GradleVariantConfiguration;
30import com.android.build.gradle.internal.variant.ApplicationVariantData;
31import com.android.build.gradle.internal.variant.BaseVariantData;
32import com.android.build.gradle.internal.variant.LibraryVariantData;
33import com.android.build.gradle.internal.variant.TestVariantData;
34import com.android.build.gradle.tasks.ProcessAndroidResources;
35import com.android.builder.model.ApiVersion;
36
37import org.apache.commons.io.IOUtils;
38import org.apache.commons.lang3.StringUtils;
39import org.apache.commons.lang3.exception.ExceptionUtils;
40import org.gradle.api.Action;
41import org.gradle.api.Plugin;
42import org.gradle.api.Project;
43import org.gradle.api.Task;
44import org.gradle.api.logging.LogLevel;
45import org.gradle.api.logging.Logger;
46import org.gradle.api.plugins.ExtraPropertiesExtension;
47import org.gradle.api.tasks.bundling.Jar;
48import org.gradle.api.tasks.compile.AbstractCompile;
49
50import android.databinding.tool.processing.ScopedException;
51import android.databinding.tool.util.L;
52import android.databinding.tool.writer.JavaFileWriter;
53
54import java.io.File;
55import java.io.FileOutputStream;
56import java.io.IOException;
57import java.io.InputStream;
58import java.lang.reflect.Field;
59import java.util.Arrays;
60import java.util.List;
61
62import javax.tools.Diagnostic;
63import javax.xml.bind.JAXBException;
64
65public 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