1 22 package org.jboss.test.xml; 23 24 import java.io.StringReader ; 25 import java.util.ArrayList ; 26 import java.util.Collections ; 27 import java.util.List ; 28 import javax.xml.namespace.QName ; 29 import javax.xml.parsers.DocumentBuilder ; 30 import javax.xml.parsers.DocumentBuilderFactory ; 31 import javax.xml.parsers.ParserConfigurationException ; 32 import org.w3c.dom.Attr ; 33 import org.w3c.dom.Document ; 34 import org.w3c.dom.Element ; 35 import org.w3c.dom.NamedNodeMap ; 36 import org.w3c.dom.Node ; 37 import org.w3c.dom.NodeList ; 38 import org.w3c.dom.Text ; 39 import org.xml.sax.EntityResolver ; 40 import org.xml.sax.ErrorHandler ; 41 import org.xml.sax.InputSource ; 42 import org.xml.sax.SAXException ; 43 import org.xml.sax.SAXParseException ; 44 import org.jboss.xb.binding.Constants; 45 46 50 public class XmlDiff 51 { 52 public static final ErrorHandler ERROR_HANDLER = new DefErrorHandler(); 53 54 public static final byte PRINT_ELEMENT = 0; 55 public static final byte PRINT_PARENT = 1; 56 public static final byte PRINT_ALL = 2; 57 58 private static final DocumentBuilderFactory FACTORY = DocumentBuilderFactory.newInstance(); 59 static 60 { 61 FACTORY.setNamespaceAware(true); 62 FACTORY.setValidating(true); 63 } 64 65 private static final String INDENT = " "; 66 67 public static void main(String [] args) throws Exception 68 { 69 String xml1 = 70 "<ns1:e xmlns:ns1='http://ns' attr1='attr1_val' ns1:attr2='attr2_val'>\n" + 71 " <ns1:child1>\n" + 72 " <ns2:child2 xmlns:ns2='http://ns2' child2_attr='child2_attr_val'>child2_val</ns2:child2>\n" + 73 " </ns1:child1>\n" + 74 " text\n" + 75 "</ns1:e>"; 76 77 String xml2 = 78 "<e xmlns='http://ns' attr1='attr1_val'" + 79 " xmlns:ns='http://ns' ns:attr2='attr2_val'>text" + 80 " <child1>" + 81 " <child2 xmlns='http://ns2' child2_attr='child2_attr_val'>child2_val</child2>" + 82 " </child1>" + 83 "</e>"; 84 85 System.out.println(new XmlDiff().diff(xml1, xml2)); 86 } 87 88 public XmlDiff() 89 { 90 this(PRINT_ALL, true); 91 } 92 93 public XmlDiff(byte print, boolean reformat) 94 { 95 this.print = print; 96 this.reformat = reformat; 97 } 98 99 private byte print = PRINT_ALL; 100 private boolean reformat = true; 101 102 public byte getPrint() 103 { 104 return print; 105 } 106 107 public void setPrint(byte print) 108 { 109 this.print = print; 110 } 111 112 public boolean isReformat() 113 { 114 return reformat; 115 } 116 117 public void setReformat(boolean reformat) 118 { 119 this.reformat = reformat; 120 } 121 122 129 public String diff(String expected, String was) 130 { 131 return diff(expected, was, ERROR_HANDLER, null); 132 } 133 134 public String diff(String expected, String was, ErrorHandler eh) 135 { 136 return diff(expected, was, eh, null); 137 } 138 139 public String diff(String expected, String was, EntityResolver er) 140 { 141 return diff(expected, was, ERROR_HANDLER, er); 142 } 143 144 public String diff(String expected, String was, ErrorHandler eh, EntityResolver er) 145 { 146 DocumentBuilder documentBuilder = null; 147 try 148 { 149 documentBuilder = FACTORY.newDocumentBuilder(); 150 } 151 catch(ParserConfigurationException e) 152 { 153 throw new IllegalStateException ("Failed to create a document builder: " + e.getMessage()); 154 } 155 156 if(eh != null) 157 { 158 documentBuilder.setErrorHandler(eh); 159 } 160 161 if(er != null) 162 { 163 documentBuilder.setEntityResolver(er); 164 } 165 166 Document expDoc = null; 167 try 168 { 169 expDoc = documentBuilder.parse(new InputSource (new StringReader (expected))); 170 } 171 catch(Exception e) 172 { 173 throw new IllegalStateException ("Failed to parse expected XML\n" + expected + ": " + e.getMessage()); 174 } 175 176 Document wasDoc = null; 177 try 178 { 179 wasDoc = documentBuilder.parse(new InputSource (new StringReader (was))); 180 } 181 catch(Exception e) 182 { 183 throw new IllegalStateException ("Failed to parse XML\n" + was + ": " + e.getMessage()); 184 } 185 186 Element expElement = expDoc.getDocumentElement(); 187 Element wasElement = wasDoc.getDocumentElement(); 188 return assertEquals(expElement, wasElement, expElement, wasElement); 189 } 190 191 private String assertEquals(Element exp, Element was, Element printAsExp, Element printAsWas) 192 { 193 QName expName = new QName (exp.getNamespaceURI(), exp.getLocalName()); 194 QName wasName = new QName (was.getNamespaceURI(), was.getLocalName()); 195 196 if(!expName.equals(wasName)) 197 { 198 return fail("Expected name " + expName + " but was " + wasName, exp, was); 199 } 200 201 NamedNodeMap expAttrs = exp.getAttributes(); 202 NamedNodeMap wasAttrs = was.getAttributes(); 203 204 if(expAttrs == null && wasAttrs != null && hasNonIgnorableNs(wasAttrs)) 205 { 206 return fail("Element " + expName + " doesn't have attributes", printAsExp, printAsWas); 207 } 208 else if(wasAttrs == null && expAttrs != null && hasNonIgnorableNs(expAttrs)) 209 { 210 return fail("Element " + expName + " has attributes", printAsExp, printAsWas); 211 } 212 else if(expAttrs != null && wasAttrs != null) 213 { 214 String msg = assertAttrs(expAttrs, wasAttrs, printAsExp); 215 if(msg != null) 216 { 217 return fail(msg, printAsExp, printAsWas); 218 } 219 } 220 221 NodeList expChildren = exp.getChildNodes(); 222 NodeList wasChildren = was.getChildNodes(); 223 224 NodeList expTexts = getTextNodes(expChildren); 225 NodeList wasTexts = getTextNodes(wasChildren); 226 if(expTexts.getLength() > 0 && wasTexts.getLength() == 0) 227 { 228 return fail("Element " + expName + " has text content", printAsExp, printAsWas); 229 } 230 else if(expTexts.getLength() == 0 && wasTexts.getLength() > 0) 231 { 232 return fail("Element " + expName + " doesn't have text content", printAsExp, printAsWas); 233 } 234 else if(expTexts.getLength() != wasTexts.getLength()) 236 { 237 return fail( 238 "Element " + expName + " has " + expTexts.getLength() + " text nodes (was " + wasTexts.getLength() + ")", 239 printAsExp, 240 printAsWas 241 ); 242 } 243 else if(expTexts.getLength() > 0 && wasTexts.getLength() > 0) 244 { 245 for(int i = 0; i < expTexts.getLength(); ++i) 246 { 247 Text text = (Text )expTexts.item(i); 248 if(!containsText(text.getNodeValue(), wasTexts, i)) 249 { 250 return fail("Element " + expName + " has text '" + text.getNodeValue() + "'", printAsExp, printAsWas); 251 } 252 } 253 } 254 255 NodeList expElems = sublist(expChildren, Node.ELEMENT_NODE); 256 NodeList wasElems = sublist(wasChildren, Node.ELEMENT_NODE); 257 if(expElems.getLength() > 0 && wasElems.getLength() == 0) 258 { 259 return fail("Element " + expName + " has child elements", printAsExp, printAsWas); 260 } 261 else if(expElems.getLength() == 0 && wasElems.getLength() > 0) 262 { 263 return fail("Element " + expName + " doesn't have child elements", printAsExp, printAsWas); 264 } 265 else if(expElems.getLength() != wasElems.getLength()) 266 { 267 return fail("Element " + 268 expName + 269 " has " + 270 expElems.getLength() + 271 " child elements (was " + 272 wasElems.getLength() + 273 ")", 274 printAsExp, 275 printAsWas 276 ); 277 } 278 else if(expElems.getLength() > 0 && wasElems.getLength() > 0) 279 { 280 if(print == PRINT_PARENT) 281 { 282 printAsExp = exp; 283 printAsWas = was; 284 } 285 286 for(int i = 0; i < expElems.getLength(); ++i) 287 { 288 Element expChild = (Element )expElems.item(i); 289 Element wasChild = getElement(expChild.getNamespaceURI(), expChild.getLocalName(), wasElems, i); 290 if(wasChild == null) 291 { 292 return fail("Element " + 293 expName + 294 " has child element " + 295 new QName (expChild.getNamespaceURI(), expChild.getLocalName()), 296 printAsExp, 297 printAsWas 298 ); 299 } 300 301 if(print == PRINT_ELEMENT) 302 { 303 printAsExp = expChild; 304 printAsWas = wasChild; 305 } 306 307 String diff = assertEquals(expChild, wasChild, printAsExp, printAsWas); 308 if(diff != null) 309 { 310 return diff; 311 } 312 } 313 } 314 return null; 315 } 316 317 private static Element getElement(String ns, String local, NodeList elements, int suggestedIndex) 318 { 319 if(suggestedIndex >= 0 && suggestedIndex < elements.getLength()) 320 { 321 Element element = (Element )elements.item(suggestedIndex); 322 if((ns == null && element.getNamespaceURI() == null || 323 ns != null && ns.equals(element.getNamespaceURI()) 324 ) && 325 local.equals(element.getLocalName())) 326 { 327 return element; 328 } 329 } 330 331 for(int i = 0; i < elements.getLength(); ++i) 332 { 333 Element element = (Element )elements.item(i); 334 if((ns == null && element.getNamespaceURI() == null || 335 ns != null && ns.equals(element.getNamespaceURI()) 336 ) && 337 local.equals(element.getLocalName())) 338 { 339 return element; 340 } 341 } 342 return null; 343 } 344 345 private static boolean containsText(String text, NodeList textNodes, int suggestedIndex) 346 { 347 text = text.trim(); 348 if(suggestedIndex >= 0) 349 { 350 Text textNode = (Text )textNodes.item(suggestedIndex); 351 String wasText = textNode.getNodeValue().trim(); 352 if(text.equals(wasText)) 353 { 354 return true; 355 } 356 } 357 358 for(int i = 0; i < textNodes.getLength(); ++i) 359 { 360 Text textNode = (Text )textNodes.item(i); 361 String wasText = textNode.getNodeValue().trim(); 362 if(text.equals(wasText)) 363 { 364 return true; 365 } 366 } 367 return false; 368 } 369 370 private static NodeList getTextNodes(NodeList list) 371 { 372 MutableNodeList result = new MutableNodeList(); 373 for(int i = 0; i < list.getLength(); ++i) 374 { 375 Node node = list.item(i); 376 if(node.getNodeType() == Node.TEXT_NODE) 377 { 378 String text = node.getNodeValue(); 379 if(text.trim().length() > 0) 380 { 381 result.add(node); 382 } 383 } 384 } 385 return result; 386 } 387 388 private static NodeList sublist(NodeList list, short nodeType) 389 { 390 MutableNodeList result = new MutableNodeList(); 391 for(int i = 0; i < list.getLength(); ++i) 392 { 393 Node node = list.item(i); 394 if(node.getNodeType() == nodeType) 395 { 396 result.add(node); 397 } 398 } 399 return result; 400 } 401 402 private static String assertAttrs(NamedNodeMap attrsExp, 403 NamedNodeMap attrsWas, 404 Element printAsExp) 405 { 406 String result = assertSubset(attrsExp, attrsWas, printAsExp, true); 407 if(result == null) 408 { 409 result = assertSubset(attrsWas, attrsExp, printAsExp, false); 410 } 411 return result; 412 } 413 414 private static String assertSubset(NamedNodeMap attrsSubset, 415 NamedNodeMap attrsSet, 416 Element printAsExp, 417 boolean checkHave) 418 { 419 String msg = checkHave ? " has attribute " : " doesn't have attribute "; 420 QName expName = new QName (printAsExp.getNamespaceURI(), printAsExp.getLocalName()); 421 for(int i = 0; i < attrsSubset.getLength(); ++i) 422 { 423 Attr attr = (Attr )attrsSubset.item(i); 424 String attrNs = attr.getNamespaceURI(); 425 String localName = attr.getLocalName(); 426 if(xsiNs(attrNs) && "type".equals(localName)) 427 { 428 Attr wasAttr = (Attr )attrsSet.getNamedItemNS(attrNs, localName); 429 if(wasAttr == null) 430 { 431 return "Element " + expName + msg + new QName (attrNs, localName); 432 } 433 434 String typeName = attr.getValue(); 435 int colon = typeName.indexOf(':'); 436 if(colon != -1) 437 { 438 typeName = typeName.substring(colon); 439 } 440 441 if(!wasAttr.getValue().endsWith(typeName)) 442 { 443 return "Element " + expName + 444 (checkHave ? " has xsi:type " : " doesn't have xsi:type ") + 445 attr.getValue(); 446 } 447 448 } 450 else if(nonIgnorableNs(attrNs) || xsiNs(attrNs) && localName.equals("nil")) 451 { 452 Attr wasAttr = (Attr )attrsSet.getNamedItemNS(attrNs, localName); 453 if(wasAttr == null) 454 { 455 return "Element " + expName + msg + new QName (attrNs, localName); 456 } 457 458 if(!attr.getValue().equals(wasAttr.getValue())) 459 { 460 return "Attribute " + 461 new QName (attrNs, localName) + 462 " in element " + 463 expName + 464 " has value " + attr.getValue(); 465 } 466 } 467 } 468 return null; 469 } 470 471 private static boolean hasNonIgnorableNs(NamedNodeMap nodeMap) 472 { 473 for(int i = 0; i < nodeMap.getLength(); ++i) 474 { 475 Node node = nodeMap.item(i); 476 if(nonIgnorableNs(node.getNamespaceURI())) 477 { 478 return true; 479 } 480 } 481 return false; 482 } 483 484 private static boolean nonIgnorableNs(String ns) 485 { 486 return ns == null || 487 !ns.equals(Constants.NS_XML_SCHEMA) 488 && !ns.equals(Constants.NS_XML_SCHEMA_INSTANCE) 489 && !ns.equals(Constants.NS_XML_XMLNS); 490 } 491 492 private static boolean xsiNs(String ns) 493 { 494 return Constants.NS_XML_SCHEMA_INSTANCE.equals(ns); 495 } 496 497 private String fail(String msg, Element exp, Element was) 498 { 499 return msg + ". Expected\n" + toString(exp) + "\nbut was\n" + toString(was); 500 } 501 502 private String toString(Element e) 503 { 504 return append(e, new StringBuffer (), 0).toString(); 505 } 506 507 private StringBuffer append(Element e, StringBuffer buf, int depth) 508 { 509 if(reformat && depth > 0) 510 { 511 buf.append('\n'); 512 for(int i = 0; i < depth; ++i) 513 { 514 buf.append(INDENT); 515 } 516 } 517 518 buf.append('<'); 519 if(e.getPrefix() != null && e.getPrefix().length() > 0) 520 { 521 buf.append(e.getPrefix()).append(':'); 522 } 523 buf.append(e.getLocalName()); 524 525 NamedNodeMap attrs = e.getAttributes(); 526 if(attrs != null && attrs.getLength() > 0) 527 { 528 for(int i = 0; i < attrs.getLength(); ++i) 529 { 530 Attr attr = (Attr )attrs.item(i); 531 buf.append(' ') 532 .append(attr.getName()) 533 .append('=') 534 .append('\'') 535 .append(attr.getValue()) 536 .append('\''); 537 } 538 } 539 540 buf.append('>'); 541 542 NodeList childNodes = e.getChildNodes(); 543 boolean childElements = false; 544 for(int i = 0; i < childNodes.getLength(); ++i) 545 { 546 Node child = childNodes.item(i); 547 switch(child.getNodeType()) 548 { 549 case Node.TEXT_NODE: 550 String chars = child.getNodeValue(); 551 if(chars.trim().length() > 0) 552 { 553 buf.append(chars); 554 } 555 break; 556 case Node.ELEMENT_NODE: 557 append((Element )child, buf, depth + 1); 558 childElements = true; 559 break; 560 default: 561 throw new IllegalStateException ("Node type is not supported: " + child.getNodeType()); 562 } 563 } 564 565 if(reformat && childElements) 566 { 567 buf.append('\n'); 568 for(int i = 0; i < depth; ++i) 569 { 570 buf.append(INDENT); 571 } 572 } 573 574 buf.append("</"); 575 if(e.getPrefix() != null && e.getPrefix().length() > 0) 576 { 577 buf.append(e.getPrefix()).append(':'); 578 } 579 buf.append(e.getLocalName()) 580 .append('>'); 581 582 return buf; 583 } 584 585 587 private static final class MutableNodeList 588 implements NodeList 589 { 590 private List list = Collections.EMPTY_LIST; 591 592 public void add(Node node) 593 { 594 switch(list.size()) 595 { 596 case 0: 597 list = Collections.singletonList(node); 598 break; 599 case 1: 600 list = new ArrayList (list); 601 default: 602 list.add(node); 603 } 604 } 605 606 public int getLength() 607 { 608 return list.size(); 609 } 610 611 public Node item(int index) 612 { 613 return (Node )list.get(index); 614 } 615 } 616 617 public static final class DefErrorHandler 618 implements ErrorHandler 619 { 620 public static final byte IGNORE = 0; 621 public static final byte LOG = 1; 622 public static final byte FAIL = 3; 623 624 private byte warnEvent = IGNORE; 625 private byte errorEvent = IGNORE; 626 private byte fatalEvent = FAIL; 627 628 public void error(SAXParseException e) throws SAXException 629 { 630 handleEvent(warnEvent, e); 631 } 632 633 public void fatalError(SAXParseException e) throws SAXException 634 { 635 handleEvent(errorEvent, e); 636 } 637 638 public void warning(SAXParseException e) throws SAXException 639 { 640 handleEvent(fatalEvent, e); 641 } 642 643 private void handleEvent(byte event, SAXParseException e) 644 throws SAXException 645 { 646 switch(event) 647 { 648 case IGNORE: 649 break; 650 case LOG: 651 System.out.println(formatMessage(e)); 652 break; 653 case FAIL: 654 String msg = formatMessage(e); 655 throw new SAXException (msg); 656 } 657 } 658 } 659 660 private static String formatMessage(SAXParseException exception) 661 { 662 StringBuffer buffer = new StringBuffer (50); 663 buffer.append(exception.getMessage()).append(" @ "); 664 String location = exception.getPublicId(); 665 if(location != null) 666 { 667 buffer.append(location); 668 } 669 else 670 { 671 location = exception.getSystemId(); 672 if(location != null) 673 { 674 buffer.append(location); 675 } 676 else 677 { 678 buffer.append("*unknown*"); 679 } 680 } 681 buffer.append('['); 682 buffer.append(exception.getLineNumber()).append(','); 683 buffer.append(exception.getColumnNumber()).append(']'); 684 return buffer.toString(); 685 } 686 } 687 | Popular Tags |