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