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