• 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 org.antlr.v4.runtime.ANTLRInputStream;
20 import org.antlr.v4.runtime.CommonTokenStream;
21 import org.antlr.v4.runtime.Token;
22 import org.antlr.v4.runtime.tree.TerminalNode;
23 import org.apache.commons.io.FileUtils;
24 import org.apache.commons.lang3.StringEscapeUtils;
25 import org.apache.commons.lang3.StringUtils;
26 import org.apache.commons.lang3.tuple.ImmutablePair;
27 import org.apache.commons.lang3.tuple.Pair;
28 
29 import android.databinding.parser.BindingExpressionLexer;
30 import android.databinding.parser.BindingExpressionParser;
31 import android.databinding.parser.XMLLexer;
32 import android.databinding.parser.XMLParser;
33 import android.databinding.parser.XMLParser.AttributeContext;
34 import android.databinding.parser.XMLParser.ElementContext;
35 
36 import java.io.File;
37 import java.io.FileReader;
38 import java.io.IOException;
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)50     public static String strip(File f, String newTag) throws IOException {
51         ANTLRInputStream inputStream = new ANTLRInputStream(new FileReader(f));
52         XMLLexer lexer = new XMLLexer(inputStream);
53         CommonTokenStream tokenStream = new CommonTokenStream(lexer);
54         XMLParser parser = new XMLParser(tokenStream);
55         XMLParser.DocumentContext expr = parser.document();
56         XMLParser.ElementContext root = expr.element();
57 
58         if (root == null || !"layout".equals(nodeName(root))) {
59             return null; // not a binding layout
60         }
61 
62         List<? extends ElementContext> childrenOfRoot = elements(root);
63         List<? extends XMLParser.ElementContext> dataNodes = filterNodesByName("data",
64                 childrenOfRoot);
65         if (dataNodes.size() > 1) {
66             L.e("Multiple binding data tags in %s. Expecting a maximum of one.",
67                     f.getAbsolutePath());
68         }
69 
70         ArrayList<String> lines = new ArrayList<>();
71         lines.addAll(FileUtils.readLines(f, "utf-8"));
72 
73         for (android.databinding.parser.XMLParser.ElementContext it : dataNodes) {
74             replace(lines, toPosition(it.getStart()), toEndPosition(it.getStop()), "");
75         }
76         List<? extends XMLParser.ElementContext> layoutNodes =
77                 excludeNodesByName("data", childrenOfRoot);
78         if (layoutNodes.size() != 1) {
79             L.e("Only one layout element and one data element are allowed. %s has %d",
80                     f.getAbsolutePath(), layoutNodes.size());
81         }
82 
83         final XMLParser.ElementContext layoutNode = layoutNodes.get(0);
84 
85         ArrayList<Pair<String, android.databinding.parser.XMLParser.ElementContext>> noTag =
86                 new ArrayList<>();
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         ImmutablePair<Position, Position> 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         Pair<String, XMLParser.ElementContext> noTagRoot = null;
104         for (Pair<String, XMLParser.ElementContext> pair : noTag) {
105             if (pair.getRight() == layoutNode) {
106                 noTagRoot = pair;
107                 break;
108             }
109         }
110         if (noTagRoot != null) {
111             ImmutablePair<String, XMLParser.ElementContext>
112                     newRootTag = new ImmutablePair<>(
113                     noTagRoot.getLeft() + rootAttributes.toString(), layoutNode);
114             int index = noTag.indexOf(noTagRoot);
115             noTag.set(index, newRootTag);
116         } else {
117             ImmutablePair<String, XMLParser.ElementContext> newRootTag =
118                     new ImmutablePair<>(rootAttributes.toString(), layoutNode);
119             noTag.add(newRootTag);
120         }
121         //noinspection NullableProblems
122         Collections.sort(noTag, new Comparator<Pair<String, XMLParser.ElementContext>>() {
123             @Override
124             public int compare(Pair<String, XMLParser.ElementContext> o1,
125                     Pair<String, XMLParser.ElementContext> o2) {
126                 Position start1 = toPosition(o1.getRight().getStart());
127                 Position start2 = toPosition(o2.getRight().getStart());
128                 int lineCmp = Integer.compare(start2.line, start1.line);
129                 if (lineCmp != 0) {
130                     return lineCmp;
131                 }
132                 return Integer.compare(start2.charIndex, start1.charIndex);
133             }
134         });
135         for (Pair<String, android.databinding.parser.XMLParser.ElementContext> it : noTag) {
136             XMLParser.ElementContext element = it.getRight();
137             String tag = it.getLeft();
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 StringUtils.join(lines, System.getProperty("line.separator"));
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<>();
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<>();
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(XMLParser.ElementContext elementContext)179     public static String nodeName(XMLParser.ElementContext elementContext) {
180         return elementContext.elmName.getText();
181     }
182 
attributes(XMLParser.ElementContext elementContext)183     public static List<? extends AttributeContext> attributes(XMLParser.ElementContext elementContext) {
184         if (elementContext.attribute() == null) {
185             return new ArrayList<>();
186         } else {
187             return elementContext.attribute();
188         }
189     }
190 
expressionAttributes( XMLParser.ElementContext elementContext)191     public static List<? extends AttributeContext> expressionAttributes (
192             XMLParser.ElementContext elementContext) {
193         List<AttributeContext> result = new ArrayList<>();
194         for (AttributeContext input : attributes(elementContext)) {
195             String attrName = input.attrName.getText();
196             String value = input.attrValue.getText();
197             if (attrName.equals("android:tag") ||
198                     (value.startsWith("\"@{") && value.endsWith("}\"")) ||
199                     (value.startsWith("'@{") && value.endsWith("}'"))) {
200                 result.add(input);
201             }
202         }
203         return result;
204     }
205 
endTagPosition(XMLParser.ElementContext context)206     private static Position endTagPosition(XMLParser.ElementContext context) {
207         if (context.content() == null) {
208             // no content, so just subtract from the "/>"
209             Position endTag = toEndPosition(context.getStop());
210             if (endTag.charIndex <= 0) {
211                 L.e("invalid input in %s", context);
212             }
213             endTag.charIndex -= 2;
214             return endTag;
215         } else {
216             // tag with no attributes, but with content
217             Position position = toPosition(context.content().getStart());
218             if (position.charIndex <= 0) {
219                 L.e("invalid input in %s", context);
220             }
221             position.charIndex--;
222             return position;
223         }
224     }
225 
elements( XMLParser.ElementContext context)226     public static List<? extends android.databinding.parser.XMLParser.ElementContext> elements(
227             XMLParser.ElementContext context) {
228         if (context.content() != null && context.content().element() != null) {
229             return context.content().element();
230         }
231         return new ArrayList<>();
232     }
233 
replace(ArrayList<String> lines, Position start, Position end, String text)234     private static boolean replace(ArrayList<String> lines, Position start, Position end,
235             String text) {
236         fixPosition(lines, start);
237         fixPosition(lines, end);
238         if (start.line != end.line) {
239             String startLine = lines.get(start.line);
240             String newStartLine = startLine.substring(0, start.charIndex) + text;
241             lines.set(start.line, newStartLine);
242             for (int i = start.line + 1; i < end.line; i++) {
243                 String line = lines.get(i);
244                 lines.set(i, replaceWithSpaces(line, 0, line.length() - 1));
245             }
246             String endLine = lines.get(end.line);
247             String newEndLine = replaceWithSpaces(endLine, 0, end.charIndex - 1);
248             lines.set(end.line, newEndLine);
249             return true;
250         } else if (end.charIndex - start.charIndex >= text.length()) {
251             String line = lines.get(start.line);
252             int endTextIndex = start.charIndex + text.length();
253             String replacedText = replaceRange(line, start.charIndex, endTextIndex, text);
254             String spacedText = replaceWithSpaces(replacedText, endTextIndex, end.charIndex - 1);
255             lines.set(start.line, spacedText);
256             return true;
257         } else {
258             String line = lines.get(start.line);
259             String newLine = replaceWithSpaces(line, start.charIndex, end.charIndex - 1);
260             lines.set(start.line, newLine);
261             return false;
262         }
263     }
264 
replaceRange(String line, int start, int end, String newText)265     private static String replaceRange(String line, int start, int end, String newText) {
266         return line.substring(0, start) + newText + line.substring(end);
267     }
268 
hasExpressionAttributes(XMLParser.ElementContext context)269     public static boolean hasExpressionAttributes(XMLParser.ElementContext context) {
270         List<? extends AttributeContext> expressions = expressionAttributes(context);
271         int size = expressions.size();
272         //noinspection ConstantConditions
273         return size > 1 || (size == 1 &&
274                 !expressions.get(0).attrName.getText().equals("android:tag"));
275     }
276 
recurseReplace(XMLParser.ElementContext node, ArrayList<String> lines, ArrayList<Pair<String, XMLParser.ElementContext>> noTag, String newTag, int bindingIndex)277     private static int recurseReplace(XMLParser.ElementContext node, ArrayList<String> lines,
278             ArrayList<Pair<String, XMLParser.ElementContext>> noTag,
279             String newTag, int bindingIndex) {
280         int nextBindingIndex = bindingIndex;
281         boolean isMerge = "merge".equals(nodeName(node));
282         final boolean containsInclude = filterNodesByName("include", elements(node)).size() > 0;
283         if (!isMerge && (hasExpressionAttributes(node) || newTag != null || containsInclude)) {
284             String tag = "";
285             if (newTag != null) {
286                 tag = "android:tag=\"" + newTag + "_" + bindingIndex + "\"";
287                 nextBindingIndex++;
288             } else if (!"include".equals(nodeName(node))) {
289                 tag = "android:tag=\"binding_" + bindingIndex + "\"";
290                 nextBindingIndex++;
291             }
292             for (AttributeContext it : expressionAttributes(node)) {
293                 Position start = toPosition(it.getStart());
294                 Position end = toEndPosition(it.getStop());
295                 String defaultVal = defaultReplacement(it);
296                 if (defaultVal != null) {
297                     replace(lines, start, end, it.attrName.getText() + "=\"" + defaultVal + "\"");
298                 } else if (replace(lines, start, end, tag)) {
299                     tag = "";
300                 }
301             }
302             if (tag.length() != 0) {
303                 noTag.add(new ImmutablePair<>(tag, node));
304             }
305         }
306 
307         String nextTag;
308         if (bindingIndex == 0 && isMerge) {
309             nextTag = newTag;
310         } else {
311             nextTag = null;
312         }
313         for (XMLParser.ElementContext it : elements(node)) {
314             nextBindingIndex = recurseReplace(it, lines, noTag, nextTag, nextBindingIndex);
315         }
316         return nextBindingIndex;
317     }
318 
defaultReplacement(XMLParser.AttributeContext attr)319     private static String defaultReplacement(XMLParser.AttributeContext attr) {
320         String textWithQuotes = attr.attrValue.getText();
321         String escapedText = textWithQuotes.substring(1, textWithQuotes.length() - 1);
322         if (!escapedText.startsWith("@{") || !escapedText.endsWith("}")) {
323             return null;
324         }
325         String text = StringEscapeUtils
326                 .unescapeXml(escapedText.substring(2, escapedText.length() - 1));
327         ANTLRInputStream inputStream = new ANTLRInputStream(text);
328         BindingExpressionLexer lexer = new BindingExpressionLexer(inputStream);
329         CommonTokenStream tokenStream = new CommonTokenStream(lexer);
330         BindingExpressionParser parser = new BindingExpressionParser(tokenStream);
331         BindingExpressionParser.BindingSyntaxContext root = parser.bindingSyntax();
332         BindingExpressionParser.DefaultsContext defaults = root.defaults();
333         if (defaults != null) {
334             BindingExpressionParser.ConstantValueContext constantValue = defaults
335                     .constantValue();
336             BindingExpressionParser.LiteralContext literal = constantValue.literal();
337             if (literal != null) {
338                 BindingExpressionParser.StringLiteralContext stringLiteral = literal
339                         .stringLiteral();
340                 if (stringLiteral != null) {
341                     TerminalNode doubleQuote = stringLiteral.DoubleQuoteString();
342                     if (doubleQuote != null) {
343                         String quotedStr = doubleQuote.getText();
344                         String unquoted = quotedStr.substring(1, quotedStr.length() - 1);
345                         return StringEscapeUtils.escapeXml10(unquoted);
346                     } else {
347                         String quotedStr = stringLiteral.SingleQuoteString().getText();
348                         String unquoted = quotedStr.substring(1, quotedStr.length() - 1);
349                         String unescaped = unquoted.replace("\"", "\\\"").replace("\\`", "`");
350                         return StringEscapeUtils.escapeXml10(unescaped);
351                     }
352                 }
353             }
354             return constantValue.getText();
355         }
356         return null;
357     }
358 
findTerminalPositions( XMLParser.ElementContext node, ArrayList<String> lines)359     private static ImmutablePair<Position, Position> findTerminalPositions(
360             XMLParser.ElementContext node,  ArrayList<String> lines) {
361         Position endPosition = toEndPosition(node.getStop());
362         Position startPosition = toPosition(node.getStop());
363         int index;
364         do {
365             index = lines.get(startPosition.line).lastIndexOf("</");
366             startPosition.line--;
367         } while (index < 0);
368         startPosition.line++;
369         startPosition.charIndex = index;
370         //noinspection unchecked
371         return new ImmutablePair<>(startPosition, endPosition);
372     }
373 
replaceWithSpaces(String line, int start, int end)374     private static String replaceWithSpaces(String line, int start, int end) {
375         StringBuilder lineBuilder = new StringBuilder(line);
376         for (int i = start; i <= end; i++) {
377             lineBuilder.setCharAt(i, ' ');
378         }
379         return lineBuilder.toString();
380     }
381 
fixPosition(ArrayList<String> lines, Position pos)382     private static void fixPosition(ArrayList<String> lines, Position pos) {
383         String line = lines.get(pos.line);
384         while (pos.charIndex > line.length()) {
385             pos.charIndex--;
386         }
387     }
388 
389     private static class Position {
390 
391         int line;
392         int charIndex;
393 
Position(int line, int charIndex)394         public Position(int line, int charIndex) {
395             this.line = line;
396             this.charIndex = charIndex;
397         }
398     }
399 
400 }
401