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
14package android.databinding.tool;
15
16import com.google.common.escape.Escaper;
17
18import org.apache.commons.io.FileUtils;
19import org.xml.sax.SAXException;
20
21import android.databinding.BindingBuildInfo;
22import android.databinding.tool.store.LayoutFileParser;
23import android.databinding.tool.store.ResourceBundle;
24import android.databinding.tool.util.L;
25import android.databinding.tool.util.Preconditions;
26import android.databinding.tool.util.SourceCodeEscapers;
27import android.databinding.tool.writer.JavaFileWriter;
28
29import java.io.File;
30import java.io.FilenameFilter;
31import java.io.IOException;
32import java.net.URI;
33import java.util.ArrayList;
34import java.util.List;
35import java.util.UUID;
36
37import javax.xml.bind.JAXBException;
38import javax.xml.parsers.ParserConfigurationException;
39import 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 */
46public 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