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