Home | History | Annotate | Download | only in tool
      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 
     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 
     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 
     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 
     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 
    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      */
    139     public ResourceBundle getResourceBundle() {
    140         return mResourceBundle;
    141     }
    142 
    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 
    225     public static String toSystemDependentPath(String path) {
    226         if (File.separatorChar != '/') {
    227             path = path.replace('/', File.separatorChar);
    228         }
    229         return path;
    230     }
    231 
    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 
    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 
    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      */
    263     private static String generateExportFileName(ResourceBundle.LayoutFileBundle layout) {
    264         return generateExportFileName(layout.getFileName(), layout.getDirectory());
    265     }
    266 
    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 
    273     public static String generateExportFileName(String fileName, String dirName) {
    274         return fileName + '-' + dirName + ".xml";
    275     }
    276 
    277     public static String exportLayoutNameFromInfoFileName(String infoFileName) {
    278         return infoFileName.substring(0, infoFileName.indexOf('-'));
    279     }
    280 
    281     public void writeInfoClass(/*Nullable*/ File sdkDir, File xmlOutDir,
    282             /*Nullable*/ File exportClassListTo) {
    283         writeInfoClass(sdkDir, xmlOutDir, exportClassListTo, false, false);
    284     }
    285 
    286     public String getPackage() {
    287         return mResourceBundle.getAppPackage();
    288     }
    289 
    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          */
    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 
    351         public ResourceInput(boolean incremental, File rootInputFolder, File rootOutputFolder) {
    352             mIncremental = incremental;
    353             mRootInputFolder = rootInputFolder;
    354             mRootOutputFolder = rootOutputFolder;
    355         }
    356 
    357         public void added(File file) {
    358             mAdded.add(file);
    359         }
    360         public void removed(File file) {
    361             mRemoved.add(file);
    362         }
    363         public void changed(File file) {
    364             mChanged.add(file);
    365         }
    366 
    367         public boolean shouldCopy() {
    368             return !mRootInputFolder.equals(mRootOutputFolder);
    369         }
    370 
    371         List<File> getAdded() {
    372             return mAdded;
    373         }
    374 
    375         List<File> getRemoved() {
    376             return mRemoved;
    377         }
    378 
    379         List<File> getChanged() {
    380             return mChanged;
    381         }
    382 
    383         File getRootInputFolder() {
    384             return mRootInputFolder;
    385         }
    386 
    387         File getRootOutputFolder() {
    388             return mRootOutputFolder;
    389         }
    390 
    391         public boolean isIncremental() {
    392             return mIncremental;
    393         }
    394 
    395         @Override
    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 
    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 {
    418         void processLayoutFile(File file)
    419                 throws ParserConfigurationException, SAXException, XPathExpressionException,
    420                 IOException;
    421         void processOtherFile(File parentFolder, File file) throws IOException;
    422         void processRemovedLayoutFile(File file);
    423         void processRemovedOtherFile(File parentFolder, File file) throws IOException;
    424 
    425         void processOtherFolder(File folder);
    426 
    427         void processLayoutFolder(File folder);
    428 
    429         void processOtherRootFile(File file) throws IOException;
    430 
    431         void processRemovedOtherRootFile(File file) throws IOException;
    432     }
    433 }
    434