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