blob: 6255416854d0c63c13e091e6e92dc8c3a4e527bf [file] [log] [blame]
/*
* Copyright (C) 2014 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.util;
import android.databinding.parser.BindingExpressionBaseVisitor;
import android.databinding.parser.BindingExpressionLexer;
import android.databinding.parser.BindingExpressionParser;
import android.databinding.parser.XMLLexer;
import android.databinding.parser.XMLParser;
import android.databinding.parser.XMLParser.AttributeContext;
import android.databinding.parser.XMLParser.ElementContext;
import com.google.common.base.Joiner;
import com.google.common.xml.XmlEscapers;
import org.antlr.v4.runtime.ANTLRInputStream;
import org.antlr.v4.runtime.CommonTokenStream;
import org.antlr.v4.runtime.Token;
import org.antlr.v4.runtime.misc.NotNull;
import org.antlr.v4.runtime.tree.TerminalNode;
import org.apache.commons.io.FileUtils;
import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;
import java.io.InputStreamReader;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Comparator;
import java.util.List;
/**
* Ugly inefficient class to strip unwanted tags from XML.
* Band-aid solution to unblock development
*/
public class XmlEditor {
public static String strip(File f, String newTag, String encoding) throws IOException {
FileInputStream fin = new FileInputStream(f);
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();
ElementContext root = expr.element();
if (root == null || !"layout".equals(nodeName(root))) {
return null; // not a binding layout
}
List<? extends ElementContext> childrenOfRoot = elements(root);
List<? extends ElementContext> dataNodes = filterNodesByName("data", childrenOfRoot);
if (dataNodes.size() > 1) {
L.e("Multiple binding data tags in %s. Expecting a maximum of one.",
f.getAbsolutePath());
}
ArrayList<String> lines = new ArrayList<String>();
lines.addAll(FileUtils.readLines(f, encoding));
for (ElementContext it : dataNodes) {
replace(lines, toPosition(it.getStart()), toEndPosition(it.getStop()), "");
}
List<? extends ElementContext> layoutNodes =
excludeNodesByName("data", childrenOfRoot);
if (layoutNodes.size() != 1) {
L.e("Only one layout element and one data element are allowed. %s has %d",
f.getAbsolutePath(), layoutNodes.size());
}
final ElementContext layoutNode = layoutNodes.get(0);
ArrayList<TagAndContext> noTag = new ArrayList<TagAndContext>();
recurseReplace(layoutNode, lines, noTag, newTag, 0);
// Remove the <layout>
Position rootStartTag = toPosition(root.getStart());
Position rootEndTag = toPosition(root.content().getStart());
replace(lines, rootStartTag, rootEndTag, "");
// Remove the </layout>
PositionPair endLayoutPositions = findTerminalPositions(root, lines);
replace(lines, endLayoutPositions.left, endLayoutPositions.right, "");
StringBuilder rootAttributes = new StringBuilder();
for (AttributeContext attr : attributes(root)) {
rootAttributes.append(' ').append(attr.getText());
}
TagAndContext noTagRoot = null;
for (TagAndContext tagAndContext : noTag) {
if (tagAndContext.getContext() == layoutNode) {
noTagRoot = tagAndContext;
break;
}
}
if (noTagRoot != null) {
TagAndContext newRootTag = new TagAndContext(
noTagRoot.getTag() + rootAttributes.toString(), layoutNode);
int index = noTag.indexOf(noTagRoot);
noTag.set(index, newRootTag);
} else {
TagAndContext newRootTag =
new TagAndContext(rootAttributes.toString(), layoutNode);
noTag.add(newRootTag);
}
//noinspection NullableProblems
Collections.sort(noTag, new Comparator<TagAndContext>() {
@Override
public int compare(TagAndContext o1, TagAndContext o2) {
Position start1 = toPosition(o1.getContext().getStart());
Position start2 = toPosition(o2.getContext().getStart());
int lineCmp = start2.line - start1.line;
if (lineCmp != 0) {
return lineCmp;
}
return start2.charIndex - start1.charIndex;
}
});
for (TagAndContext it : noTag) {
ElementContext element = it.getContext();
String tag = it.getTag();
Position endTagPosition = endTagPosition(element);
fixPosition(lines, endTagPosition);
String line = lines.get(endTagPosition.line);
String newLine = line.substring(0, endTagPosition.charIndex) + " " + tag +
line.substring(endTagPosition.charIndex);
lines.set(endTagPosition.line, newLine);
}
return Joiner.on(StringUtils.LINE_SEPARATOR).join(lines);
}
private static <T extends XMLParser.ElementContext> List<T>
filterNodesByName(String name, Iterable<T> items) {
List<T> result = new ArrayList<T>();
for (T item : items) {
if (name.equals(nodeName(item))) {
result.add(item);
}
}
return result;
}
private static <T extends XMLParser.ElementContext> List<T>
excludeNodesByName(String name, Iterable<T> items) {
List<T> result = new ArrayList<T>();
for (T item : items) {
if (!name.equals(nodeName(item))) {
result.add(item);
}
}
return result;
}
private static Position toPosition(Token token) {
return new Position(token.getLine() - 1, token.getCharPositionInLine());
}
private static Position toEndPosition(Token token) {
return new Position(token.getLine() - 1,
token.getCharPositionInLine() + token.getText().length());
}
public static String nodeName(ElementContext elementContext) {
return elementContext.elmName.getText();
}
public static List<? extends AttributeContext> attributes(ElementContext elementContext) {
if (elementContext.attribute() == null)
return new ArrayList<AttributeContext>();
else {
return elementContext.attribute();
}
}
public static List<? extends AttributeContext> expressionAttributes(
ElementContext elementContext) {
List<AttributeContext> result = new ArrayList<AttributeContext>();
for (AttributeContext input : attributes(elementContext)) {
String attrName = input.attrName.getText();
boolean isExpression = attrName.equals("android:tag");
if (!isExpression) {
final String value = input.attrValue.getText();
isExpression = isExpressionText(input.attrValue.getText());
}
if (isExpression) {
result.add(input);
}
}
return result;
}
private static boolean isExpressionText(String value) {
// Check if the expression ends with "}" and starts with "@{" or "@={", ignoring
// the surrounding quotes.
return (value.length() > 5 && value.charAt(value.length() - 2) == '}' &&
("@{".equals(value.substring(1, 3)) || "@={".equals(value.substring(1, 4))));
}
private static Position endTagPosition(ElementContext context) {
if (context.content() == null) {
// no content, so just choose the start of the "/>"
Position endTag = toPosition(context.getStop());
if (endTag.charIndex <= 0) {
L.e("invalid input in %s", context);
}
return endTag;
} else {
// tag with no attributes, but with content
Position position = toPosition(context.content().getStart());
if (position.charIndex <= 0) {
L.e("invalid input in %s", context);
}
position.charIndex--;
return position;
}
}
public static List<? extends ElementContext> elements(ElementContext context) {
if (context.content() != null && context.content().element() != null) {
return context.content().element();
}
return new ArrayList<ElementContext>();
}
private static boolean replace(ArrayList<String> lines, Position start, Position end,
String text) {
fixPosition(lines, start);
fixPosition(lines, end);
if (start.line != end.line) {
String startLine = lines.get(start.line);
String newStartLine = startLine.substring(0, start.charIndex) + text;
lines.set(start.line, newStartLine);
for (int i = start.line + 1; i < end.line; i++) {
String line = lines.get(i);
lines.set(i, replaceWithSpaces(line, 0, line.length() - 1));
}
String endLine = lines.get(end.line);
String newEndLine = replaceWithSpaces(endLine, 0, end.charIndex - 1);
lines.set(end.line, newEndLine);
return true;
} else if (end.charIndex - start.charIndex >= text.length()) {
String line = lines.get(start.line);
int endTextIndex = start.charIndex + text.length();
String replacedText = replaceRange(line, start.charIndex, endTextIndex, text);
String spacedText = replaceWithSpaces(replacedText, endTextIndex, end.charIndex - 1);
lines.set(start.line, spacedText);
return true;
} else {
String line = lines.get(start.line);
String newLine = replaceWithSpaces(line, start.charIndex, end.charIndex - 1);
lines.set(start.line, newLine);
return false;
}
}
private static String replaceRange(String line, int start, int end, String newText) {
return line.substring(0, start) + newText + line.substring(end);
}
public static boolean hasExpressionAttributes(ElementContext context) {
List<? extends AttributeContext> expressions = expressionAttributes(context);
int size = expressions.size();
if (size == 0) {
return false;
} else if (size > 1) {
return true;
} else {
// android:tag is included, regardless, so we must only count as an expression
// if android:tag has a binding expression.
return isExpressionText(expressions.get(0).attrValue.getText());
}
}
private static int recurseReplace(ElementContext node, ArrayList<String> lines,
ArrayList<TagAndContext> noTag,
String newTag, int bindingIndex) {
int nextBindingIndex = bindingIndex;
boolean isMerge = "merge".equals(nodeName(node));
final boolean containsInclude = filterNodesByName("include", elements(node)).size() > 0;
if (!isMerge && (hasExpressionAttributes(node) || newTag != null || containsInclude)) {
String tag = "";
if (newTag != null) {
tag = "android:tag=\"" + newTag + "_" + bindingIndex + "\"";
nextBindingIndex++;
} else if (!"include".equals(nodeName(node))) {
tag = "android:tag=\"binding_" + bindingIndex + "\"";
nextBindingIndex++;
}
for (AttributeContext it : expressionAttributes(node)) {
Position start = toPosition(it.getStart());
Position end = toEndPosition(it.getStop());
String defaultVal = defaultReplacement(it);
if (defaultVal != null) {
replace(lines, start, end, it.attrName.getText() + "=\"" + defaultVal + "\"");
} else if (replace(lines, start, end, tag)) {
tag = "";
}
}
if (tag.length() != 0) {
noTag.add(new TagAndContext(tag, node));
}
}
String nextTag;
if (bindingIndex == 0 && isMerge) {
nextTag = newTag;
} else {
nextTag = null;
}
for (ElementContext it : elements(node)) {
nextBindingIndex = recurseReplace(it, lines, noTag, nextTag, nextBindingIndex);
}
return nextBindingIndex;
}
private static String defaultReplacement(XMLParser.AttributeContext attr) {
String textWithQuotes = attr.attrValue.getText();
String escapedText = textWithQuotes.substring(1, textWithQuotes.length() - 1);
final boolean isTwoWay = escapedText.startsWith("@={");
final boolean isOneWay = escapedText.startsWith("@{");
if ((!isTwoWay && !isOneWay) || !escapedText.endsWith("}")) {
return null;
}
final int startIndex = isTwoWay ? 3 : 2;
final int endIndex = escapedText.length() - 1;
String text = StringUtils.unescapeXml(escapedText.substring(startIndex, endIndex));
ANTLRInputStream inputStream = new ANTLRInputStream(text);
BindingExpressionLexer lexer = new BindingExpressionLexer(inputStream);
CommonTokenStream tokenStream = new CommonTokenStream(lexer);
BindingExpressionParser parser = new BindingExpressionParser(tokenStream);
BindingExpressionParser.BindingSyntaxContext root = parser.bindingSyntax();
BindingExpressionParser.DefaultsContext defaults = root
.accept(new BindingExpressionBaseVisitor<BindingExpressionParser.DefaultsContext>() {
@Override
public BindingExpressionParser.DefaultsContext visitDefaults(
@NotNull BindingExpressionParser.DefaultsContext ctx) {
return ctx;
}
});
if (defaults != null) {
BindingExpressionParser.ConstantValueContext constantValue = defaults
.constantValue();
BindingExpressionParser.LiteralContext literal = constantValue.literal();
if (literal != null) {
BindingExpressionParser.StringLiteralContext stringLiteral = literal
.stringLiteral();
if (stringLiteral != null) {
TerminalNode doubleQuote = stringLiteral.DoubleQuoteString();
if (doubleQuote != null) {
String quotedStr = doubleQuote.getText();
String unquoted = quotedStr.substring(1, quotedStr.length() - 1);
return XmlEscapers.xmlAttributeEscaper().escape(unquoted);
} else {
String quotedStr = stringLiteral.SingleQuoteString().getText();
String unquoted = quotedStr.substring(1, quotedStr.length() - 1);
String unescaped = unquoted.replace("\"", "\\\"").replace("\\`", "`");
return XmlEscapers.xmlAttributeEscaper().escape(unescaped);
}
}
}
return constantValue.getText();
}
return null;
}
private static PositionPair findTerminalPositions(ElementContext node,
ArrayList<String> lines) {
Position endPosition = toEndPosition(node.getStop());
Position startPosition = toPosition(node.getStop());
int index;
do {
index = lines.get(startPosition.line).lastIndexOf("</");
startPosition.line--;
} while (index < 0);
startPosition.line++;
startPosition.charIndex = index;
//noinspection unchecked
return new PositionPair(startPosition, endPosition);
}
private static String replaceWithSpaces(String line, int start, int end) {
StringBuilder lineBuilder = new StringBuilder(line);
for (int i = start; i <= end; i++) {
lineBuilder.setCharAt(i, ' ');
}
return lineBuilder.toString();
}
private static void fixPosition(ArrayList<String> lines, Position pos) {
String line = lines.get(pos.line);
while (pos.charIndex > line.length()) {
pos.charIndex--;
}
}
private static class Position {
int line;
int charIndex;
public Position(int line, int charIndex) {
this.line = line;
this.charIndex = charIndex;
}
}
private static class TagAndContext {
private final String mTag;
private final ElementContext mElementContext;
private TagAndContext(String tag, ElementContext elementContext) {
mTag = tag;
mElementContext = elementContext;
}
private ElementContext getContext() {
return mElementContext;
}
private String getTag() {
return mTag;
}
}
private static class PositionPair {
private final Position left;
private final Position right;
private PositionPair(Position left, Position right) {
this.left = left;
this.right = right;
}
}
}