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