| author | Manuel Doninger | 2011-10-12 19:17:54 (EDT) |
|---|---|---|
| committer | Steffen Pingel | 2011-10-12 19:17:54 (EDT) |
| commit | e69647f46f596af376bd28d907cef49b2f3b056c (patch) (side-by-side diff) | |
| tree | 7c1dec98bd3f090e92bba995b1ec4007c4c1e40a | |
| parent | c50ec1fb00f710bb7738999c384b844a8ed57491 (diff) | |
| download | org.eclipse.mylyn.commons-e69647f46f596af376bd28d907cef49b2f3b056c.zip org.eclipse.mylyn.commons-e69647f46f596af376bd28d907cef49b2f3b056c.tar.gz org.eclipse.mylyn.commons-e69647f46f596af376bd28d907cef49b2f3b056c.tar.bz2 | |
NEW - bug 360365: Add XmlMemento to support externalizing data
https://bugs.eclipse.org/bugs/show_bug.cgi?id=360365
Change-Id: Id254fad10a7f50319419651045ec0743ac667344
3 files changed, 719 insertions, 0 deletions
diff --git a/org.eclipse.mylyn.commons.core/src/org/eclipse/mylyn/commons/core/XmlMemento.java b/org.eclipse.mylyn.commons.core/src/org/eclipse/mylyn/commons/core/XmlMemento.java new file mode 100644 index 0000000..5098601 --- a/dev/null +++ b/org.eclipse.mylyn.commons.core/src/org/eclipse/mylyn/commons/core/XmlMemento.java @@ -0,0 +1,707 @@ +/******************************************************************************* + * Copyright (c) 2000, 2010 IBM Corporation and others. + * All rights reserved. This program and the accompanying materials + * are made available under the terms of the Eclipse Public License v1.0 + * which accompanies this distribution, and is available at + * http://www.eclipse.org/legal/epl-v10.html + * + * Contributors: + * IBM Corporation - initial API and implementation + * Manuel Doninger - fixes for bug 360365 + *******************************************************************************/ +package org.eclipse.mylyn.commons.core; + +import java.io.IOException; +import java.io.PrintWriter; +import java.io.Reader; +import java.io.Writer; +import java.lang.reflect.InvocationTargetException; +import java.util.ArrayList; + +import javax.xml.parsers.DocumentBuilder; +import javax.xml.parsers.DocumentBuilderFactory; +import javax.xml.parsers.ParserConfigurationException; + +import org.eclipse.core.runtime.IStatus; +import org.eclipse.core.runtime.Status; +import org.eclipse.mylyn.internal.commons.core.ICommonsCoreConstants; +import org.eclipse.mylyn.internal.commons.core.Messages; +import org.w3c.dom.Attr; +import org.w3c.dom.DOMException; +import org.w3c.dom.Document; +import org.w3c.dom.Element; +import org.w3c.dom.NamedNodeMap; +import org.w3c.dom.Node; +import org.w3c.dom.NodeList; +import org.w3c.dom.Text; +import org.xml.sax.ErrorHandler; +import org.xml.sax.InputSource; +import org.xml.sax.SAXException; +import org.xml.sax.SAXParseException; + +/** + * This class represents the default implementation of the <code>XMLMemento</code> interface. + * <p> + * This class is not intended to be extended by clients. + * </p> + * + * @see XmlMemento + * @author Manuel Doninger + * @since 3.7 + */ +public final class XmlMemento { + + private final Document factory; + + private final Element element; + + private static final String TAG_ID = "XmlMemento.internal.id"; //$NON-NLS-1$ + + /** + * Creates a <code>Document</code> from the <code>Reader</code> and returns a memento on the first + * <code>Element</code> for reading the document. + * <p> + * Same as calling createReadRoot(reader, null) + * </p> + * + * @param reader + * the <code>Reader</code> used to create the memento's document + * @return a memento on the first <code>Element</code> for reading the document + * @throws InvocationTargetException + * if IO problems, invalid format, or no element. + */ + public static XmlMemento createReadRoot(Reader reader) throws InvocationTargetException { + return createReadRoot(reader, null); + } + + /** + * Creates a <code>Document</code> from the <code>Reader</code> and returns a memento on the first + * <code>Element</code> for reading the document. + * + * @param reader + * the <code>Reader</code> used to create the memento's document + * @param baseDir + * the directory used to resolve relative file names in the XML document. This directory must exist and + * include the trailing separator. The directory format, including the separators, must be valid for the + * platform. Can be <code>null</code> if not needed. + * @return a memento on the first <code>Element</code> for reading the document + * @throws InvocationTargetException + * if IO problems, invalid format, or no element. + */ + public static XmlMemento createReadRoot(Reader reader, String baseDir) throws InvocationTargetException { + String errorMessage = null; + Exception exception = null; + + try { + DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance(); + DocumentBuilder parser = factory.newDocumentBuilder(); + InputSource source = new InputSource(reader); + if (baseDir != null) { + source.setSystemId(baseDir); + } + + parser.setErrorHandler(new ErrorHandler() { + /** + * @throws SAXException + */ + public void warning(SAXParseException exception) throws SAXException { + // ignore + } + + /** + * @throws SAXException + */ + public void error(SAXParseException exception) throws SAXException { + // ignore + } + + public void fatalError(SAXParseException exception) throws SAXException { + throw exception; + } + }); + + Document document = parser.parse(source); + NodeList list = document.getChildNodes(); + for (int i = 0; i < list.getLength(); i++) { + Node node = list.item(i); + if (node instanceof Element) { + return new XmlMemento(document, (Element) node); + } + } + } catch (ParserConfigurationException e) { + exception = e; + errorMessage = Messages.XMLMemento_parserConfigError; + } catch (IOException e) { + exception = e; + errorMessage = Messages.XMLMemento_ioError; + } catch (SAXException e) { + exception = e; + errorMessage = Messages.XMLMemento_formatError; + } + + String problemText = null; + if (exception != null) { + problemText = exception.getMessage(); + } + if (problemText == null || problemText.length() == 0) { + problemText = errorMessage != null ? errorMessage : Messages.XMLMemento_noElement; + } + throw new InvocationTargetException(exception, errorMessage); + } + + /** + * Returns a root memento for writing a document. + * + * @param type + * the element node type to create on the document + * @return the root memento for writing a document + * @throws DOMException + */ + public static XmlMemento createWriteRoot(String type) throws DOMException { + Document document; + try { + document = DocumentBuilderFactory.newInstance().newDocumentBuilder().newDocument(); + Element element = document.createElement(type); + document.appendChild(element); + return new XmlMemento(document, element); + } catch (ParserConfigurationException e) { +// throw new Error(e); + throw new Error(e.getMessage()); + } + } + + /** + * Creates a memento for the specified document and element. + * <p> + * Clients should use <code>createReadRoot</code> and <code>createWriteRoot</code> to create the initial memento on + * a document. + * </p> + * + * @param document + * the document for the memento + * @param element + * the element node for the memento + */ + public XmlMemento(Document document, Element element) { + super(); + this.factory = document; + this.element = element; + } + + /** + * Creates a new child of this memento with the given type. + * <p> + * The <code>getChild</code> and <code>getChildren</code> methods are used to retrieve children of a given type. + * </p> + * + * @param type + * the type + * @return a new child memento + * @see #getChild + * @see #getChildren + * @throws DOMException + * if the child cannot be created + */ + public XmlMemento createChild(String type) throws DOMException { + Element child = factory.createElement(type); + element.appendChild(child); + return new XmlMemento(factory, child); + } + + /** + * Creates a new child of this memento with the given type and id. The id is stored in the child memento (using a + * special reserved key, <code>TAG_ID</code>) and can be retrieved using <code>getId</code>. + * <p> + * The <code>getChild</code> and <code>getChildren</code> methods are used to retrieve children of a given type. + * </p> + * + * @param type + * the type + * @param id + * the child id + * @return a new child memento with the given type and id + * @see #getID + * @throws DOMException + * if the child cannot be created + */ + public XmlMemento createChild(String type, String id) throws DOMException { + Element child = factory.createElement(type); + child.setAttribute(TAG_ID, id == null ? "" : id); //$NON-NLS-1$ + element.appendChild(child); + return new XmlMemento(factory, child); + } + + /** + * Create a copy of the child node and append it to this node. + * + * @param child + * @return An IMenento for the new child node. + * @throws DOMException + * if the child cannot be created + */ + public XmlMemento copyChild(XmlMemento child) throws DOMException { + Element childElement = child.element; + Element newElement = (Element) factory.importNode(childElement, true); + element.appendChild(newElement); + return new XmlMemento(factory, newElement); + } + + /* (non-Javadoc) + * Method declared in XMLMemento. + */ + public XmlMemento getChild(String type) { + + // Get the nodes. + NodeList nodes = element.getChildNodes(); + int size = nodes.getLength(); + if (size == 0) { + return null; + } + + // Find the first node which is a child of this node. + for (int nX = 0; nX < size; nX++) { + Node node = nodes.item(nX); + if (node instanceof Element) { + Element element = (Element) node; + if (element.getNodeName().equals(type)) { + return new XmlMemento(factory, element); + } + } + } + + // A child was not found. + return null; + } + + /* (non-Javadoc) + * Method declared in XMLMemento. + */ + public XmlMemento[] getChildren(String type) { + + // Get the nodes. + NodeList nodes = element.getChildNodes(); + int size = nodes.getLength(); + if (size == 0) { + return new XmlMemento[0]; + } + + // Extract each node with given type. + ArrayList list = new ArrayList(size); + for (int nX = 0; nX < size; nX++) { + Node node = nodes.item(nX); + if (node instanceof Element) { + Element element = (Element) node; + if (element.getNodeName().equals(type)) { + list.add(element); + } + } + } + + // Create a memento for each node. + size = list.size(); + XmlMemento[] results = new XmlMemento[size]; + for (int x = 0; x < size; x++) { + results[x] = new XmlMemento(factory, (Element) list.get(x)); + } + return results; + } + + /* (non-Javadoc) + * Method declared in XMLMemento. + */ + public Float getFloat(String key) { + Attr attr = element.getAttributeNode(key); + if (attr == null) { + return null; + } + String strValue = attr.getValue(); + try { + return new Float(strValue); + } catch (NumberFormatException e) { + StatusHandler.log(new Status(IStatus.ERROR, ICommonsCoreConstants.ID_PLUGIN, + "Memento problem - Invalid float for key: " //$NON-NLS-1$ + + key + " value: " + strValue, e)); //$NON-NLS-1$ + return null; + } + } + + /** + * @since 3.4 + */ + public String getType() { + return element.getNodeName(); + } + + /* (non-Javadoc) + * Method declared in XMLMemento. + */ + public String getID() { + return element.getAttribute(TAG_ID); + } + + /* (non-Javadoc) + * Method declared in XMLMemento. + */ + public Integer getInteger(String key) { + Attr attr = element.getAttributeNode(key); + if (attr == null) { + return null; + } + String strValue = attr.getValue(); + try { + return new Integer(strValue); + } catch (NumberFormatException e) { + StatusHandler.log(new Status(IStatus.ERROR, ICommonsCoreConstants.ID_PLUGIN, + "Memento problem - invalid integer for key: " + key //$NON-NLS-1$ + + " value: " + strValue, e)); //$NON-NLS-1$ + return null; + } + } + + /* (non-Javadoc) + * Method declared in XMLMemento. + */ + public String getString(String key) { + Attr attr = element.getAttributeNode(key); + if (attr == null) { + return null; + } + return attr.getValue(); + } + + /** + * @since 3.4 + */ + public Boolean getBoolean(String key) { + Attr attr = element.getAttributeNode(key); + if (attr == null) { + return null; + } + return Boolean.valueOf(attr.getValue()); + } + + /** + * Returns the data of the Text node of the memento. Each memento is allowed only one Text node. + * + * @return the data of the Text node of the memento, or <code>null</code> if the memento has no Text node. + * @since 2.0 + * @throws DOMException + * if the text node is too big + */ + public String getTextData() throws DOMException { + Text textNode = getTextNode(); + if (textNode != null) { + return textNode.getData(); + } + return null; + } + + /** + * @since 3.4 + */ + public String[] getAttributeKeys() { + NamedNodeMap map = element.getAttributes(); + int size = map.getLength(); + String[] attributes = new String[size]; + for (int i = 0; i < size; i++) { + Node node = map.item(i); + attributes[i] = node.getNodeName(); + } + return attributes; + } + + /** + * Returns the Text node of the memento. Each memento is allowed only one Text node. + * + * @return the Text node of the memento, or <code>null</code> if the memento has no Text node. + */ + private Text getTextNode() { + // Get the nodes. + NodeList nodes = element.getChildNodes(); + int size = nodes.getLength(); + if (size == 0) { + return null; + } + for (int nX = 0; nX < size; nX++) { + Node node = nodes.item(nX); + if (node instanceof Text) { + return (Text) node; + } + } + // a Text node was not found + return null; + } + + /** + * Places the element's attributes into the document. + * + * @param copyText + * true if the first text node should be copied + * @throws DOMException + * if the attributes or children cannot be copied to this node. + */ + private void putElement(Element element, boolean copyText) throws DOMException { + NamedNodeMap nodeMap = element.getAttributes(); + int size = nodeMap.getLength(); + for (int i = 0; i < size; i++) { + Attr attr = (Attr) nodeMap.item(i); + putString(attr.getName(), attr.getValue()); + } + + NodeList nodes = element.getChildNodes(); + size = nodes.getLength(); + // Copy first text node (fixes bug 113659). + // Note that text data will be added as the first child (see putTextData) + boolean needToCopyText = copyText; + for (int i = 0; i < size; i++) { + Node node = nodes.item(i); + if (node instanceof Element) { + XmlMemento child = createChild(node.getNodeName()); + child.putElement((Element) node, true); + } else if (node instanceof Text && needToCopyText) { + putTextData(((Text) node).getData()); + needToCopyText = false; + } + } + } + + /** + * Sets the value of the given key to the given floating point number. + * + * @param key + * the key + * @param f + * the value + * @throws DOMException + * if the attribute cannot be set + */ + public void putFloat(String key, float f) throws DOMException { + element.setAttribute(key, String.valueOf(f)); + } + + /** + * Sets the value of the given key to the given integer. + * + * @param key + * the key + * @param n + * the value + * @throws DOMException + * if the attribute cannot be set + */ + public void putInteger(String key, int n) throws DOMException { + element.setAttribute(key, String.valueOf(n)); + } + + /** + * Copy the attributes and children from <code>memento</code> to the receiver. + * + * @param memento + * the XMLMemento to be copied. + * @throws DOMException + * if the attributes or children cannot be copied to this node. + */ + public void putMemento(XmlMemento memento) throws DOMException { + // Do not copy the element's top level text node (this would overwrite the existing text). + // Text nodes of children are copied. + putElement(memento.element, false); + } + + /** + * Sets the value of the given key to the given string. + * + * @param key + * the key + * @param value + * the value + * @throws DOMException + * if the attribute cannot be set + */ + public void putString(String key, String value) throws DOMException { + if (value == null) { + return; + } + element.setAttribute(key, value); + } + + /** + * Sets the value of the given key to the given boolean value. + * + * @param key + * the key + * @param value + * the value + * @since 3.4 + * @throws DOMException + * if the attribute cannot be set + */ + public void putBoolean(String key, boolean value) throws DOMException { + element.setAttribute(key, value ? "true" : "false"); //$NON-NLS-1$ //$NON-NLS-2$ + } + + /** + * Sets the memento's Text node to contain the given data. Creates the Text node if none exists. If a Text node does + * exist, it's current contents are replaced. Each memento is allowed only one text node. + * + * @param data + * the data to be placed on the Text node + * @since 2.0 + * @throws DOMException + * if the text node cannot be created under this node. + */ + public void putTextData(String data) throws DOMException { + Text textNode = getTextNode(); + if (textNode == null) { + textNode = factory.createTextNode(data); + // Always add the text node as the first child (fixes bug 93718) + element.insertBefore(textNode, element.getFirstChild()); + } else { + textNode.setData(data); + } + } + + /** + * Saves this memento's document current values to the specified writer. + * + * @param writer + * the writer used to save the memento's document + * @throws IOException + * if there is a problem serializing the document to the stream. + */ + public void save(Writer writer) throws IOException { + DOMWriter out = new DOMWriter(writer); + try { + out.print(element); + } finally { + out.close(); + } + } + + /** + * A simple XML writer. Using this instead of the javax.xml.transform classes allows compilation against JCL + * Foundation (bug 80053). + */ + private static final class DOMWriter extends PrintWriter { + + /* constants */ + private static final String XML_VERSION = "<?xml version=\"1.0\" encoding=\"UTF-8\"?>"; //$NON-NLS-1$ + + /** + * Creates a new DOM writer on the given output writer. + * + * @param output + * the output writer + */ + public DOMWriter(Writer output) { + super(output); + println(XML_VERSION); + } + + /** + * Prints the given element. + * + * @param element + * the element to print + */ + public void print(Element element) { + // Ensure extra whitespace is not emitted next to a Text node, + // as that will result in a situation where the restored text data is not the + // same as the saved text data. + boolean hasChildren = element.hasChildNodes(); + startTag(element, hasChildren); + if (hasChildren) { + boolean prevWasText = false; + NodeList children = element.getChildNodes(); + for (int i = 0; i < children.getLength(); i++) { + Node node = children.item(i); + if (node instanceof Element) { + if (!prevWasText) { + println(); + } + print((Element) children.item(i)); + prevWasText = false; + } else if (node instanceof Text) { + print(getEscaped(node.getNodeValue())); + prevWasText = true; + } + } + if (!prevWasText) { + println(); + } + endTag(element); + } + } + + private void startTag(Element element, boolean hasChildren) { + StringBuffer sb = new StringBuffer(); + sb.append("<"); //$NON-NLS-1$ + sb.append(element.getTagName()); + NamedNodeMap attributes = element.getAttributes(); + for (int i = 0; i < attributes.getLength(); i++) { + Attr attribute = (Attr) attributes.item(i); + sb.append(" "); //$NON-NLS-1$ + sb.append(attribute.getName()); + sb.append("=\""); //$NON-NLS-1$ + sb.append(getEscaped(String.valueOf(attribute.getValue()))); + sb.append("\""); //$NON-NLS-1$ + } + sb.append(hasChildren ? ">" : "/>"); //$NON-NLS-1$ //$NON-NLS-2$ + print(sb.toString()); + } + + private void endTag(Element element) { + StringBuffer sb = new StringBuffer(); + sb.append("</"); //$NON-NLS-1$ + sb.append(element.getNodeName()); + sb.append(">"); //$NON-NLS-1$ + print(sb.toString()); + } + + private static void appendEscapedChar(StringBuffer buffer, char c) { + String replacement = getReplacement(c); + if (replacement != null) { + buffer.append('&'); + buffer.append(replacement); + buffer.append(';'); + } else if (c == 9 || c == 10 || c == 13 || c >= 32) { + buffer.append(c); + } + } + + private static String getEscaped(String s) { + StringBuffer result = new StringBuffer(s.length() + 10); + for (int i = 0; i < s.length(); ++i) { + appendEscapedChar(result, s.charAt(i)); + } + return result.toString(); + } + + private static String getReplacement(char c) { + // Encode special XML characters into the equivalent character references. + // The first five are defined by default for all XML documents. + // The next three (#xD, #xA, #x9) are encoded to avoid them + // being converted to spaces on deserialization + // (fixes bug 93720) + switch (c) { + case '<': + return "lt"; //$NON-NLS-1$ + case '>': + return "gt"; //$NON-NLS-1$ + case '"': + return "quot"; //$NON-NLS-1$ + case '\'': + return "apos"; //$NON-NLS-1$ + case '&': + return "amp"; //$NON-NLS-1$ + case '\r': + return "#x0D"; //$NON-NLS-1$ + case '\n': + return "#x0A"; //$NON-NLS-1$ + case '\u0009': + return "#x09"; //$NON-NLS-1$ + } + return null; + } + } + +} diff --git a/org.eclipse.mylyn.commons.core/src/org/eclipse/mylyn/internal/commons/core/Messages.java b/org.eclipse.mylyn.commons.core/src/org/eclipse/mylyn/internal/commons/core/Messages.java index 7d0b623..56ef3dd 100644 --- a/org.eclipse.mylyn.commons.core/src/org/eclipse/mylyn/internal/commons/core/Messages.java +++ b/org.eclipse.mylyn.commons.core/src/org/eclipse/mylyn/internal/commons/core/Messages.java @@ -45,6 +45,14 @@ public class Messages extends NLS { public static String DateUtil_weeks; + public static String XMLMemento_parserConfigError; + + public static String XMLMemento_ioError; + + public static String XMLMemento_formatError; + + public static String XMLMemento_noElement; + static { // initialize resource bundle NLS.initializeMessages(BUNDLE_NAME, Messages.class); diff --git a/org.eclipse.mylyn.commons.core/src/org/eclipse/mylyn/internal/commons/core/messages.properties b/org.eclipse.mylyn.commons.core/src/org/eclipse/mylyn/internal/commons/core/messages.properties index 8c30def..cb3cb4a 100644 --- a/org.eclipse.mylyn.commons.core/src/org/eclipse/mylyn/internal/commons/core/messages.properties +++ b/org.eclipse.mylyn.commons.core/src/org/eclipse/mylyn/internal/commons/core/messages.properties @@ -12,3 +12,7 @@ DateUtil_second={0} sec DateUtil_seconds={0} secs DateUtil_week={0} week DateUtil_weeks={0} weeks +XMLMemento_parserConfigError = Internal XML parser configuration error. +XMLMemento_ioError = Could not read content of XML file. +XMLMemento_formatError = Could not parse content of XML file. +XMLMemento_noElement = Could not find root element node of XML file.
\ No newline at end of file |

