1 /* 2 * Copyright (C) 2015 The Android Open Source Project 3 * Licensed under the Apache License, Version 2.0 (the "License"); 4 * you may not use this file except in compliance with the License. 5 * You may obtain a copy of the License at 6 * http://www.apache.org/licenses/LICENSE-2.0 7 * Unless required by applicable law or agreed to in writing, software 8 * distributed under the License is distributed on an "AS IS" BASIS, 9 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 10 * See the License for the specific language governing permissions and 11 * limitations under the License. 12 */ 13 14 package android.databinding.tool; 15 16 import com.google.common.escape.Escaper; 17 18 import org.apache.commons.io.FileUtils; 19 import org.xml.sax.SAXException; 20 21 import android.databinding.BindingBuildInfo; 22 import android.databinding.tool.store.LayoutFileParser; 23 import android.databinding.tool.store.ResourceBundle; 24 import android.databinding.tool.util.L; 25 import android.databinding.tool.util.Preconditions; 26 import android.databinding.tool.util.SourceCodeEscapers; 27 import android.databinding.tool.writer.JavaFileWriter; 28 29 import java.io.File; 30 import java.io.FilenameFilter; 31 import java.io.IOException; 32 import java.net.URI; 33 import java.util.ArrayList; 34 import java.util.List; 35 import java.util.UUID; 36 37 import javax.xml.bind.JAXBException; 38 import javax.xml.parsers.ParserConfigurationException; 39 import javax.xml.xpath.XPathExpressionException; 40 41 /** 42 * Processes the layout XML, stripping the binding attributes and elements 43 * and writes the information into an annotated class file for the annotation 44 * processor to work with. 45 */ 46 public class LayoutXmlProcessor { 47 // hardcoded in baseAdapters 48 public static final String RESOURCE_BUNDLE_PACKAGE = "android.databinding.layouts"; 49 public static final String CLASS_NAME = "DataBindingInfo"; 50 private final JavaFileWriter mFileWriter; 51 private final ResourceBundle mResourceBundle; 52 private final int mMinSdk; 53 54 private boolean mProcessingComplete; 55 private boolean mWritten; 56 private final boolean mIsLibrary; 57 private final String mBuildId = UUID.randomUUID().toString(); 58 private final OriginalFileLookup mOriginalFileLookup; 59 LayoutXmlProcessor(String applicationPackage, JavaFileWriter fileWriter, int minSdk, boolean isLibrary, OriginalFileLookup originalFileLookup)60 public LayoutXmlProcessor(String applicationPackage, 61 JavaFileWriter fileWriter, int minSdk, boolean isLibrary, 62 OriginalFileLookup originalFileLookup) { 63 mFileWriter = fileWriter; 64 mResourceBundle = new ResourceBundle(applicationPackage); 65 mMinSdk = minSdk; 66 mIsLibrary = isLibrary; 67 mOriginalFileLookup = originalFileLookup; 68 } 69 processIncrementalInputFiles(ResourceInput input, ProcessFileCallback callback)70 private static void processIncrementalInputFiles(ResourceInput input, 71 ProcessFileCallback callback) 72 throws IOException, ParserConfigurationException, XPathExpressionException, 73 SAXException { 74 processExistingIncrementalFiles(input.getRootInputFolder(), input.getAdded(), callback); 75 processExistingIncrementalFiles(input.getRootInputFolder(), input.getChanged(), callback); 76 processRemovedIncrementalFiles(input.getRootInputFolder(), input.getRemoved(), callback); 77 } 78 processExistingIncrementalFiles(File inputRoot, List<File> files, ProcessFileCallback callback)79 private static void processExistingIncrementalFiles(File inputRoot, List<File> files, 80 ProcessFileCallback callback) 81 throws IOException, XPathExpressionException, SAXException, 82 ParserConfigurationException { 83 for (File file : files) { 84 File parent = file.getParentFile(); 85 if (inputRoot.equals(parent)) { 86 callback.processOtherRootFile(file); 87 } else if (layoutFolderFilter.accept(parent, parent.getName())) { 88 callback.processLayoutFile(file); 89 } else { 90 callback.processOtherFile(parent, file); 91 } 92 } 93 } 94 processRemovedIncrementalFiles(File inputRoot, List<File> files, ProcessFileCallback callback)95 private static void processRemovedIncrementalFiles(File inputRoot, List<File> files, 96 ProcessFileCallback callback) 97 throws IOException { 98 for (File file : files) { 99 File parent = file.getParentFile(); 100 if (inputRoot.equals(parent)) { 101 callback.processRemovedOtherRootFile(file); 102 } else if (layoutFolderFilter.accept(parent, parent.getName())) { 103 callback.processRemovedLayoutFile(file); 104 } else { 105 callback.processRemovedOtherFile(parent, file); 106 } 107 } 108 } 109 processAllInputFiles(ResourceInput input, ProcessFileCallback callback)110 private static void processAllInputFiles(ResourceInput input, ProcessFileCallback callback) 111 throws IOException, XPathExpressionException, SAXException, 112 ParserConfigurationException { 113 FileUtils.deleteDirectory(input.getRootOutputFolder()); 114 Preconditions.check(input.getRootOutputFolder().mkdirs(), "out dir should be re-created"); 115 Preconditions.check(input.getRootInputFolder().isDirectory(), "it must be a directory"); 116 for (File firstLevel : input.getRootInputFolder().listFiles()) { 117 if (firstLevel.isDirectory()) { 118 if (layoutFolderFilter.accept(firstLevel, firstLevel.getName())) { 119 callback.processLayoutFolder(firstLevel); 120 for (File xmlFile : firstLevel.listFiles(xmlFileFilter)) { 121 callback.processLayoutFile(xmlFile); 122 } 123 } else { 124 callback.processOtherFolder(firstLevel); 125 for (File file : firstLevel.listFiles()) { 126 callback.processOtherFile(firstLevel, file); 127 } 128 } 129 } else { 130 callback.processOtherRootFile(firstLevel); 131 } 132 133 } 134 } 135 136 /** 137 * used by the studio plugin 138 */ getResourceBundle()139 public ResourceBundle getResourceBundle() { 140 return mResourceBundle; 141 } 142 processResources(final ResourceInput input)143 public boolean processResources(final ResourceInput input) 144 throws ParserConfigurationException, SAXException, XPathExpressionException, 145 IOException { 146 if (mProcessingComplete) { 147 return false; 148 } 149 final LayoutFileParser layoutFileParser = new LayoutFileParser(); 150 final URI inputRootUri = input.getRootInputFolder().toURI(); 151 ProcessFileCallback callback = new ProcessFileCallback() { 152 private File convertToOutFile(File file) { 153 final String subPath = toSystemDependentPath(inputRootUri 154 .relativize(file.toURI()).getPath()); 155 return new File(input.getRootOutputFolder(), subPath); 156 } 157 @Override 158 public void processLayoutFile(File file) 159 throws ParserConfigurationException, SAXException, XPathExpressionException, 160 IOException { 161 final File output = convertToOutFile(file); 162 final ResourceBundle.LayoutFileBundle bindingLayout = layoutFileParser 163 .parseXml(file, output, mResourceBundle.getAppPackage(), mOriginalFileLookup); 164 if (bindingLayout != null && !bindingLayout.isEmpty()) { 165 mResourceBundle.addLayoutBundle(bindingLayout); 166 } 167 } 168 169 @Override 170 public void processOtherFile(File parentFolder, File file) throws IOException { 171 final File outParent = convertToOutFile(parentFolder); 172 FileUtils.copyFile(file, new File(outParent, file.getName())); 173 } 174 175 @Override 176 public void processRemovedLayoutFile(File file) { 177 mResourceBundle.addRemovedFile(file); 178 final File out = convertToOutFile(file); 179 FileUtils.deleteQuietly(out); 180 } 181 182 @Override 183 public void processRemovedOtherFile(File parentFolder, File file) throws IOException { 184 final File outParent = convertToOutFile(parentFolder); 185 FileUtils.deleteQuietly(new File(outParent, file.getName())); 186 } 187 188 @Override 189 public void processOtherFolder(File folder) { 190 //noinspection ResultOfMethodCallIgnored 191 convertToOutFile(folder).mkdirs(); 192 } 193 194 @Override 195 public void processLayoutFolder(File folder) { 196 //noinspection ResultOfMethodCallIgnored 197 convertToOutFile(folder).mkdirs(); 198 } 199 200 @Override 201 public void processOtherRootFile(File file) throws IOException { 202 final File outFile = convertToOutFile(file); 203 if (file.isDirectory()) { 204 FileUtils.copyDirectory(file, outFile); 205 } else { 206 FileUtils.copyFile(file, outFile); 207 } 208 } 209 210 @Override 211 public void processRemovedOtherRootFile(File file) throws IOException { 212 final File outFile = convertToOutFile(file); 213 FileUtils.deleteQuietly(outFile); 214 } 215 }; 216 if (input.isIncremental()) { 217 processIncrementalInputFiles(input, callback); 218 } else { 219 processAllInputFiles(input, callback); 220 } 221 mProcessingComplete = true; 222 return true; 223 } 224 toSystemDependentPath(String path)225 public static String toSystemDependentPath(String path) { 226 if (File.separatorChar != '/') { 227 path = path.replace('/', File.separatorChar); 228 } 229 return path; 230 } 231 writeLayoutInfoFiles(File xmlOutDir)232 public void writeLayoutInfoFiles(File xmlOutDir) throws JAXBException { 233 if (mWritten) { 234 return; 235 } 236 for (List<ResourceBundle.LayoutFileBundle> layouts : mResourceBundle.getLayoutBundles() 237 .values()) { 238 for (ResourceBundle.LayoutFileBundle layout : layouts) { 239 writeXmlFile(xmlOutDir, layout); 240 } 241 } 242 for (File file : mResourceBundle.getRemovedFiles()) { 243 String exportFileName = generateExportFileName(file); 244 FileUtils.deleteQuietly(new File(xmlOutDir, exportFileName)); 245 } 246 mWritten = true; 247 } 248 writeXmlFile(File xmlOutDir, ResourceBundle.LayoutFileBundle layout)249 private void writeXmlFile(File xmlOutDir, ResourceBundle.LayoutFileBundle layout) 250 throws JAXBException { 251 String filename = generateExportFileName(layout); 252 mFileWriter.writeToFile(new File(xmlOutDir, filename), layout.toXML()); 253 } 254 getInfoClassFullName()255 public String getInfoClassFullName() { 256 return RESOURCE_BUNDLE_PACKAGE + "." + CLASS_NAME; 257 } 258 259 /** 260 * Generates a string identifier that can uniquely identify the given layout bundle. 261 * This identifier can be used when we need to export data about this layout bundle. 262 */ generateExportFileName(ResourceBundle.LayoutFileBundle layout)263 private static String generateExportFileName(ResourceBundle.LayoutFileBundle layout) { 264 return generateExportFileName(layout.getFileName(), layout.getDirectory()); 265 } 266 generateExportFileName(File file)267 private static String generateExportFileName(File file) { 268 final String fileName = file.getName(); 269 return generateExportFileName(fileName.substring(0, fileName.lastIndexOf('.')), 270 file.getParentFile().getName()); 271 } 272 generateExportFileName(String fileName, String dirName)273 public static String generateExportFileName(String fileName, String dirName) { 274 return fileName + '-' + dirName + ".xml"; 275 } 276 exportLayoutNameFromInfoFileName(String infoFileName)277 public static String exportLayoutNameFromInfoFileName(String infoFileName) { 278 return infoFileName.substring(0, infoFileName.indexOf('-')); 279 } 280 writeInfoClass( File sdkDir, File xmlOutDir, File exportClassListTo)281 public void writeInfoClass(/*Nullable*/ File sdkDir, File xmlOutDir, 282 /*Nullable*/ File exportClassListTo) { 283 writeInfoClass(sdkDir, xmlOutDir, exportClassListTo, false, false); 284 } 285 getPackage()286 public String getPackage() { 287 return mResourceBundle.getAppPackage(); 288 } 289 writeInfoClass( File sdkDir, File xmlOutDir, File exportClassListTo, boolean enableDebugLogs, boolean printEncodedErrorLogs)290 public void writeInfoClass(/*Nullable*/ File sdkDir, File xmlOutDir, File exportClassListTo, 291 boolean enableDebugLogs, boolean printEncodedErrorLogs) { 292 Escaper javaEscaper = SourceCodeEscapers.javaCharEscaper(); 293 final String sdkPath = sdkDir == null ? null : javaEscaper.escape(sdkDir.getAbsolutePath()); 294 final Class annotation = BindingBuildInfo.class; 295 final String layoutInfoPath = javaEscaper.escape(xmlOutDir.getAbsolutePath()); 296 final String exportClassListToPath = exportClassListTo == null ? "" : 297 javaEscaper.escape(exportClassListTo.getAbsolutePath()); 298 String classString = "package " + RESOURCE_BUNDLE_PACKAGE + ";\n\n" + 299 "import " + annotation.getCanonicalName() + ";\n\n" + 300 "@" + annotation.getSimpleName() + "(buildId=\"" + mBuildId + "\", " + 301 "modulePackage=\"" + mResourceBundle.getAppPackage() + "\", " + 302 "sdkRoot=" + "\"" + (sdkPath == null ? "" : sdkPath) + "\"," + 303 "layoutInfoDir=\"" + layoutInfoPath + "\"," + 304 "exportClassListTo=\"" + exportClassListToPath + "\"," + 305 "isLibrary=" + mIsLibrary + "," + 306 "minSdk=" + mMinSdk + "," + 307 "enableDebugLogs=" + enableDebugLogs + "," + 308 "printEncodedError=" + printEncodedErrorLogs + ")\n" + 309 "public class " + CLASS_NAME + " {}\n"; 310 mFileWriter.writeToFile(RESOURCE_BUNDLE_PACKAGE + "." + CLASS_NAME, classString); 311 } 312 313 private static final FilenameFilter layoutFolderFilter = new FilenameFilter() { 314 @Override 315 public boolean accept(File dir, String name) { 316 return name.startsWith("layout"); 317 } 318 }; 319 320 private static final FilenameFilter xmlFileFilter = new FilenameFilter() { 321 @Override 322 public boolean accept(File dir, String name) { 323 return name.toLowerCase().endsWith(".xml"); 324 } 325 }; 326 327 /** 328 * Helper interface that can find the original copy of a resource XML. 329 */ 330 public interface OriginalFileLookup { 331 332 /** 333 * @param file The intermediate build file 334 * @return The original file or null if original File cannot be found. 335 */ getOriginalFileFor(File file)336 File getOriginalFileFor(File file); 337 } 338 339 /** 340 * API agnostic class to get resource changes incrementally. 341 */ 342 public static class ResourceInput { 343 private final boolean mIncremental; 344 private final File mRootInputFolder; 345 private final File mRootOutputFolder; 346 347 private List<File> mAdded = new ArrayList<File>(); 348 private List<File> mRemoved = new ArrayList<File>(); 349 private List<File> mChanged = new ArrayList<File>(); 350 ResourceInput(boolean incremental, File rootInputFolder, File rootOutputFolder)351 public ResourceInput(boolean incremental, File rootInputFolder, File rootOutputFolder) { 352 mIncremental = incremental; 353 mRootInputFolder = rootInputFolder; 354 mRootOutputFolder = rootOutputFolder; 355 } 356 added(File file)357 public void added(File file) { 358 mAdded.add(file); 359 } removed(File file)360 public void removed(File file) { 361 mRemoved.add(file); 362 } changed(File file)363 public void changed(File file) { 364 mChanged.add(file); 365 } 366 shouldCopy()367 public boolean shouldCopy() { 368 return !mRootInputFolder.equals(mRootOutputFolder); 369 } 370 getAdded()371 List<File> getAdded() { 372 return mAdded; 373 } 374 getRemoved()375 List<File> getRemoved() { 376 return mRemoved; 377 } 378 getChanged()379 List<File> getChanged() { 380 return mChanged; 381 } 382 getRootInputFolder()383 File getRootInputFolder() { 384 return mRootInputFolder; 385 } 386 getRootOutputFolder()387 File getRootOutputFolder() { 388 return mRootOutputFolder; 389 } 390 isIncremental()391 public boolean isIncremental() { 392 return mIncremental; 393 } 394 395 @Override toString()396 public String toString() { 397 StringBuilder out = new StringBuilder(); 398 out.append("ResourceInput{") 399 .append("mIncremental=").append(mIncremental) 400 .append(", mRootInputFolder=").append(mRootInputFolder) 401 .append(", mRootOutputFolder=").append(mRootOutputFolder); 402 logFiles(out, "added", mAdded); 403 logFiles(out, "removed", mRemoved); 404 logFiles(out, "changed", mChanged); 405 return out.toString(); 406 407 } 408 logFiles(StringBuilder out, String name, List<File> files)409 private static void logFiles(StringBuilder out, String name, List<File> files) { 410 out.append("\n ").append(name); 411 for (File file : files) { 412 out.append("\n - ").append(file.getAbsolutePath()); 413 } 414 } 415 } 416 417 private interface ProcessFileCallback { processLayoutFile(File file)418 void processLayoutFile(File file) 419 throws ParserConfigurationException, SAXException, XPathExpressionException, 420 IOException; processOtherFile(File parentFolder, File file)421 void processOtherFile(File parentFolder, File file) throws IOException; processRemovedLayoutFile(File file)422 void processRemovedLayoutFile(File file); processRemovedOtherFile(File parentFolder, File file)423 void processRemovedOtherFile(File parentFolder, File file) throws IOException; 424 processOtherFolder(File folder)425 void processOtherFolder(File folder); 426 processLayoutFolder(File folder)427 void processLayoutFolder(File folder); 428 processOtherRootFile(File file)429 void processOtherRootFile(File file) throws IOException; 430 processRemovedOtherRootFile(File file)431 void processRemovedOtherRootFile(File file) throws IOException; 432 } 433 } 434