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