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