001: /*******************************************************************************
002: * Copyright (c) 2000, 2007 IBM Corporation and others.
003: * All rights reserved. This program and the accompanying materials
004: * are made available under the terms of the Eclipse Public License v1.0
005: * which accompanies this distribution, and is available at
006: * http://www.eclipse.org/legal/epl-v10.html
007: *
008: * Contributors:
009: * IBM Corporation - initial API and implementation
010: *******************************************************************************/package org.eclipse.ui;
011:
012: import java.io.IOException;
013: import java.io.PrintWriter;
014: import java.io.Reader;
015: import java.io.Writer;
016: import java.util.ArrayList;
017:
018: import javax.xml.parsers.DocumentBuilder;
019: import javax.xml.parsers.DocumentBuilderFactory;
020: import javax.xml.parsers.ParserConfigurationException;
021:
022: import org.eclipse.ui.internal.WorkbenchMessages;
023: import org.eclipse.ui.internal.WorkbenchPlugin;
024: import org.w3c.dom.Attr;
025: import org.w3c.dom.Document;
026: import org.w3c.dom.Element;
027: import org.w3c.dom.NamedNodeMap;
028: import org.w3c.dom.Node;
029: import org.w3c.dom.NodeList;
030: import org.w3c.dom.Text;
031: import org.xml.sax.InputSource;
032: import org.xml.sax.SAXException;
033:
034: /**
035: * This class represents the default implementation of the
036: * <code>IMemento</code> interface.
037: * <p>
038: * This class is not intended to be extended by clients.
039: * </p>
040: *
041: * @see IMemento
042: */
043: public final class XMLMemento implements IMemento {
044: private Document factory;
045:
046: private Element element;
047:
048: /**
049: * Creates a <code>Document</code> from the <code>Reader</code>
050: * and returns a memento on the first <code>Element</code> for reading
051: * the document.
052: * <p>
053: * Same as calling createReadRoot(reader, null)
054: * </p>
055: *
056: * @param reader the <code>Reader</code> used to create the memento's document
057: * @return a memento on the first <code>Element</code> for reading the document
058: * @throws WorkbenchException if IO problems, invalid format, or no element.
059: */
060: public static XMLMemento createReadRoot(Reader reader)
061: throws WorkbenchException {
062: return createReadRoot(reader, null);
063: }
064:
065: /**
066: * Creates a <code>Document</code> from the <code>Reader</code>
067: * and returns a memento on the first <code>Element</code> for reading
068: * the document.
069: *
070: * @param reader the <code>Reader</code> used to create the memento's document
071: * @param baseDir the directory used to resolve relative file names
072: * in the XML document. This directory must exist and include the
073: * trailing separator. The directory format, including the separators,
074: * must be valid for the platform. Can be <code>null</code> if not
075: * needed.
076: * @return a memento on the first <code>Element</code> for reading the document
077: * @throws WorkbenchException if IO problems, invalid format, or no element.
078: */
079: public static XMLMemento createReadRoot(Reader reader,
080: String baseDir) throws WorkbenchException {
081: String errorMessage = null;
082: Exception exception = null;
083:
084: try {
085: DocumentBuilderFactory factory = DocumentBuilderFactory
086: .newInstance();
087: DocumentBuilder parser = factory.newDocumentBuilder();
088: InputSource source = new InputSource(reader);
089: if (baseDir != null) {
090: source.setSystemId(baseDir);
091: }
092: Document document = parser.parse(source);
093: NodeList list = document.getChildNodes();
094: for (int i = 0; i < list.getLength(); i++) {
095: Node node = list.item(i);
096: if (node instanceof Element) {
097: return new XMLMemento(document, (Element) node);
098: }
099: }
100: } catch (ParserConfigurationException e) {
101: exception = e;
102: errorMessage = WorkbenchMessages.XMLMemento_parserConfigError;
103: } catch (IOException e) {
104: exception = e;
105: errorMessage = WorkbenchMessages.XMLMemento_ioError;
106: } catch (SAXException e) {
107: exception = e;
108: errorMessage = WorkbenchMessages.XMLMemento_formatError;
109: }
110:
111: String problemText = null;
112: if (exception != null) {
113: problemText = exception.getMessage();
114: }
115: if (problemText == null || problemText.length() == 0) {
116: problemText = errorMessage != null ? errorMessage
117: : WorkbenchMessages.XMLMemento_noElement;
118: }
119: throw new WorkbenchException(problemText, exception);
120: }
121:
122: /**
123: * Returns a root memento for writing a document.
124: *
125: * @param type the element node type to create on the document
126: * @return the root memento for writing a document
127: */
128: public static XMLMemento createWriteRoot(String type) {
129: Document document;
130: try {
131: document = DocumentBuilderFactory.newInstance()
132: .newDocumentBuilder().newDocument();
133: Element element = document.createElement(type);
134: document.appendChild(element);
135: return new XMLMemento(document, element);
136: } catch (ParserConfigurationException e) {
137: // throw new Error(e);
138: throw new Error(e.getMessage());
139: }
140: }
141:
142: /**
143: * Creates a memento for the specified document and element.
144: * <p>
145: * Clients should use <code>createReadRoot</code> and
146: * <code>createWriteRoot</code> to create the initial
147: * memento on a document.
148: * </p>
149: *
150: * @param document the document for the memento
151: * @param element the element node for the memento
152: */
153: public XMLMemento(Document document, Element element) {
154: super ();
155: this .factory = document;
156: this .element = element;
157: }
158:
159: /* (non-Javadoc)
160: * Method declared in IMemento.
161: */
162: public IMemento createChild(String type) {
163: Element child = factory.createElement(type);
164: element.appendChild(child);
165: return new XMLMemento(factory, child);
166: }
167:
168: /* (non-Javadoc)
169: * Method declared in IMemento.
170: */
171: public IMemento createChild(String type, String id) {
172: Element child = factory.createElement(type);
173: child.setAttribute(TAG_ID, id == null ? "" : id); //$NON-NLS-1$
174: element.appendChild(child);
175: return new XMLMemento(factory, child);
176: }
177:
178: /* (non-Javadoc)
179: * Method declared in IMemento.
180: */
181: public IMemento copyChild(IMemento child) {
182: Element childElement = ((XMLMemento) child).element;
183: Element newElement = (Element) factory.importNode(childElement,
184: true);
185: element.appendChild(newElement);
186: return new XMLMemento(factory, newElement);
187: }
188:
189: /* (non-Javadoc)
190: * Method declared in IMemento.
191: */
192: public IMemento getChild(String type) {
193:
194: // Get the nodes.
195: NodeList nodes = element.getChildNodes();
196: int size = nodes.getLength();
197: if (size == 0) {
198: return null;
199: }
200:
201: // Find the first node which is a child of this node.
202: for (int nX = 0; nX < size; nX++) {
203: Node node = nodes.item(nX);
204: if (node instanceof Element) {
205: Element element = (Element) node;
206: if (element.getNodeName().equals(type)) {
207: return new XMLMemento(factory, element);
208: }
209: }
210: }
211:
212: // A child was not found.
213: return null;
214: }
215:
216: /* (non-Javadoc)
217: * Method declared in IMemento.
218: */
219: public IMemento[] getChildren(String type) {
220:
221: // Get the nodes.
222: NodeList nodes = element.getChildNodes();
223: int size = nodes.getLength();
224: if (size == 0) {
225: return new IMemento[0];
226: }
227:
228: // Extract each node with given type.
229: ArrayList list = new ArrayList(size);
230: for (int nX = 0; nX < size; nX++) {
231: Node node = nodes.item(nX);
232: if (node instanceof Element) {
233: Element element = (Element) node;
234: if (element.getNodeName().equals(type)) {
235: list.add(element);
236: }
237: }
238: }
239:
240: // Create a memento for each node.
241: size = list.size();
242: IMemento[] results = new IMemento[size];
243: for (int x = 0; x < size; x++) {
244: results[x] = new XMLMemento(factory, (Element) list.get(x));
245: }
246: return results;
247: }
248:
249: /* (non-Javadoc)
250: * Method declared in IMemento.
251: */
252: public Float getFloat(String key) {
253: Attr attr = element.getAttributeNode(key);
254: if (attr == null) {
255: return null;
256: }
257: String strValue = attr.getValue();
258: try {
259: return new Float(strValue);
260: } catch (NumberFormatException e) {
261: WorkbenchPlugin.log(
262: "Memento problem - Invalid float for key: " //$NON-NLS-1$
263: + key + " value: " + strValue, e); //$NON-NLS-1$
264: return null;
265: }
266: }
267:
268: /* (non-Javadoc)
269: * @see org.eclipse.ui.IMemento#getType()
270: */
271: public String getType() {
272: return element.getNodeName();
273: }
274:
275: /* (non-Javadoc)
276: * Method declared in IMemento.
277: */
278: public String getID() {
279: return element.getAttribute(TAG_ID);
280: }
281:
282: /* (non-Javadoc)
283: * Method declared in IMemento.
284: */
285: public Integer getInteger(String key) {
286: Attr attr = element.getAttributeNode(key);
287: if (attr == null) {
288: return null;
289: }
290: String strValue = attr.getValue();
291: try {
292: return new Integer(strValue);
293: } catch (NumberFormatException e) {
294: WorkbenchPlugin.log(
295: "Memento problem - invalid integer for key: " + key //$NON-NLS-1$
296: + " value: " + strValue, e); //$NON-NLS-1$
297: return null;
298: }
299: }
300:
301: /* (non-Javadoc)
302: * Method declared in IMemento.
303: */
304: public String getString(String key) {
305: Attr attr = element.getAttributeNode(key);
306: if (attr == null) {
307: return null;
308: }
309: return attr.getValue();
310: }
311:
312: /* (non-Javadoc)
313: * @see org.eclipse.ui.IMemento#getBoolean(java.lang.String)
314: */
315: public Boolean getBoolean(String key) {
316: Attr attr = element.getAttributeNode(key);
317: if (attr == null) {
318: return null;
319: }
320: return Boolean.valueOf(attr.getValue());
321: }
322:
323: /* (non-Javadoc)
324: * Method declared in IMemento.
325: */
326: public String getTextData() {
327: Text textNode = getTextNode();
328: if (textNode != null) {
329: return textNode.getData();
330: }
331: return null;
332: }
333:
334: /* (non-Javadoc)
335: * @see org.eclipse.ui.IMemento#getAttributeKeys()
336: */
337: public String[] getAttributeKeys() {
338: NamedNodeMap map = element.getAttributes();
339: int size = map.getLength();
340: String[] attributes = new String[size];
341: for (int i = 0; i < size; i++) {
342: Node node = map.item(i);
343: attributes[i] = node.getNodeName();
344: }
345: return attributes;
346: }
347:
348: /**
349: * Returns the Text node of the memento. Each memento is allowed only
350: * one Text node.
351: *
352: * @return the Text node of the memento, or <code>null</code> if
353: * the memento has no Text node.
354: */
355: private Text getTextNode() {
356: // Get the nodes.
357: NodeList nodes = element.getChildNodes();
358: int size = nodes.getLength();
359: if (size == 0) {
360: return null;
361: }
362: for (int nX = 0; nX < size; nX++) {
363: Node node = nodes.item(nX);
364: if (node instanceof Text) {
365: return (Text) node;
366: }
367: }
368: // a Text node was not found
369: return null;
370: }
371:
372: /**
373: * Places the element's attributes into the document.
374: * @param copyText true if the first text node should be copied
375: */
376: private void putElement(Element element, boolean copyText) {
377: NamedNodeMap nodeMap = element.getAttributes();
378: int size = nodeMap.getLength();
379: for (int i = 0; i < size; i++) {
380: Attr attr = (Attr) nodeMap.item(i);
381: putString(attr.getName(), attr.getValue());
382: }
383:
384: NodeList nodes = element.getChildNodes();
385: size = nodes.getLength();
386: // Copy first text node (fixes bug 113659).
387: // Note that text data will be added as the first child (see putTextData)
388: boolean needToCopyText = copyText;
389: for (int i = 0; i < size; i++) {
390: Node node = nodes.item(i);
391: if (node instanceof Element) {
392: XMLMemento child = (XMLMemento) createChild(node
393: .getNodeName());
394: child.putElement((Element) node, true);
395: } else if (node instanceof Text && needToCopyText) {
396: putTextData(((Text) node).getData());
397: needToCopyText = false;
398: }
399: }
400: }
401:
402: /* (non-Javadoc)
403: * Method declared in IMemento.
404: */
405: public void putFloat(String key, float f) {
406: element.setAttribute(key, String.valueOf(f));
407: }
408:
409: /* (non-Javadoc)
410: * Method declared in IMemento.
411: */
412: public void putInteger(String key, int n) {
413: element.setAttribute(key, String.valueOf(n));
414: }
415:
416: /* (non-Javadoc)
417: * Method declared in IMemento.
418: */
419: public void putMemento(IMemento memento) {
420: // Do not copy the element's top level text node (this would overwrite the existing text).
421: // Text nodes of children are copied.
422: putElement(((XMLMemento) memento).element, false);
423: }
424:
425: /* (non-Javadoc)
426: * Method declared in IMemento.
427: */
428: public void putString(String key, String value) {
429: if (value == null) {
430: return;
431: }
432: element.setAttribute(key, value);
433: }
434:
435: /* (non-Javadoc)
436: * @see org.eclipse.ui.IMemento#putBoolean(java.lang.String, boolean)
437: */
438: public void putBoolean(String key, boolean value) {
439: element.setAttribute(key, value ? "true" : "false"); //$NON-NLS-1$ //$NON-NLS-2$
440: }
441:
442: /* (non-Javadoc)
443: * Method declared in IMemento.
444: */
445: public void putTextData(String data) {
446: Text textNode = getTextNode();
447: if (textNode == null) {
448: textNode = factory.createTextNode(data);
449: // Always add the text node as the first child (fixes bug 93718)
450: element.insertBefore(textNode, element.getFirstChild());
451: } else {
452: textNode.setData(data);
453: }
454: }
455:
456: /**
457: * Saves this memento's document current values to the
458: * specified writer.
459: *
460: * @param writer the writer used to save the memento's document
461: * @throws IOException if there is a problem serializing the document to the stream.
462: */
463: public void save(Writer writer) throws IOException {
464: DOMWriter out = new DOMWriter(writer);
465: try {
466: out.print(element);
467: } finally {
468: out.close();
469: }
470: }
471:
472: /**
473: * A simple XML writer. Using this instead of the javax.xml.transform classes allows
474: * compilation against JCL Foundation (bug 80053).
475: */
476: private static final class DOMWriter extends PrintWriter {
477:
478: private int tab;
479:
480: /* constants */
481: private static final String XML_VERSION = "<?xml version=\"1.0\" encoding=\"UTF-8\"?>"; //$NON-NLS-1$
482:
483: /**
484: * Creates a new DOM writer on the given output writer.
485: *
486: * @param output the output writer
487: */
488: public DOMWriter(Writer output) {
489: super (output);
490: tab = 0;
491: println(XML_VERSION);
492: }
493:
494: /**
495: * Prints the given element.
496: *
497: * @param element the element to print
498: */
499: public void print(Element element) {
500: // Ensure extra whitespace is not emitted next to a Text node,
501: // as that will result in a situation where the restored text data is not the
502: // same as the saved text data.
503: boolean hasChildren = element.hasChildNodes();
504: startTag(element, hasChildren);
505: if (hasChildren) {
506: tab++;
507: boolean prevWasText = false;
508: NodeList children = element.getChildNodes();
509: for (int i = 0; i < children.getLength(); i++) {
510: Node node = children.item(i);
511: if (node instanceof Element) {
512: if (!prevWasText) {
513: println();
514: printTabulation();
515: }
516: print((Element) children.item(i));
517: prevWasText = false;
518: } else if (node instanceof Text) {
519: print(getEscaped(node.getNodeValue()));
520: prevWasText = true;
521: }
522: }
523: tab--;
524: if (!prevWasText) {
525: println();
526: printTabulation();
527: }
528: endTag(element);
529: }
530: }
531:
532: private void printTabulation() {
533: // Indenting is disabled, as it can affect the result of getTextData().
534: // In 3.0, elements were separated by a newline but not indented.
535: // This causes getTextData() to return "\n" even if no text data had explicitly been set.
536: // The code here emulates that behaviour.
537:
538: // for (int i = 0; i < tab; i++)
539: // super.print("\t"); //$NON-NLS-1$
540: }
541:
542: private void startTag(Element element, boolean hasChildren) {
543: StringBuffer sb = new StringBuffer();
544: sb.append("<"); //$NON-NLS-1$
545: sb.append(element.getTagName());
546: NamedNodeMap attributes = element.getAttributes();
547: for (int i = 0; i < attributes.getLength(); i++) {
548: Attr attribute = (Attr) attributes.item(i);
549: sb.append(" "); //$NON-NLS-1$
550: sb.append(attribute.getName());
551: sb.append("=\""); //$NON-NLS-1$
552: sb.append(getEscaped(String.valueOf(attribute
553: .getValue())));
554: sb.append("\""); //$NON-NLS-1$
555: }
556: sb.append(hasChildren ? ">" : "/>"); //$NON-NLS-1$ //$NON-NLS-2$
557: print(sb.toString());
558: }
559:
560: private void endTag(Element element) {
561: StringBuffer sb = new StringBuffer();
562: sb.append("</"); //$NON-NLS-1$
563: sb.append(element.getNodeName());
564: sb.append(">"); //$NON-NLS-1$
565: print(sb.toString());
566: }
567:
568: private static void appendEscapedChar(StringBuffer buffer,
569: char c) {
570: String replacement = getReplacement(c);
571: if (replacement != null) {
572: buffer.append('&');
573: buffer.append(replacement);
574: buffer.append(';');
575: } else {
576: buffer.append(c);
577: }
578: }
579:
580: private static String getEscaped(String s) {
581: StringBuffer result = new StringBuffer(s.length() + 10);
582: for (int i = 0; i < s.length(); ++i) {
583: appendEscapedChar(result, s.charAt(i));
584: }
585: return result.toString();
586: }
587:
588: private static String getReplacement(char c) {
589: // Encode special XML characters into the equivalent character references.
590: // The first five are defined by default for all XML documents.
591: // The next three (#xD, #xA, #x9) are encoded to avoid them
592: // being converted to spaces on deserialization
593: // (fixes bug 93720)
594: switch (c) {
595: case '<':
596: return "lt"; //$NON-NLS-1$
597: case '>':
598: return "gt"; //$NON-NLS-1$
599: case '"':
600: return "quot"; //$NON-NLS-1$
601: case '\'':
602: return "apos"; //$NON-NLS-1$
603: case '&':
604: return "amp"; //$NON-NLS-1$
605: case '\r':
606: return "#x0D"; //$NON-NLS-1$
607: case '\n':
608: return "#x0A"; //$NON-NLS-1$
609: case '\u0009':
610: return "#x09"; //$NON-NLS-1$
611: }
612: return null;
613: }
614: }
615: }
|