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