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