1 /*
2  * Copyright (C) 2014 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 
17 package android.databinding.tool.util;
18 
19 import android.databinding.parser.BindingExpressionLexer;
20 import android.databinding.parser.BindingExpressionParser;
21 import android.databinding.parser.XMLLexer;
22 import android.databinding.parser.XMLParser;
23 import android.databinding.parser.XMLParser.AttributeContext;
24 import android.databinding.parser.XMLParser.ElementContext;
25 
26 import com.google.common.base.Joiner;
27 import com.google.common.xml.XmlEscapers;
28 
29 import org.antlr.v4.runtime.ANTLRInputStream;
30 import org.antlr.v4.runtime.CommonTokenStream;
31 import org.antlr.v4.runtime.Token;
32 import org.antlr.v4.runtime.tree.TerminalNode;
33 import org.apache.commons.io.FileUtils;
34 
35 import java.io.File;
36 import java.io.FileInputStream;
37 import java.io.IOException;
38 import java.io.InputStreamReader;
39 import java.util.ArrayList;
40 import java.util.Collections;
41 import java.util.Comparator;
42 import java.util.List;
43 
44 /**
45  * Ugly inefficient class to strip unwanted tags from XML.
46  * Band-aid solution to unblock development
47  */
48 public class XmlEditor {
49 
strip(File f, String newTag, String encoding)50     public static String strip(File f, String newTag, String encoding) throws IOException {
51         FileInputStream fin = new FileInputStream(f);
52         InputStreamReader reader = new InputStreamReader(fin, encoding);
53         ANTLRInputStream inputStream = new ANTLRInputStream(reader);
54         XMLLexer lexer = new XMLLexer(inputStream);
55         CommonTokenStream tokenStream = new CommonTokenStream(lexer);
56         XMLParser parser = new XMLParser(tokenStream);
57         XMLParser.DocumentContext expr = parser.document();
58         ElementContext root = expr.element();
59 
60         if (root == null || !"layout".equals(nodeName(root))) {
61             return null; // not a binding layout
62         }
63 
64         List<? extends ElementContext> childrenOfRoot = elements(root);
65         List<? extends ElementContext> dataNodes = filterNodesByName("data", childrenOfRoot);
66         if (dataNodes.size() > 1) {
67             L.e("Multiple binding data tags in %s. Expecting a maximum of one.",
68                     f.getAbsolutePath());
69         }
70 
71         ArrayList<String> lines = new ArrayList<String>();
72         lines.addAll(FileUtils.readLines(f, "utf-8"));
73 
74         for (ElementContext it : dataNodes) {
75             replace(lines, toPosition(it.getStart()), toEndPosition(it.getStop()), "");
76         }
77         List<? extends ElementContext> layoutNodes =
78                 excludeNodesByName("data", childrenOfRoot);
79         if (layoutNodes.size() != 1) {
80             L.e("Only one layout element and one data element are allowed. %s has %d",
81                     f.getAbsolutePath(), layoutNodes.size());
82         }
83 
84         final ElementContext layoutNode = layoutNodes.get(0);
85 
86         ArrayList<TagAndContext> noTag = new ArrayList<TagAndContext>();
87 
88         recurseReplace(layoutNode, lines, noTag, newTag, 0);
89 
90         // Remove the <layout>
91         Position rootStartTag = toPosition(root.getStart());
92         Position rootEndTag = toPosition(root.content().getStart());
93         replace(lines, rootStartTag, rootEndTag, "");
94 
95         // Remove the </layout>
96         PositionPair endLayoutPositions = findTerminalPositions(root, lines);
97         replace(lines, endLayoutPositions.left, endLayoutPositions.right, "");
98 
99         StringBuilder rootAttributes = new StringBuilder();
100         for (AttributeContext attr : attributes(root)) {
101             rootAttributes.append(' ').append(attr.getText());
102         }
103         TagAndContext noTagRoot = null;
104         for (TagAndContext tagAndContext : noTag) {
105             if (tagAndContext.getContext() == layoutNode) {
106                 noTagRoot = tagAndContext;
107                 break;
108             }
109         }
110         if (noTagRoot != null) {
111             TagAndContext newRootTag = new TagAndContext(
112                     noTagRoot.getTag() + rootAttributes.toString(), layoutNode);
113             int index = noTag.indexOf(noTagRoot);
114             noTag.set(index, newRootTag);
115         } else {
116             TagAndContext newRootTag =
117                     new TagAndContext(rootAttributes.toString(), layoutNode);
118             noTag.add(newRootTag);
119         }
120         //noinspection NullableProblems
121         Collections.sort(noTag, new Comparator<TagAndContext>() {
122             @Override
123             public int compare(TagAndContext o1, TagAndContext o2) {
124                 Position start1 = toPosition(o1.getContext().getStart());
125                 Position start2 = toPosition(o2.getContext().getStart());
126                 int lineCmp = start2.line - start1.line;
127                 if (lineCmp != 0) {
128                     return lineCmp;
129                 }
130                 return start2.charIndex - start1.charIndex;
131             }
132         });
133         for (TagAndContext it : noTag) {
134             ElementContext element = it.getContext();
135             String tag = it.getTag();
136             Position endTagPosition = endTagPosition(element);
137             fixPosition(lines, endTagPosition);
138             String line = lines.get(endTagPosition.line);
139             String newLine = line.substring(0, endTagPosition.charIndex) + " " + tag +
140                     line.substring(endTagPosition.charIndex);
141             lines.set(endTagPosition.line, newLine);
142         }
143         return Joiner.on(StringUtils.LINE_SEPARATOR).join(lines);
144     }
145 
146     private static <T extends XMLParser.ElementContext> List<T>
filterNodesByName(String name, Iterable<T> items)147             filterNodesByName(String name, Iterable<T> items) {
148         List<T> result = new ArrayList<T>();
149         for (T item : items) {
150             if (name.equals(nodeName(item))) {
151                 result.add(item);
152             }
153         }
154         return result;
155     }
156 
157     private static <T extends XMLParser.ElementContext> List<T>
excludeNodesByName(String name, Iterable<T> items)158             excludeNodesByName(String name, Iterable<T> items) {
159         List<T> result = new ArrayList<T>();
160         for (T item : items) {
161             if (!name.equals(nodeName(item))) {
162                 result.add(item);
163             }
164         }
165         return result;
166     }
167 
toPosition(Token token)168     private static Position toPosition(Token token) {
169         return new Position(token.getLine() - 1, token.getCharPositionInLine());
170     }
171 
toEndPosition(Token token)172     private static Position toEndPosition(Token token) {
173         return new Position(token.getLine() - 1,
174                 token.getCharPositionInLine() + token.getText().length());
175     }
176 
nodeName(ElementContext elementContext)177     public static String nodeName(ElementContext elementContext) {
178         return elementContext.elmName.getText();
179     }
180 
attributes(ElementContext elementContext)181     public static List<? extends AttributeContext> attributes(ElementContext elementContext) {
182         if (elementContext.attribute() == null)
183             return new ArrayList<AttributeContext>();
184         else {
185             return elementContext.attribute();
186         }
187     }
188 
expressionAttributes( ElementContext elementContext)189     public static List<? extends AttributeContext> expressionAttributes(
190             ElementContext elementContext) {
191         List<AttributeContext> result = new ArrayList<AttributeContext>();
192         for (AttributeContext input : attributes(elementContext)) {
193             String attrName = input.attrName.getText();
194             boolean isExpression = attrName.equals("android:tag");
195             if (!isExpression) {
196                 final String value = input.attrValue.getText();
197                 isExpression = isExpressionText(input.attrValue.getText());
198             }
199             if (isExpression) {
200                 result.add(input);
201             }
202         }
203         return result;
204     }
205 
isExpressionText(String value)206     private static boolean isExpressionText(String value) {
207         // Check if the expression ends with "}" and starts with "@{" or "@={", ignoring
208         // the surrounding quotes.
209         return (value.length() > 5 && value.charAt(value.length() - 2) == '}' &&
210                 ("@{".equals(value.substring(1, 3)) || "@={".equals(value.substring(1, 4))));
211     }
212 
endTagPosition(ElementContext context)213     private static Position endTagPosition(ElementContext context) {
214         if (context.content() == null) {
215             // no content, so just choose the start of the "/>"
216             Position endTag = toPosition(context.getStop());
217             if (endTag.charIndex <= 0) {
218                 L.e("invalid input in %s", context);
219             }
220             return endTag;
221         } else {
222             // tag with no attributes, but with content
223             Position position = toPosition(context.content().getStart());
224             if (position.charIndex <= 0) {
225                 L.e("invalid input in %s", context);
226             }
227             position.charIndex--;
228             return position;
229         }
230     }
231 
elements(ElementContext context)232     public static List<? extends ElementContext> elements(ElementContext context) {
233         if (context.content() != null && context.content().element() != null) {
234             return context.content().element();
235         }
236         return new ArrayList<ElementContext>();
237     }
238 
replace(ArrayList<String> lines, Position start, Position end, String text)239     private static boolean replace(ArrayList<String> lines, Position start, Position end,
240             String text) {
241         fixPosition(lines, start);
242         fixPosition(lines, end);
243         if (start.line != end.line) {
244             String startLine = lines.get(start.line);
245             String newStartLine = startLine.substring(0, start.charIndex) + text;
246             lines.set(start.line, newStartLine);
247             for (int i = start.line + 1; i < end.line; i++) {
248                 String line = lines.get(i);
249                 lines.set(i, replaceWithSpaces(line, 0, line.length() - 1));
250             }
251             String endLine = lines.get(end.line);
252             String newEndLine = replaceWithSpaces(endLine, 0, end.charIndex - 1);
253             lines.set(end.line, newEndLine);
254             return true;
255         } else if (end.charIndex - start.charIndex >= text.length()) {
256             String line = lines.get(start.line);
257             int endTextIndex = start.charIndex + text.length();
258             String replacedText = replaceRange(line, start.charIndex, endTextIndex, text);
259             String spacedText = replaceWithSpaces(replacedText, endTextIndex, end.charIndex - 1);
260             lines.set(start.line, spacedText);
261             return true;
262         } else {
263             String line = lines.get(start.line);
264             String newLine = replaceWithSpaces(line, start.charIndex, end.charIndex - 1);
265             lines.set(start.line, newLine);
266             return false;
267         }
268     }
269 
replaceRange(String line, int start, int end, String newText)270     private static String replaceRange(String line, int start, int end, String newText) {
271         return line.substring(0, start) + newText + line.substring(end);
272     }
273 
hasExpressionAttributes(ElementContext context)274     public static boolean hasExpressionAttributes(ElementContext context) {
275         List<? extends AttributeContext> expressions = expressionAttributes(context);
276         int size = expressions.size();
277         if (size == 0) {
278             return false;
279         } else if (size > 1) {
280             return true;
281         } else {
282             // android:tag is included, regardless, so we must only count as an expression
283             // if android:tag has a binding expression.
284             return isExpressionText(expressions.get(0).attrValue.getText());
285         }
286     }
287 
recurseReplace(ElementContext node, ArrayList<String> lines, ArrayList<TagAndContext> noTag, String newTag, int bindingIndex)288     private static int recurseReplace(ElementContext node, ArrayList<String> lines,
289             ArrayList<TagAndContext> noTag,
290             String newTag, int bindingIndex) {
291         int nextBindingIndex = bindingIndex;
292         boolean isMerge = "merge".equals(nodeName(node));
293         final boolean containsInclude = filterNodesByName("include", elements(node)).size() > 0;
294         if (!isMerge && (hasExpressionAttributes(node) || newTag != null || containsInclude)) {
295             String tag = "";
296             if (newTag != null) {
297                 tag = "android:tag=\"" + newTag + "_" + bindingIndex + "\"";
298                 nextBindingIndex++;
299             } else if (!"include".equals(nodeName(node))) {
300                 tag = "android:tag=\"binding_" + bindingIndex + "\"";
301                 nextBindingIndex++;
302             }
303             for (AttributeContext it : expressionAttributes(node)) {
304                 Position start = toPosition(it.getStart());
305                 Position end = toEndPosition(it.getStop());
306                 String defaultVal = defaultReplacement(it);
307                 if (defaultVal != null) {
308                     replace(lines, start, end, it.attrName.getText() + "=\"" + defaultVal + "\"");
309                 } else if (replace(lines, start, end, tag)) {
310                     tag = "";
311                 }
312             }
313             if (tag.length() != 0) {
314                 noTag.add(new TagAndContext(tag, node));
315             }
316         }
317 
318         String nextTag;
319         if (bindingIndex == 0 && isMerge) {
320             nextTag = newTag;
321         } else {
322             nextTag = null;
323         }
324         for (ElementContext it : elements(node)) {
325             nextBindingIndex = recurseReplace(it, lines, noTag, nextTag, nextBindingIndex);
326         }
327         return nextBindingIndex;
328     }
329 
defaultReplacement(XMLParser.AttributeContext attr)330     private static String defaultReplacement(XMLParser.AttributeContext attr) {
331         String textWithQuotes = attr.attrValue.getText();
332         String escapedText = textWithQuotes.substring(1, textWithQuotes.length() - 1);
333         final boolean isTwoWay = escapedText.startsWith("@={");
334         final boolean isOneWay = escapedText.startsWith("@{");
335         if ((!isTwoWay && !isOneWay) || !escapedText.endsWith("}")) {
336             return null;
337         }
338         final int startIndex = isTwoWay ? 3 : 2;
339         final int endIndex = escapedText.length() - 1;
340         String text = StringUtils.unescapeXml(escapedText.substring(startIndex, endIndex));
341         ANTLRInputStream inputStream = new ANTLRInputStream(text);
342         BindingExpressionLexer lexer = new BindingExpressionLexer(inputStream);
343         CommonTokenStream tokenStream = new CommonTokenStream(lexer);
344         BindingExpressionParser parser = new BindingExpressionParser(tokenStream);
345         BindingExpressionParser.BindingSyntaxContext root = parser.bindingSyntax();
346         BindingExpressionParser.DefaultsContext defaults = root.defaults();
347         if (defaults != null) {
348             BindingExpressionParser.ConstantValueContext constantValue = defaults
349                     .constantValue();
350             BindingExpressionParser.LiteralContext literal = constantValue.literal();
351             if (literal != null) {
352                 BindingExpressionParser.StringLiteralContext stringLiteral = literal
353                         .stringLiteral();
354                 if (stringLiteral != null) {
355                     TerminalNode doubleQuote = stringLiteral.DoubleQuoteString();
356                     if (doubleQuote != null) {
357                         String quotedStr = doubleQuote.getText();
358                         String unquoted = quotedStr.substring(1, quotedStr.length() - 1);
359                         return XmlEscapers.xmlAttributeEscaper().escape(unquoted);
360                     } else {
361                         String quotedStr = stringLiteral.SingleQuoteString().getText();
362                         String unquoted = quotedStr.substring(1, quotedStr.length() - 1);
363                         String unescaped = unquoted.replace("\"", "\\\"").replace("\\`", "`");
364                         return XmlEscapers.xmlAttributeEscaper().escape(unescaped);
365                     }
366                 }
367             }
368             return constantValue.getText();
369         }
370         return null;
371     }
372 
findTerminalPositions(ElementContext node, ArrayList<String> lines)373     private static PositionPair findTerminalPositions(ElementContext node,
374             ArrayList<String> lines) {
375         Position endPosition = toEndPosition(node.getStop());
376         Position startPosition = toPosition(node.getStop());
377         int index;
378         do {
379             index = lines.get(startPosition.line).lastIndexOf("</");
380             startPosition.line--;
381         } while (index < 0);
382         startPosition.line++;
383         startPosition.charIndex = index;
384         //noinspection unchecked
385         return new PositionPair(startPosition, endPosition);
386     }
387 
replaceWithSpaces(String line, int start, int end)388     private static String replaceWithSpaces(String line, int start, int end) {
389         StringBuilder lineBuilder = new StringBuilder(line);
390         for (int i = start; i <= end; i++) {
391             lineBuilder.setCharAt(i, ' ');
392         }
393         return lineBuilder.toString();
394     }
395 
fixPosition(ArrayList<String> lines, Position pos)396     private static void fixPosition(ArrayList<String> lines, Position pos) {
397         String line = lines.get(pos.line);
398         while (pos.charIndex > line.length()) {
399             pos.charIndex--;
400         }
401     }
402 
403     private static class Position {
404 
405         int line;
406         int charIndex;
407 
Position(int line, int charIndex)408         public Position(int line, int charIndex) {
409             this.line = line;
410             this.charIndex = charIndex;
411         }
412     }
413 
414     private static class TagAndContext {
415         private final String mTag;
416         private final ElementContext mElementContext;
417 
TagAndContext(String tag, ElementContext elementContext)418         private TagAndContext(String tag, ElementContext elementContext) {
419             mTag = tag;
420             mElementContext = elementContext;
421         }
422 
getContext()423         private ElementContext getContext() {
424             return mElementContext;
425         }
426 
getTag()427         private String getTag() {
428             return mTag;
429         }
430     }
431 
432     private static class PositionPair {
433         private final Position left;
434         private final Position right;
435 
PositionPair(Position left, Position right)436         private PositionPair(Position left, Position right) {
437             this.left = left;
438             this.right = right;
439         }
440     }
441 }
442