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