LayoutXmlProcessor.java revision 0cb9fbb96197af013f4f879ed6cddf2681b88fd6
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.base.Predicate;
17import com.google.common.collect.Iterables;
18
19import org.apache.commons.lang3.StringEscapeUtils;
20import org.xml.sax.SAXException;
21
22import android.databinding.BindingBuildInfo;
23import android.databinding.tool.store.LayoutFileParser;
24import android.databinding.tool.store.ResourceBundle;
25import android.databinding.tool.writer.JavaFileWriter;
26
27import java.io.File;
28import java.io.FilenameFilter;
29import java.io.IOException;
30import java.io.StringWriter;
31import java.util.ArrayList;
32import java.util.List;
33import java.util.UUID;
34
35import javax.xml.bind.JAXBContext;
36import javax.xml.bind.JAXBException;
37import javax.xml.bind.Marshaller;
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    // can be a list of xml files or folders that contain XML files
59    private final List<File> mResources;
60
61    public LayoutXmlProcessor(String applicationPackage, List<File> resources,
62            JavaFileWriter fileWriter, int minSdk, boolean isLibrary) {
63        mFileWriter = fileWriter;
64        mResourceBundle = new ResourceBundle(applicationPackage);
65        mResources = resources;
66        mMinSdk = minSdk;
67        mIsLibrary = isLibrary;
68    }
69
70    public static List<File> getLayoutFiles(List<File> resources) {
71        List<File> result = new ArrayList<File>();
72        for (File resource : Iterables.filter(resources, fileExists)) {
73            if (resource.isDirectory()) {
74                for (File layoutFolder : resource.listFiles(layoutFolderFilter)) {
75                    for (File xmlFile : layoutFolder.listFiles(xmlFileFilter)) {
76                        result.add(xmlFile);
77                    }
78
79                }
80            } else if (xmlFileFilter.accept(resource.getParentFile(), resource.getName())) {
81                result.add(resource);
82            }
83        }
84        return result;
85    }
86
87    /**
88     * used by the studio plugin
89     */
90    public ResourceBundle getResourceBundle() {
91        return mResourceBundle;
92    }
93
94    public boolean processResources(int minSdk)
95            throws ParserConfigurationException, SAXException, XPathExpressionException,
96            IOException {
97        if (mProcessingComplete) {
98            return false;
99        }
100        LayoutFileParser layoutFileParser = new LayoutFileParser();
101        for (File xmlFile : getLayoutFiles(mResources)) {
102            final ResourceBundle.LayoutFileBundle bindingLayout = layoutFileParser
103                    .parseXml(xmlFile, mResourceBundle.getAppPackage(), minSdk);
104            if (bindingLayout != null && !bindingLayout.isEmpty()) {
105                mResourceBundle.addLayoutBundle(bindingLayout);
106            }
107        }
108        mProcessingComplete = true;
109        return true;
110    }
111
112    public void writeLayoutInfoFiles(File xmlOutDir) throws JAXBException {
113        if (mWritten) {
114            return;
115        }
116        JAXBContext context = JAXBContext.newInstance(ResourceBundle.LayoutFileBundle.class);
117        Marshaller marshaller = context.createMarshaller();
118
119        for (List<ResourceBundle.LayoutFileBundle> layouts : mResourceBundle.getLayoutBundles()
120                .values()) {
121            for (ResourceBundle.LayoutFileBundle layout : layouts) {
122                writeXmlFile(xmlOutDir, layout, marshaller);
123            }
124        }
125        mWritten = true;
126    }
127
128    private void writeXmlFile(File xmlOutDir, ResourceBundle.LayoutFileBundle layout,
129            Marshaller marshaller) throws JAXBException {
130        String filename = generateExportFileName(layout) + ".xml";
131        String xml = toXML(layout, marshaller);
132        mFileWriter.writeToFile(new File(xmlOutDir, filename), xml);
133    }
134
135    public String getInfoClassFullName() {
136        return RESOURCE_BUNDLE_PACKAGE + "." + CLASS_NAME;
137    }
138
139    private String toXML(ResourceBundle.LayoutFileBundle layout, Marshaller marshaller)
140            throws JAXBException {
141        StringWriter writer = new StringWriter();
142        marshaller.marshal(layout, writer);
143        return writer.getBuffer().toString();
144    }
145
146    /**
147     * Generates a string identifier that can uniquely identify the given layout bundle.
148     * This identifier can be used when we need to export data about this layout bundle.
149     */
150    public String generateExportFileName(ResourceBundle.LayoutFileBundle layout) {
151        StringBuilder name = new StringBuilder(layout.getFileName());
152        name.append('-').append(layout.getDirectory());
153        for (int i = name.length() - 1; i >= 0; i--) {
154            char c = name.charAt(i);
155            if (c == '-') {
156                name.deleteCharAt(i);
157                c = Character.toUpperCase(name.charAt(i));
158                name.setCharAt(i, c);
159            }
160        }
161        return name.toString();
162    }
163
164    public void writeInfoClass(/*Nullable*/ File sdkDir, File xmlOutDir,
165            /*Nullable*/ File exportClassListTo) {
166        writeInfoClass(sdkDir, xmlOutDir, exportClassListTo, false);
167    }
168
169    public void writeInfoClass(/*Nullable*/ File sdkDir, File xmlOutDir, File exportClassListTo,
170            boolean enableDebugLogs) {
171        final String sdkPath = sdkDir == null ? null : StringEscapeUtils.escapeJava(sdkDir.getAbsolutePath());
172        final Class annotation = BindingBuildInfo.class;
173        final String layoutInfoPath = StringEscapeUtils.escapeJava(xmlOutDir.getAbsolutePath());
174        final String exportClassListToPath = exportClassListTo == null ? "" :
175                StringEscapeUtils.escapeJava(exportClassListTo.getAbsolutePath());
176        String classString = "package " + RESOURCE_BUNDLE_PACKAGE + ";\n\n" +
177                "import " + annotation.getCanonicalName() + ";\n\n" +
178                "@" + annotation.getSimpleName() + "(buildId=\"" + mBuildId + "\", " +
179                "modulePackage=\"" + mResourceBundle.getAppPackage() + "\", " +
180                "sdkRoot=" + "\"" + (sdkPath == null ? "" : sdkPath) + "\"," +
181                "layoutInfoDir=\"" + layoutInfoPath + "\"," +
182                "exportClassListTo=\"" + exportClassListToPath + "\"," +
183                "isLibrary=" + mIsLibrary + "," +
184                "minSdk=" + mMinSdk + "," +
185                "enableDebugLogs=" + enableDebugLogs + ")\n" +
186                "public class " + CLASS_NAME + " {}\n";
187        mFileWriter.writeToFile(RESOURCE_BUNDLE_PACKAGE + "." + CLASS_NAME, classString);
188    }
189
190    private static final Predicate<File> fileExists = new Predicate<File>() {
191        @Override
192        public boolean apply(File input) {
193            return input.exists() && input.canRead();
194        }
195    };
196
197    private static final FilenameFilter layoutFolderFilter = new FilenameFilter() {
198        @Override
199        public boolean accept(File dir, String name) {
200            return name.startsWith("layout");
201        }
202    };
203
204    private static final FilenameFilter xmlFileFilter = new FilenameFilter() {
205        @Override
206        public boolean accept(File dir, String name) {
207            return name.toLowerCase().endsWith(".xml");
208        }
209    };
210}
211