Home | History | Annotate | Download | only in util
      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 
     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>
    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>
    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 
    170     private static Position toPosition(Token token) {
    171         return new Position(token.getLine() - 1, token.getCharPositionInLine());
    172     }
    173 
    174     private static Position toEndPosition(Token token) {
    175         return new Position(token.getLine() - 1,
    176                 token.getCharPositionInLine() + token.getText().length());
    177     }
    178 
    179     public static String nodeName(XMLParser.ElementContext elementContext) {
    180         return elementContext.elmName.getText();
    181     }
    182 
    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 
    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 
    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 
    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 
    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 
    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 
    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 
    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 
    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 
    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 
    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 
    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 
    394         public Position(int line, int charIndex) {
    395             this.line = line;
    396             this.charIndex = charIndex;
    397         }
    398     }
    399 
    400 }
    401