Home | History | Annotate | Download | only in annotationprocessor
      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 
     17 package android.databinding.annotationprocessor;
     18 
     19 import com.google.common.base.Joiner;
     20 
     21 import org.apache.commons.io.FileUtils;
     22 import org.apache.commons.io.IOUtils;
     23 
     24 import android.databinding.BindingBuildInfo;
     25 import android.databinding.tool.CompilerChef;
     26 import android.databinding.tool.LayoutXmlProcessor;
     27 import android.databinding.tool.reflection.SdkUtil;
     28 import android.databinding.tool.store.ResourceBundle;
     29 import android.databinding.tool.util.GenerationalClassUtil;
     30 import android.databinding.tool.util.L;
     31 import android.databinding.tool.util.Preconditions;
     32 import android.databinding.tool.util.StringUtils;
     33 
     34 import java.io.File;
     35 import java.io.FilenameFilter;
     36 import java.io.IOException;
     37 import java.io.InputStream;
     38 import java.io.Serializable;
     39 import java.util.ArrayList;
     40 import java.util.HashMap;
     41 import java.util.HashSet;
     42 import java.util.List;
     43 import java.util.Map;
     44 import java.util.Set;
     45 
     46 import javax.annotation.processing.ProcessingEnvironment;
     47 import javax.annotation.processing.RoundEnvironment;
     48 import javax.xml.bind.JAXBContext;
     49 import javax.xml.bind.JAXBException;
     50 import javax.xml.bind.Unmarshaller;
     51 
     52 public class ProcessExpressions extends ProcessDataBinding.ProcessingStep {
     53     public ProcessExpressions() {
     54     }
     55 
     56     @Override
     57     public boolean onHandleStep(RoundEnvironment roundEnvironment,
     58             ProcessingEnvironment processingEnvironment, BindingBuildInfo buildInfo)
     59             throws JAXBException {
     60         ResourceBundle resourceBundle;
     61         SdkUtil.initialize(buildInfo.minSdk(), new File(buildInfo.sdkRoot()));
     62         resourceBundle = new ResourceBundle(buildInfo.modulePackage());
     63         List<IntermediateV2> intermediateList = loadDependencyIntermediates();
     64         for (Intermediate intermediate : intermediateList) {
     65             try {
     66                 intermediate.appendTo(resourceBundle);
     67             } catch (Throwable throwable) {
     68                 L.e(throwable, "unable to prepare resource bundle");
     69             }
     70         }
     71 
     72         IntermediateV2 mine = createIntermediateFromLayouts(buildInfo.layoutInfoDir(),
     73                 intermediateList);
     74         if (mine != null) {
     75             mine.updateOverridden(resourceBundle);
     76             intermediateList.add(mine);
     77             saveIntermediate(processingEnvironment, buildInfo, mine);
     78             mine.appendTo(resourceBundle);
     79         }
     80         // generate them here so that bindable parser can read
     81         try {
     82             writeResourceBundle(resourceBundle, buildInfo.isLibrary(), buildInfo.minSdk(),
     83                     buildInfo.exportClassListTo());
     84         } catch (Throwable t) {
     85             L.e(t, "cannot generate view binders");
     86         }
     87         return true;
     88     }
     89 
     90     private List<IntermediateV2> loadDependencyIntermediates() {
     91         final List<Intermediate> original = GenerationalClassUtil.loadObjects(
     92                 GenerationalClassUtil.ExtensionFilter.LAYOUT);
     93         final List<IntermediateV2> upgraded = new ArrayList<IntermediateV2>(original.size());
     94         for (Intermediate intermediate : original) {
     95             final Intermediate updatedIntermediate = intermediate.upgrade();
     96             Preconditions.check(updatedIntermediate instanceof IntermediateV2, "Incompatible data"
     97                     + " binding dependency. Please update your dependencies or recompile them with"
     98                     + " application module's data binding version.");
     99             //noinspection ConstantConditions
    100             upgraded.add((IntermediateV2) updatedIntermediate);
    101         }
    102         return upgraded;
    103     }
    104 
    105     private void saveIntermediate(ProcessingEnvironment processingEnvironment,
    106             BindingBuildInfo buildInfo, IntermediateV2 intermediate) {
    107         GenerationalClassUtil.writeIntermediateFile(processingEnvironment,
    108                 buildInfo.modulePackage(), buildInfo.modulePackage() +
    109                         GenerationalClassUtil.ExtensionFilter.LAYOUT.getExtension(),
    110                 intermediate);
    111     }
    112 
    113     @Override
    114     public void onProcessingOver(RoundEnvironment roundEnvironment,
    115             ProcessingEnvironment processingEnvironment, BindingBuildInfo buildInfo) {
    116     }
    117 
    118     private IntermediateV2 createIntermediateFromLayouts(String layoutInfoFolderPath,
    119             List<IntermediateV2> intermediateList) {
    120         final Set<String> excludeList = new HashSet<String>();
    121         for (IntermediateV2 lib : intermediateList) {
    122             excludeList.addAll(lib.mLayoutInfoMap.keySet());
    123         }
    124         final File layoutInfoFolder = new File(layoutInfoFolderPath);
    125         if (!layoutInfoFolder.isDirectory()) {
    126             L.d("layout info folder does not exist, skipping for %s", layoutInfoFolderPath);
    127             return null;
    128         }
    129         IntermediateV2 result = new IntermediateV2();
    130         for (File layoutFile : layoutInfoFolder.listFiles(new FilenameFilter() {
    131             @Override
    132             public boolean accept(File dir, String name) {
    133                 return name.endsWith(".xml") && !excludeList.contains(name);
    134             }
    135         })) {
    136             try {
    137                 result.addEntry(layoutFile.getName(), FileUtils.readFileToString(layoutFile));
    138             } catch (IOException e) {
    139                 L.e(e, "cannot load layout file information. Try a clean build");
    140             }
    141         }
    142         return result;
    143     }
    144 
    145     private void writeResourceBundle(ResourceBundle resourceBundle, boolean forLibraryModule,
    146             final int minSdk, String exportClassNamesTo)
    147             throws JAXBException {
    148         final CompilerChef compilerChef = CompilerChef.createChef(resourceBundle, getWriter());
    149         compilerChef.sealModels();
    150         compilerChef.writeComponent();
    151         if (compilerChef.hasAnythingToGenerate()) {
    152             compilerChef.writeViewBinderInterfaces(forLibraryModule);
    153             if (!forLibraryModule) {
    154                 compilerChef.writeViewBinders(minSdk);
    155             }
    156         }
    157         if (forLibraryModule && exportClassNamesTo == null) {
    158             L.e("When compiling a library module, build info must include exportClassListTo path");
    159         }
    160         if (forLibraryModule) {
    161             Set<String> classNames = compilerChef.getWrittenClassNames();
    162             String out = Joiner.on(StringUtils.LINE_SEPARATOR).join(classNames);
    163             L.d("Writing list of classes to %s . \nList:%s", exportClassNamesTo, out);
    164             try {
    165                 //noinspection ConstantConditions
    166                 FileUtils.write(new File(exportClassNamesTo), out);
    167             } catch (IOException e) {
    168                 L.e(e, "Cannot create list of written classes");
    169             }
    170         }
    171         mCallback.onChefReady(compilerChef, forLibraryModule, minSdk);
    172     }
    173 
    174     public interface Intermediate extends Serializable {
    175 
    176         Intermediate upgrade();
    177 
    178         void appendTo(ResourceBundle resourceBundle) throws Throwable;
    179     }
    180 
    181     public static class IntermediateV1 implements Intermediate {
    182 
    183         transient Unmarshaller mUnmarshaller;
    184 
    185         // name to xml content map
    186         Map<String, String> mLayoutInfoMap = new HashMap<String, String>();
    187 
    188         @Override
    189         public Intermediate upgrade() {
    190             final IntermediateV2 updated = new IntermediateV2();
    191             updated.mLayoutInfoMap = mLayoutInfoMap;
    192             updated.mUnmarshaller = mUnmarshaller;
    193             return updated;
    194         }
    195 
    196         @Override
    197         public void appendTo(ResourceBundle resourceBundle) throws JAXBException {
    198             if (mUnmarshaller == null) {
    199                 JAXBContext context = JAXBContext
    200                         .newInstance(ResourceBundle.LayoutFileBundle.class);
    201                 mUnmarshaller = context.createUnmarshaller();
    202             }
    203             for (String content : mLayoutInfoMap.values()) {
    204                 final InputStream is = IOUtils.toInputStream(content);
    205                 try {
    206                     final ResourceBundle.LayoutFileBundle bundle
    207                             = (ResourceBundle.LayoutFileBundle) mUnmarshaller.unmarshal(is);
    208                     resourceBundle.addLayoutBundle(bundle);
    209                     L.d("loaded layout info file %s", bundle);
    210                 } finally {
    211                     IOUtils.closeQuietly(is);
    212                 }
    213             }
    214         }
    215 
    216         public void addEntry(String name, String contents) {
    217             mLayoutInfoMap.put(name, contents);
    218         }
    219 
    220         // keeping the method to match deserialized structure
    221         @SuppressWarnings("unused")
    222         public void removeOverridden(List<Intermediate> existing) {
    223         }
    224     }
    225 
    226     public static class IntermediateV2 extends IntermediateV1 {
    227         // specify so that we can define updates ourselves.
    228         private static final long serialVersionUID = 2L;
    229         @Override
    230         public void appendTo(ResourceBundle resourceBundle) throws JAXBException {
    231             for (Map.Entry<String, String> entry : mLayoutInfoMap.entrySet()) {
    232                 final InputStream is = IOUtils.toInputStream(entry.getValue());
    233                 try {
    234                     final ResourceBundle.LayoutFileBundle bundle = ResourceBundle.LayoutFileBundle
    235                             .fromXML(is);
    236                     resourceBundle.addLayoutBundle(bundle);
    237                     L.d("loaded layout info file %s", bundle);
    238                 } finally {
    239                     IOUtils.closeQuietly(is);
    240                 }
    241             }
    242         }
    243 
    244         /**
    245          * if a layout is overridden from a module (which happens when layout is auto-generated),
    246          * we need to update its contents from the class that overrides it.
    247          * This must be done before this bundle is saved, otherwise, it will not be recognized
    248          * when it is used in another project.
    249          */
    250         public void updateOverridden(ResourceBundle bundle) throws JAXBException {
    251             // When a layout is copied from inherited module, it is eleminated while reading
    252             // info files. (createIntermediateFromLayouts).
    253             // Build process may also duplicate some files at compile time. This is where
    254             // we detect those copies and force inherit their module and classname information.
    255             final HashMap<String, List<ResourceBundle.LayoutFileBundle>> bundles = bundle
    256                     .getLayoutBundles();
    257             for (Map.Entry<String, String> info : mLayoutInfoMap.entrySet()) {
    258                 String key = LayoutXmlProcessor.exportLayoutNameFromInfoFileName(info.getKey());
    259                 final List<ResourceBundle.LayoutFileBundle> existingList = bundles.get(key);
    260                 if (existingList != null && !existingList.isEmpty()) {
    261                     ResourceBundle.LayoutFileBundle myBundle = ResourceBundle.LayoutFileBundle
    262                             .fromXML(IOUtils.toInputStream(info.getValue()));
    263                     final ResourceBundle.LayoutFileBundle inheritFrom = existingList.get(0);
    264                     myBundle.inheritConfigurationFrom(inheritFrom);
    265                     L.d("inheriting data for %s (%s) from %s", info.getKey(), key, inheritFrom);
    266                     mLayoutInfoMap.put(info.getKey(), myBundle.toXML());
    267                 }
    268             }
    269         }
    270     }
    271 }
    272