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 com.google.common.base.Joiner;
20
21import org.apache.commons.io.FileUtils;
22import org.apache.commons.io.IOUtils;
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;
32import android.databinding.tool.util.StringUtils;
33
34import java.io.File;
35import java.io.FilenameFilter;
36import java.io.IOException;
37import java.io.InputStream;
38import java.io.Serializable;
39import java.util.ArrayList;
40import java.util.HashMap;
41import java.util.HashSet;
42import java.util.List;
43import java.util.Map;
44import java.util.Set;
45
46import javax.annotation.processing.ProcessingEnvironment;
47import javax.annotation.processing.RoundEnvironment;
48import javax.xml.bind.JAXBContext;
49import javax.xml.bind.JAXBException;
50import javax.xml.bind.Unmarshaller;
51
52public 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