
android.databinding.tool.store.LayoutFileParser Maven / Gradle / Ivy
Go to download
Show more of this group Show more artifacts with this name
Show all versions of compilerCommon Show documentation
Show all versions of compilerCommon Show documentation
Common library that can be shared between different build tools
/*
* Copyright (C) 2015 The Android Open Source Project
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
* http://www.apache.org/licenses/LICENSE-2.0
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package android.databinding.tool.store;
import android.databinding.parser.XMLLexer;
import android.databinding.parser.XMLParser;
import android.databinding.parser.XMLParserBaseVisitor;
import android.databinding.tool.LayoutXmlProcessor;
import android.databinding.tool.processing.ErrorMessages;
import android.databinding.tool.processing.Scope;
import android.databinding.tool.processing.scopes.FileScopeProvider;
import android.databinding.tool.util.L;
import android.databinding.tool.util.ParserHelper;
import android.databinding.tool.util.Preconditions;
import android.databinding.tool.util.StringUtils;
import android.databinding.tool.util.XmlEditor;
import com.google.common.base.Strings;
import org.antlr.v4.runtime.ANTLRInputStream;
import org.antlr.v4.runtime.CommonTokenStream;
import org.antlr.v4.runtime.ParserRuleContext;
import org.antlr.v4.runtime.misc.NotNull;
import org.apache.commons.io.FileUtils;
import org.mozilla.universalchardet.UniversalDetector;
import org.w3c.dom.Document;
import org.w3c.dom.Node;
import org.w3c.dom.NodeList;
import org.xml.sax.SAXException;
import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;
import java.io.InputStreamReader;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import javax.xml.parsers.DocumentBuilder;
import javax.xml.parsers.DocumentBuilderFactory;
import javax.xml.parsers.ParserConfigurationException;
import javax.xml.xpath.XPath;
import javax.xml.xpath.XPathConstants;
import javax.xml.xpath.XPathExpression;
import javax.xml.xpath.XPathExpressionException;
import javax.xml.xpath.XPathFactory;
/**
* Gets the list of XML files and creates a list of
* {@link android.databinding.tool.store.ResourceBundle} that can be persistent or converted to
* LayoutBinder.
*/
public class LayoutFileParser {
private static final String XPATH_BINDING_LAYOUT = "/layout";
private static final String LAYOUT_PREFIX = "@layout/";
public ResourceBundle.LayoutFileBundle parseXml(final File inputFile, final File outputFile,
String pkg, final LayoutXmlProcessor.OriginalFileLookup originalFileLookup)
throws ParserConfigurationException, IOException, SAXException,
XPathExpressionException {
File originalFileFor = originalFileLookup.getOriginalFileFor(inputFile);
final String originalFilePath = originalFileFor.getAbsolutePath();
try {
Scope.enter(new FileScopeProvider() {
@Override
public String provideScopeFilePath() {
return originalFilePath;
}
});
final String encoding = findEncoding(inputFile);
stripFile(inputFile, outputFile, encoding, originalFileLookup);
return parseOriginalXml(originalFileFor, pkg, encoding);
} finally {
Scope.exit();
}
}
private ResourceBundle.LayoutFileBundle parseOriginalXml(final File original, String pkg,
String encoding) throws IOException {
try {
Scope.enter(new FileScopeProvider() {
@Override
public String provideScopeFilePath() {
return original.getAbsolutePath();
}
});
final String xmlNoExtension = ParserHelper.stripExtension(original.getName());
FileInputStream fin = new FileInputStream(original);
InputStreamReader reader = new InputStreamReader(fin, encoding);
ANTLRInputStream inputStream = new ANTLRInputStream(reader);
XMLLexer lexer = new XMLLexer(inputStream);
CommonTokenStream tokenStream = new CommonTokenStream(lexer);
XMLParser parser = new XMLParser(tokenStream);
XMLParser.DocumentContext expr = parser.document();
XMLParser.ElementContext root = expr.element();
if (!"layout".equals(root.elmName.getText())) {
return null;
}
XMLParser.ElementContext data = getDataNode(root);
XMLParser.ElementContext rootView = getViewNode(original, root);
if (hasMergeInclude(rootView)) {
L.e(ErrorMessages.INCLUDE_INSIDE_MERGE);
return null;
}
boolean isMerge = "merge".equals(rootView.elmName.getText());
ResourceBundle.LayoutFileBundle bundle = new ResourceBundle.LayoutFileBundle(original,
xmlNoExtension, original.getParentFile().getName(), pkg, isMerge);
final String newTag = original.getParentFile().getName() + '/' + xmlNoExtension;
parseData(original, data, bundle);
parseExpressions(newTag, rootView, isMerge, bundle);
return bundle;
} finally {
Scope.exit();
}
}
private static boolean isProcessedElement(String name) {
if (Strings.isNullOrEmpty(name)) {
return false;
}
if ("view".equals(name) || "include".equals(name) || name.indexOf('.') >= 0) {
return true;
}
return !name.toLowerCase().equals(name);
}
private void parseExpressions(String newTag, final XMLParser.ElementContext rootView,
final boolean isMerge, ResourceBundle.LayoutFileBundle bundle) {
final List bindingElements
= new ArrayList();
final List otherElementsWithIds
= new ArrayList();
rootView.accept(new XMLParserBaseVisitor() {
@Override
public Void visitElement(@NotNull XMLParser.ElementContext ctx) {
if (filter(ctx)) {
bindingElements.add(ctx);
} else {
String name = ctx.elmName.getText();
if (isProcessedElement(name) &&
attributeMap(ctx).containsKey("android:id")) {
otherElementsWithIds.add(ctx);
}
}
visitChildren(ctx);
return null;
}
private boolean filter(XMLParser.ElementContext ctx) {
if (isMerge) {
// account for XMLParser.ContentContext
if (ctx.getParent().getParent() == rootView) {
return true;
}
} else if (ctx == rootView) {
return true;
}
return hasIncludeChild(ctx) || XmlEditor.hasExpressionAttributes(ctx) ||
"include".equals(ctx.elmName.getText());
}
private boolean hasIncludeChild(XMLParser.ElementContext ctx) {
for (XMLParser.ElementContext child : XmlEditor.elements(ctx)) {
if ("include".equals(child.elmName.getText())) {
return true;
}
}
return false;
}
});
final HashMap nodeTagMap =
new HashMap();
L.d("number of binding nodes %d", bindingElements.size());
int tagNumber = 0;
for (XMLParser.ElementContext parent : bindingElements) {
final Map attributes = attributeMap(parent);
String nodeName = parent.elmName.getText();
String viewName = null;
String includedLayoutName = null;
final String id = attributes.get("android:id");
final String tag;
final String originalTag = attributes.get("android:tag");
if ("include".equals(nodeName)) {
// get the layout attribute
final String includeValue = attributes.get("layout");
if (Strings.isNullOrEmpty(includeValue)) {
L.e("%s must include a layout", parent);
}
if (!includeValue.startsWith(LAYOUT_PREFIX)) {
L.e("included value (%s) must start with %s.",
includeValue, LAYOUT_PREFIX);
}
// if user is binding something there, there MUST be a layout file to be
// generated.
includedLayoutName = includeValue.substring(LAYOUT_PREFIX.length());
final ParserRuleContext myParentContent = parent.getParent();
Preconditions.check(myParentContent instanceof XMLParser.ContentContext,
"parent of an include tag must be a content context but it is %s",
myParentContent.getClass().getCanonicalName());
final ParserRuleContext grandParent = myParentContent.getParent();
Preconditions.check(grandParent instanceof XMLParser.ElementContext,
"grandparent of an include tag must be an element context but it is %s",
grandParent.getClass().getCanonicalName());
//noinspection SuspiciousMethodCalls
tag = nodeTagMap.get(grandParent);
} else if ("fragment".equals(nodeName)) {
if (XmlEditor.hasExpressionAttributes(parent)) {
L.e("fragments do not support data binding expressions.");
}
continue;
} else {
viewName = getViewName(parent);
// account for XMLParser.ContentContext
if (rootView == parent || (isMerge && parent.getParent().getParent() == rootView)) {
tag = newTag + "_" + tagNumber;
} else {
tag = "binding_" + tagNumber;
}
tagNumber++;
}
final ResourceBundle.BindingTargetBundle bindingTargetBundle =
bundle.createBindingTarget(id, viewName, true, tag, originalTag,
new Location(parent));
nodeTagMap.put(parent, tag);
bindingTargetBundle.setIncludedLayout(includedLayoutName);
for (XMLParser.AttributeContext attr : XmlEditor.expressionAttributes(parent)) {
String value = escapeQuotes(attr.attrValue.getText(), true);
final boolean isOneWay = value.startsWith("@{");
final boolean isTwoWay = value.startsWith("@={");
if (isOneWay || isTwoWay) {
if (value.charAt(value.length() - 1) != '}') {
L.e("Expecting '}' in expression '%s'", attr.attrValue.getText());
}
final int startIndex = isTwoWay ? 3 : 2;
final int endIndex = value.length() - 1;
final String strippedValue = value.substring(startIndex, endIndex);
Location attrLocation = new Location(attr);
Location valueLocation = new Location();
// offset to 0 based
valueLocation.startLine = attr.attrValue.getLine() - 1;
valueLocation.startOffset = attr.attrValue.getCharPositionInLine() +
attr.attrValue.getText().indexOf(strippedValue);
valueLocation.endLine = attrLocation.endLine;
valueLocation.endOffset = attrLocation.endOffset - 2; // account for: "}
bindingTargetBundle.addBinding(escapeQuotes(attr.attrName.getText(), false),
strippedValue, isTwoWay, attrLocation, valueLocation);
}
}
}
for (XMLParser.ElementContext elm : otherElementsWithIds) {
final String id = attributeMap(elm).get("android:id");
final String className = getViewName(elm);
bundle.createBindingTarget(id, className, true, null, null, new Location(elm));
}
}
private String getViewName(XMLParser.ElementContext elm) {
String viewName = elm.elmName.getText();
if ("view".equals(viewName)) {
String classNode = attributeMap(elm).get("class");
if (Strings.isNullOrEmpty(classNode)) {
L.e("No class attribute for 'view' node");
}
viewName = classNode;
} else if ("include".equals(viewName) && !XmlEditor.hasExpressionAttributes(elm)) {
viewName = "android.view.View";
}
return viewName;
}
private void parseData(File xml, XMLParser.ElementContext data,
ResourceBundle.LayoutFileBundle bundle) {
if (data == null) {
return;
}
for (XMLParser.ElementContext imp : filter(data, "import")) {
final Map attrMap = attributeMap(imp);
String type = attrMap.get("type");
String alias = attrMap.get("alias");
Preconditions.check(StringUtils.isNotBlank(type), "Type of an import cannot be empty."
+ " %s in %s", imp.toStringTree(), xml);
if (Strings.isNullOrEmpty(alias)) {
alias = type.substring(type.lastIndexOf('.') + 1);
}
bundle.addImport(alias, type, new Location(imp));
}
for (XMLParser.ElementContext variable : filter(data, "variable")) {
final Map attrMap = attributeMap(variable);
String type = attrMap.get("type");
String name = attrMap.get("name");
Preconditions.checkNotNull(type, "variable must have a type definition %s in %s",
variable.toStringTree(), xml);
Preconditions.checkNotNull(name, "variable must have a name %s in %s",
variable.toStringTree(), xml);
bundle.addVariable(name, type, new Location(variable), true);
}
final XMLParser.AttributeContext className = findAttribute(data, "class");
if (className != null) {
final String name = escapeQuotes(className.attrValue.getText(), true);
if (StringUtils.isNotBlank(name)) {
Location location = new Location(
className.attrValue.getLine() - 1,
className.attrValue.getCharPositionInLine() + 1,
className.attrValue.getLine() - 1,
className.attrValue.getCharPositionInLine() + name.length()
);
bundle.setBindingClass(name, location);
}
}
}
private XMLParser.ElementContext getDataNode(XMLParser.ElementContext root) {
final List data = filter(root, "data");
if (data.size() == 0) {
return null;
}
Preconditions.check(data.size() == 1, "XML layout can have only 1 data tag");
return data.get(0);
}
private XMLParser.ElementContext getViewNode(File xml, XMLParser.ElementContext root) {
final List view = filterNot(root, "data");
Preconditions.check(view.size() == 1, "XML layout %s must have 1 view but has %s. root"
+ " children count %s", xml, view.size(), root.getChildCount());
return view.get(0);
}
private List filter(XMLParser.ElementContext root,
String name) {
List result = new ArrayList();
if (root == null) {
return result;
}
final XMLParser.ContentContext content = root.content();
if (content == null) {
return result;
}
for (XMLParser.ElementContext child : XmlEditor.elements(root)) {
if (name.equals(child.elmName.getText())) {
result.add(child);
}
}
return result;
}
private List filterNot(XMLParser.ElementContext root,
String name) {
List result = new ArrayList();
if (root == null) {
return result;
}
final XMLParser.ContentContext content = root.content();
if (content == null) {
return result;
}
for (XMLParser.ElementContext child : XmlEditor.elements(root)) {
if (!name.equals(child.elmName.getText())) {
result.add(child);
}
}
return result;
}
private boolean hasMergeInclude(XMLParser.ElementContext rootView) {
return "merge".equals(rootView.elmName.getText()) && filter(rootView, "include").size() > 0;
}
private void stripFile(File xml, File out, String encoding,
LayoutXmlProcessor.OriginalFileLookup originalFileLookup)
throws ParserConfigurationException, IOException, SAXException,
XPathExpressionException {
DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance();
DocumentBuilder builder = factory.newDocumentBuilder();
Document doc = builder.parse(xml);
XPathFactory xPathFactory = XPathFactory.newInstance();
XPath xPath = xPathFactory.newXPath();
File actualFile = originalFileLookup == null ? null
: originalFileLookup.getOriginalFileFor(xml);
// TODO get rid of original file lookup
if (actualFile == null) {
actualFile = xml;
}
// always create id from actual file when available. Gradle may duplicate files.
String noExt = ParserHelper.stripExtension(actualFile.getName());
String binderId = actualFile.getParentFile().getName() + '/' + noExt;
// now if file has any binding expressions, find and delete them
boolean changed = isBindingLayout(doc, xPath);
if (changed) {
stripBindingTags(xml, out, binderId, encoding);
} else if (!xml.equals(out)){
FileUtils.copyFile(xml, out);
}
}
private boolean isBindingLayout(Document doc, XPath xPath) throws XPathExpressionException {
return !get(doc, xPath, XPATH_BINDING_LAYOUT).isEmpty();
}
private List get(Document doc, XPath xPath, String pattern)
throws XPathExpressionException {
final XPathExpression expr = xPath.compile(pattern);
return toList((NodeList) expr.evaluate(doc, XPathConstants.NODESET));
}
private List toList(NodeList nodeList) {
List result = new ArrayList();
for (int i = 0; i < nodeList.getLength(); i++) {
result.add(nodeList.item(i));
}
return result;
}
private void stripBindingTags(File xml, File output, String newTag, String encoding) throws IOException {
String res = XmlEditor.strip(xml, newTag, encoding);
Preconditions.checkNotNull(res, "layout file should've changed %s", xml.getAbsolutePath());
if (res != null) {
L.d("file %s has changed, overwriting %s", xml.getName(), xml.getAbsolutePath());
FileUtils.writeStringToFile(output, res, encoding);
}
}
private static String findEncoding(File f) throws IOException {
FileInputStream fin = new FileInputStream(f);
try {
UniversalDetector universalDetector = new UniversalDetector(null);
byte[] buf = new byte[4096];
int nread;
while ((nread = fin.read(buf)) > 0 && !universalDetector.isDone()) {
universalDetector.handleData(buf, 0, nread);
}
universalDetector.dataEnd();
String encoding = universalDetector.getDetectedCharset();
if (encoding == null) {
encoding = "utf-8";
}
return encoding;
} finally {
fin.close();
}
}
private static Map attributeMap(XMLParser.ElementContext root) {
final Map result = new HashMap();
for (XMLParser.AttributeContext attr : XmlEditor.attributes(root)) {
result.put(escapeQuotes(attr.attrName.getText(), false),
escapeQuotes(attr.attrValue.getText(), true));
}
return result;
}
private static XMLParser.AttributeContext findAttribute(XMLParser.ElementContext element,
String name) {
for (XMLParser.AttributeContext attr : element.attribute()) {
if (escapeQuotes(attr.attrName.getText(), false).equals(name)) {
return attr;
}
}
return null;
}
private static String escapeQuotes(String textWithQuotes, boolean unescapeValue) {
char first = textWithQuotes.charAt(0);
int start = 0, end = textWithQuotes.length();
if (first == '"' || first == '\'') {
start = 1;
}
char last = textWithQuotes.charAt(textWithQuotes.length() - 1);
if (last == '"' || last == '\'') {
end -= 1;
}
String val = textWithQuotes.substring(start, end);
if (unescapeValue) {
return StringUtils.unescapeXml(val);
} else {
return val;
}
}
}
© 2015 - 2025 Weber Informatics LLC | Privacy Policy