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