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