• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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