1 /******************************************************************************* 2 * Copyright (c) 2000, 2018 IBM Corporation and others. 3 * 4 * This program and the accompanying materials 5 * are made available under the terms of the Eclipse Public License 2.0 6 * which accompanies this distribution, and is available at 7 * https://www.eclipse.org/legal/epl-2.0/ 8 * 9 * SPDX-License-Identifier: EPL-2.0 10 * 11 * Contributors: 12 * IBM Corporation - initial API and implementation 13 *******************************************************************************/ 14 package org.eclipse.ui; 15 16 import java.io.IOException; 17 import java.io.PrintWriter; 18 import java.io.Reader; 19 import java.io.StringWriter; 20 import java.io.Writer; 21 import java.util.ArrayList; 22 import javax.xml.parsers.DocumentBuilder; 23 import javax.xml.parsers.DocumentBuilderFactory; 24 import javax.xml.parsers.ParserConfigurationException; 25 import org.eclipse.ui.internal.WorkbenchMessages; 26 import org.eclipse.ui.internal.WorkbenchPlugin; 27 import org.w3c.dom.Attr; 28 import org.w3c.dom.DOMException; 29 import org.w3c.dom.Document; 30 import org.w3c.dom.Element; 31 import org.w3c.dom.NamedNodeMap; 32 import org.w3c.dom.Node; 33 import org.w3c.dom.NodeList; 34 import org.w3c.dom.Text; 35 import org.xml.sax.ErrorHandler; 36 import org.xml.sax.InputSource; 37 import org.xml.sax.SAXException; 38 import org.xml.sax.SAXParseException; 39 40 /** 41 * This class represents the default implementation of the <code>IMemento</code> 42 * interface. 43 * <p> 44 * This class is not intended to be extended by clients. 45 * </p> 46 * 47 * @see IMemento 48 */ 49 public final class XMLMemento implements IMemento { 50 private Document factory; 51 52 private Element element; 53 54 /** 55 * Creates a <code>Document</code> from the <code>Reader</code> and returns a 56 * memento on the first <code>Element</code> for reading the document. 57 * <p> 58 * Same as calling createReadRoot(reader, null) 59 * </p> 60 * 61 * @param reader the <code>Reader</code> used to create the memento's document 62 * @return a memento on the first <code>Element</code> for reading the document 63 * @throws WorkbenchException if IO problems, invalid format, or no element. 64 */ createReadRoot(Reader reader)65 public static XMLMemento createReadRoot(Reader reader) throws WorkbenchException { 66 return createReadRoot(reader, null); 67 } 68 69 /** 70 * Creates a <code>Document</code> from the <code>Reader</code> and returns a 71 * memento on the first <code>Element</code> for reading the document. 72 * 73 * @param reader the <code>Reader</code> used to create the memento's document 74 * @param baseDir the directory used to resolve relative file names in the XML 75 * document. This directory must exist and include the trailing 76 * separator. The directory format, including the separators, 77 * must be valid for the platform. Can be <code>null</code> if 78 * not needed. 79 * @return a memento on the first <code>Element</code> for reading the document 80 * @throws WorkbenchException if IO problems, invalid format, or no element. 81 */ createReadRoot(Reader reader, String baseDir)82 public static XMLMemento createReadRoot(Reader reader, String baseDir) throws WorkbenchException { 83 String errorMessage = null; 84 Exception exception = null; 85 86 try { 87 DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance(); 88 DocumentBuilder parser = factory.newDocumentBuilder(); 89 InputSource source = new InputSource(reader); 90 if (baseDir != null) { 91 source.setSystemId(baseDir); 92 } 93 94 parser.setErrorHandler(new ErrorHandler() { 95 @Override 96 public void warning(SAXParseException exception) { 97 // ignore 98 } 99 100 @Override 101 public void error(SAXParseException exception) { 102 // ignore 103 } 104 105 @Override 106 public void fatalError(SAXParseException exception) throws SAXException { 107 throw exception; 108 } 109 }); 110 111 Document document = parser.parse(source); 112 NodeList list = document.getChildNodes(); 113 for (int i = 0; i < list.getLength(); i++) { 114 Node node = list.item(i); 115 if (node instanceof Element) { 116 return new XMLMemento(document, (Element) node); 117 } 118 } 119 } catch (ParserConfigurationException e) { 120 exception = e; 121 errorMessage = WorkbenchMessages.XMLMemento_parserConfigError; 122 } catch (IOException e) { 123 exception = e; 124 errorMessage = WorkbenchMessages.XMLMemento_ioError; 125 } catch (SAXException e) { 126 exception = e; 127 errorMessage = WorkbenchMessages.XMLMemento_formatError; 128 } 129 130 String problemText = null; 131 if (exception != null) { 132 problemText = exception.getMessage(); 133 } 134 if (problemText == null || problemText.length() == 0) { 135 problemText = errorMessage != null ? errorMessage : WorkbenchMessages.XMLMemento_noElement; 136 } 137 throw new WorkbenchException(problemText, exception); 138 } 139 140 /** 141 * Returns a root memento for writing a document. 142 * 143 * @param type the element node type to create on the document 144 * @return the root memento for writing a document 145 * @throws DOMException when the element could not be created for the passed 146 * type 147 */ createWriteRoot(String type)148 public static XMLMemento createWriteRoot(String type) throws DOMException { 149 Document document; 150 try { 151 document = DocumentBuilderFactory.newInstance().newDocumentBuilder().newDocument(); 152 Element element = document.createElement(type); 153 document.appendChild(element); 154 return new XMLMemento(document, element); 155 } catch (ParserConfigurationException e) { 156 // throw new Error(e); 157 throw new Error(e.getMessage()); 158 } 159 } 160 161 /** 162 * Creates a memento for the specified document and element. 163 * <p> 164 * Clients should use <code>createReadRoot</code> and 165 * <code>createWriteRoot</code> to create the initial memento on a document. 166 * </p> 167 * 168 * @param document the document for the memento 169 * @param element the element node for the memento 170 */ XMLMemento(Document document, Element element)171 public XMLMemento(Document document, Element element) { 172 super(); 173 this.factory = document; 174 this.element = element; 175 } 176 177 /** 178 * Creates a new child of this memento with the given type. 179 * <p> 180 * The <code>getChild</code> and <code>getChildren</code> methods are used to 181 * retrieve children of a given type. 182 * </p> 183 * 184 * @param type the type 185 * @return a new child memento 186 * @see #getChild 187 * @see #getChildren 188 * @throws DOMException if the child cannot be created 189 */ 190 @Override createChild(String type)191 public IMemento createChild(String type) throws DOMException { 192 Element child = factory.createElement(type); 193 element.appendChild(child); 194 return new XMLMemento(factory, child); 195 } 196 197 /** 198 * Creates a new child of this memento with the given type and id. The id is 199 * stored in the child memento (using a special reserved key, 200 * <code>TAG_ID</code>) and can be retrieved using <code>getId</code>. 201 * <p> 202 * The <code>getChild</code> and <code>getChildren</code> methods are used to 203 * retrieve children of a given type. 204 * </p> 205 * 206 * @param type the type 207 * @param id the child id 208 * @return a new child memento with the given type and id 209 * @see #getID 210 * @throws DOMException if the child cannot be created 211 */ 212 @Override createChild(String type, String id)213 public IMemento createChild(String type, String id) throws DOMException { 214 Element child = factory.createElement(type); 215 child.setAttribute(TAG_ID, id == null ? "" : id); //$NON-NLS-1$ 216 element.appendChild(child); 217 return new XMLMemento(factory, child); 218 } 219 220 /** 221 * Create a copy of the child node and append it to this node. 222 * 223 * @param child the child to copy 224 * @return An IMenento for the new child node. 225 * @throws DOMException if the child cannot be created 226 */ copyChild(IMemento child)227 public IMemento copyChild(IMemento child) throws DOMException { 228 Element childElement = ((XMLMemento) child).element; 229 Element newElement = (Element) factory.importNode(childElement, true); 230 element.appendChild(newElement); 231 return new XMLMemento(factory, newElement); 232 } 233 234 @Override getChild(String type)235 public IMemento getChild(String type) { 236 237 // Get the nodes. 238 NodeList nodes = element.getChildNodes(); 239 int size = nodes.getLength(); 240 if (size == 0) { 241 return null; 242 } 243 244 // Find the first node which is a child of this node. 245 for (int nX = 0; nX < size; nX++) { 246 Node node = nodes.item(nX); 247 if (node instanceof Element) { 248 Element element = (Element) node; 249 if (element.getNodeName().equals(type)) { 250 return new XMLMemento(factory, element); 251 } 252 } 253 } 254 255 // A child was not found. 256 return null; 257 } 258 259 @Override getChildren()260 public IMemento[] getChildren() { 261 262 // Get the nodes. 263 final NodeList nodes = element.getChildNodes(); 264 int size = nodes.getLength(); 265 if (size == 0) { 266 return new IMemento[0]; 267 } 268 269 // Extract each node with given type. 270 ArrayList<Element> list = new ArrayList<>(size); 271 for (int nX = 0; nX < size; nX++) { 272 final Node node = nodes.item(nX); 273 if (node instanceof Element) 274 list.add((Element) node); 275 } 276 277 // Create a memento for each node. 278 size = list.size(); 279 IMemento[] results = new IMemento[size]; 280 for (int x = 0; x < size; x++) { 281 results[x] = new XMLMemento(factory, list.get(x)); 282 } 283 return results; 284 } 285 286 @Override getChildren(String type)287 public IMemento[] getChildren(String type) { 288 289 // Get the nodes. 290 NodeList nodes = element.getChildNodes(); 291 int size = nodes.getLength(); 292 if (size == 0) { 293 return new IMemento[0]; 294 } 295 296 // Extract each node with given type. 297 ArrayList<Element> list = new ArrayList<>(size); 298 for (int nX = 0; nX < size; nX++) { 299 Node node = nodes.item(nX); 300 if (node instanceof Element) { 301 Element element = (Element) node; 302 if (element.getNodeName().equals(type)) { 303 list.add(element); 304 } 305 } 306 } 307 308 // Create a memento for each node. 309 size = list.size(); 310 IMemento[] results = new IMemento[size]; 311 for (int x = 0; x < size; x++) { 312 results[x] = new XMLMemento(factory, list.get(x)); 313 } 314 return results; 315 } 316 317 @Override getFloat(String key)318 public Float getFloat(String key) { 319 Attr attr = element.getAttributeNode(key); 320 if (attr == null) { 321 return null; 322 } 323 String strValue = attr.getValue(); 324 try { 325 return Float.valueOf(strValue); 326 } catch (NumberFormatException e) { 327 WorkbenchPlugin.log("Memento problem - Invalid float for key: " //$NON-NLS-1$ 328 + key + " value: " + strValue, e); //$NON-NLS-1$ 329 return null; 330 } 331 } 332 333 /** 334 * @since 3.4 335 */ 336 @Override getType()337 public String getType() { 338 return element.getNodeName(); 339 } 340 341 @Override getID()342 public String getID() { 343 return element.getAttribute(TAG_ID); 344 } 345 346 @Override getInteger(String key)347 public Integer getInteger(String key) { 348 Attr attr = element.getAttributeNode(key); 349 if (attr == null) { 350 return null; 351 } 352 String strValue = attr.getValue(); 353 try { 354 return Integer.valueOf(strValue); 355 } catch (NumberFormatException e) { 356 WorkbenchPlugin.log("Memento problem - invalid integer for key: " + key //$NON-NLS-1$ 357 + " value: " + strValue, e); //$NON-NLS-1$ 358 return null; 359 } 360 } 361 362 @Override getString(String key)363 public String getString(String key) { 364 Attr attr = element.getAttributeNode(key); 365 if (attr == null) { 366 return null; 367 } 368 return attr.getValue(); 369 } 370 371 /** 372 * @since 3.4 373 */ 374 @Override getBoolean(String key)375 public Boolean getBoolean(String key) { 376 Attr attr = element.getAttributeNode(key); 377 if (attr == null) { 378 return null; 379 } 380 return Boolean.valueOf(attr.getValue()); 381 } 382 383 /** 384 * Returns the data of the Text node of the memento. Each memento is allowed 385 * only one Text node. 386 * 387 * @return the data of the Text node of the memento, or <code>null</code> if the 388 * memento has no Text node. 389 * @since 2.0 390 * @throws DOMException if the text node is too big 391 */ 392 @Override getTextData()393 public String getTextData() throws DOMException { 394 Text textNode = getTextNode(); 395 if (textNode != null) { 396 return textNode.getData(); 397 } 398 return null; 399 } 400 401 /** 402 * @since 3.4 403 */ 404 @Override getAttributeKeys()405 public String[] getAttributeKeys() { 406 NamedNodeMap map = element.getAttributes(); 407 int size = map.getLength(); 408 String[] attributes = new String[size]; 409 for (int i = 0; i < size; i++) { 410 Node node = map.item(i); 411 attributes[i] = node.getNodeName(); 412 } 413 return attributes; 414 } 415 416 /** 417 * Returns the Text node of the memento. Each memento is allowed only one Text 418 * node. 419 * 420 * @return the Text node of the memento, or <code>null</code> if the memento has 421 * no Text node. 422 */ getTextNode()423 private Text getTextNode() { 424 // Get the nodes. 425 NodeList nodes = element.getChildNodes(); 426 int size = nodes.getLength(); 427 if (size == 0) { 428 return null; 429 } 430 for (int nX = 0; nX < size; nX++) { 431 Node node = nodes.item(nX); 432 if (node instanceof Text) { 433 return (Text) node; 434 } 435 } 436 // a Text node was not found 437 return null; 438 } 439 440 /** 441 * Places the element's attributes into the document. 442 * 443 * @param copyText true if the first text node should be copied 444 * @throws DOMException if the attributes or children cannot be copied to this 445 * node. 446 */ putElement(Element element, boolean copyText)447 private void putElement(Element element, boolean copyText) throws DOMException { 448 NamedNodeMap nodeMap = element.getAttributes(); 449 int size = nodeMap.getLength(); 450 for (int i = 0; i < size; i++) { 451 Attr attr = (Attr) nodeMap.item(i); 452 putString(attr.getName(), attr.getValue()); 453 } 454 455 NodeList nodes = element.getChildNodes(); 456 size = nodes.getLength(); 457 // Copy first text node (fixes bug 113659). 458 // Note that text data will be added as the first child (see putTextData) 459 boolean needToCopyText = copyText; 460 for (int i = 0; i < size; i++) { 461 Node node = nodes.item(i); 462 if (node instanceof Element) { 463 XMLMemento child = (XMLMemento) createChild(node.getNodeName()); 464 child.putElement((Element) node, true); 465 } else if (node instanceof Text && needToCopyText) { 466 putTextData(((Text) node).getData()); 467 needToCopyText = false; 468 } 469 } 470 } 471 472 /** 473 * Sets the value of the given key to the given floating point number. 474 * 475 * @param key the key 476 * @param f the value 477 * @throws DOMException if the attribute cannot be set 478 */ 479 @Override putFloat(String key, float f)480 public void putFloat(String key, float f) throws DOMException { 481 element.setAttribute(key, String.valueOf(f)); 482 } 483 484 /** 485 * Sets the value of the given key to the given integer. 486 * 487 * @param key the key 488 * @param n the value 489 * @throws DOMException if the attribute cannot be set 490 */ 491 @Override putInteger(String key, int n)492 public void putInteger(String key, int n) throws DOMException { 493 element.setAttribute(key, String.valueOf(n)); 494 } 495 496 /** 497 * Copy the attributes and children from <code>memento</code> to the receiver. 498 * 499 * @param memento the IMemento to be copied. 500 * @throws DOMException if the attributes or children cannot be copied to this 501 * node. 502 */ 503 @Override putMemento(IMemento memento)504 public void putMemento(IMemento memento) throws DOMException { 505 // Do not copy the element's top level text node (this would overwrite the 506 // existing text). 507 // Text nodes of children are copied. 508 putElement(((XMLMemento) memento).element, false); 509 } 510 511 /** 512 * Sets the value of the given key to the given string. 513 * 514 * @param key the key 515 * @param value the value 516 * @throws DOMException if the attribute cannot be set 517 */ 518 @Override putString(String key, String value)519 public void putString(String key, String value) throws DOMException { 520 if (value == null) { 521 return; 522 } 523 element.setAttribute(key, value); 524 } 525 526 /** 527 * Sets the value of the given key to the given boolean value. 528 * 529 * @param key the key 530 * @param value the value 531 * @since 3.4 532 * @throws DOMException if the attribute cannot be set 533 */ 534 @Override putBoolean(String key, boolean value)535 public void putBoolean(String key, boolean value) throws DOMException { 536 element.setAttribute(key, value ? "true" : "false"); //$NON-NLS-1$ //$NON-NLS-2$ 537 } 538 539 /** 540 * Sets the memento's Text node to contain the given data. Creates the Text node 541 * if none exists. If a Text node does exist, it's current contents are 542 * replaced. Each memento is allowed only one text node. 543 * 544 * @param data the data to be placed on the Text node 545 * @since 2.0 546 * @throws DOMException if the text node cannot be created under this node. 547 */ 548 @Override putTextData(String data)549 public void putTextData(String data) throws DOMException { 550 Text textNode = getTextNode(); 551 if (textNode == null) { 552 textNode = factory.createTextNode(data); 553 // Always add the text node as the first child (fixes bug 93718) 554 element.insertBefore(textNode, element.getFirstChild()); 555 } else { 556 textNode.setData(data); 557 } 558 } 559 560 /** 561 * Saves this memento's document current values to the specified writer. 562 * 563 * @param writer the writer used to save the memento's document 564 * @throws IOException if there is a problem serializing the document to the 565 * stream. 566 */ save(Writer writer)567 public void save(Writer writer) throws IOException { 568 try (DOMWriter out = new DOMWriter(writer)) { 569 out.print(element); 570 } 571 } 572 573 @Override toString()574 public String toString() { 575 try { 576 StringWriter writer = new StringWriter(); 577 save(writer); 578 return writer.toString(); 579 } catch (IOException e) { 580 return super.toString(); 581 } 582 } 583 584 /** 585 * A simple XML writer. Using this instead of the javax.xml.transform classes 586 * allows compilation against JCL Foundation (bug 80053). 587 */ 588 private static final class DOMWriter extends PrintWriter { 589 590 /* constants */ 591 private static final String XML_VERSION = "<?xml version=\"1.0\" encoding=\"UTF-8\"?>"; //$NON-NLS-1$ 592 593 /** 594 * Creates a new DOM writer on the given output writer. 595 * 596 * @param output the output writer 597 */ DOMWriter(Writer output)598 public DOMWriter(Writer output) { 599 super(output); 600 println(XML_VERSION); 601 } 602 603 /** 604 * Prints the given element. 605 * 606 * @param element the element to print 607 */ print(Element element)608 public void print(Element element) { 609 // Ensure extra whitespace is not emitted next to a Text node, 610 // as that will result in a situation where the restored text data is not the 611 // same as the saved text data. 612 boolean hasChildren = element.hasChildNodes(); 613 startTag(element, hasChildren); 614 if (hasChildren) { 615 boolean prevWasText = false; 616 NodeList children = element.getChildNodes(); 617 for (int i = 0; i < children.getLength(); i++) { 618 Node node = children.item(i); 619 if (node instanceof Element) { 620 if (!prevWasText) { 621 println(); 622 } 623 print((Element) children.item(i)); 624 prevWasText = false; 625 } else if (node instanceof Text) { 626 print(getEscaped(node.getNodeValue())); 627 prevWasText = true; 628 } 629 } 630 if (!prevWasText) { 631 println(); 632 } 633 endTag(element); 634 } 635 } 636 startTag(Element element, boolean hasChildren)637 private void startTag(Element element, boolean hasChildren) { 638 StringBuilder sb = new StringBuilder(); 639 sb.append("<"); //$NON-NLS-1$ 640 sb.append(element.getTagName()); 641 NamedNodeMap attributes = element.getAttributes(); 642 for (int i = 0; i < attributes.getLength(); i++) { 643 Attr attribute = (Attr) attributes.item(i); 644 sb.append(" "); //$NON-NLS-1$ 645 sb.append(attribute.getName()); 646 sb.append("=\""); //$NON-NLS-1$ 647 sb.append(getEscaped(String.valueOf(attribute.getValue()))); 648 sb.append("\""); //$NON-NLS-1$ 649 } 650 sb.append(hasChildren ? ">" : "/>"); //$NON-NLS-1$ //$NON-NLS-2$ 651 print(sb.toString()); 652 } 653 endTag(Element element)654 private void endTag(Element element) { 655 StringBuilder sb = new StringBuilder(); 656 sb.append("</"); //$NON-NLS-1$ 657 sb.append(element.getNodeName()); 658 sb.append(">"); //$NON-NLS-1$ 659 print(sb.toString()); 660 } 661 appendEscapedChar(StringBuilder buffer, char c)662 private static void appendEscapedChar(StringBuilder buffer, char c) { 663 String replacement = getReplacement(c); 664 if (replacement != null) { 665 buffer.append('&'); 666 buffer.append(replacement); 667 buffer.append(';'); 668 } else if (c == 9 || c == 10 || c == 13 || c >= 32) { 669 buffer.append(c); 670 } 671 } 672 getEscaped(String s)673 private static String getEscaped(String s) { 674 StringBuilder result = new StringBuilder(s.length() + 10); 675 for (int i = 0; i < s.length(); ++i) { 676 appendEscapedChar(result, s.charAt(i)); 677 } 678 return result.toString(); 679 } 680 getReplacement(char c)681 private static String getReplacement(char c) { 682 // Encode special XML characters into the equivalent character references. 683 // The first five are defined by default for all XML documents. 684 // The next three (#xD, #xA, #x9) are encoded to avoid them 685 // being converted to spaces on deserialization 686 // (fixes bug 93720) 687 switch (c) { 688 case '<': 689 return "lt"; //$NON-NLS-1$ 690 case '>': 691 return "gt"; //$NON-NLS-1$ 692 case '"': 693 return "quot"; //$NON-NLS-1$ 694 case '\'': 695 return "apos"; //$NON-NLS-1$ 696 case '&': 697 return "amp"; //$NON-NLS-1$ 698 case '\r': 699 return "#x0D"; //$NON-NLS-1$ 700 case '\n': 701 return "#x0A"; //$NON-NLS-1$ 702 case '\u0009': 703 return "#x09"; //$NON-NLS-1$ 704 } 705 return null; 706 } 707 } 708 } 709