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