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 android.databinding.parser.BindingExpressionLexer; 20 import android.databinding.parser.BindingExpressionParser; 21 import android.databinding.parser.XMLLexer; 22 import android.databinding.parser.XMLParser; 23 import android.databinding.parser.XMLParser.AttributeContext; 24 import android.databinding.parser.XMLParser.ElementContext; 25 26 import com.google.common.base.Joiner; 27 import com.google.common.xml.XmlEscapers; 28 29 import org.antlr.v4.runtime.ANTLRInputStream; 30 import org.antlr.v4.runtime.CommonTokenStream; 31 import org.antlr.v4.runtime.Token; 32 import org.antlr.v4.runtime.tree.TerminalNode; 33 import org.apache.commons.io.FileUtils; 34 35 import java.io.File; 36 import java.io.FileInputStream; 37 import java.io.IOException; 38 import java.io.InputStreamReader; 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 strip(File f, String newTag, String encoding)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> filterNodesByName(String name, Iterable<T> items)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> excludeNodesByName(String name, Iterable<T> items)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 toPosition(Token token)168 private static Position toPosition(Token token) { 169 return new Position(token.getLine() - 1, token.getCharPositionInLine()); 170 } 171 toEndPosition(Token token)172 private static Position toEndPosition(Token token) { 173 return new Position(token.getLine() - 1, 174 token.getCharPositionInLine() + token.getText().length()); 175 } 176 nodeName(ElementContext elementContext)177 public static String nodeName(ElementContext elementContext) { 178 return elementContext.elmName.getText(); 179 } 180 attributes(ElementContext elementContext)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 expressionAttributes( ElementContext elementContext)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 isExpressionText(String value)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 endTagPosition(ElementContext context)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 elements(ElementContext context)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 replace(ArrayList<String> lines, Position start, Position end, String text)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 replaceRange(String line, int start, int end, String newText)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 hasExpressionAttributes(ElementContext context)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 recurseReplace(ElementContext node, ArrayList<String> lines, ArrayList<TagAndContext> noTag, String newTag, int bindingIndex)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 defaultReplacement(XMLParser.AttributeContext attr)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 findTerminalPositions(ElementContext node, ArrayList<String> lines)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 replaceWithSpaces(String line, int start, int end)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 fixPosition(ArrayList<String> lines, Position pos)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 Position(int line, int charIndex)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 TagAndContext(String tag, ElementContext elementContext)418 private TagAndContext(String tag, ElementContext elementContext) { 419 mTag = tag; 420 mElementContext = elementContext; 421 } 422 getContext()423 private ElementContext getContext() { 424 return mElementContext; 425 } 426 getTag()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 PositionPair(Position left, Position right)436 private PositionPair(Position left, Position right) { 437 this.left = left; 438 this.right = right; 439 } 440 } 441 } 442