001: /*
002: * $Id: JGraphEditorSettings.java,v 1.1.1.1 2005/08/04 11:21:58 gaudenz Exp $
003: * Copyright (c) 2001-2005, Gaudenz Alder
004: *
005: * All rights reserved.
006: *
007: * See LICENSE file for license details. If you are unable to locate
008: * this file please contact info (at) jgraph (dot) com.
009: */
010: package com.jgraph.editor;
011:
012: import java.awt.Dimension;
013: import java.awt.Rectangle;
014: import java.awt.Toolkit;
015: import java.awt.Window;
016: import java.io.IOException;
017: import java.io.InputStream;
018: import java.util.ArrayList;
019: import java.util.HashMap;
020: import java.util.Iterator;
021: import java.util.List;
022: import java.util.Map;
023: import java.util.Properties;
024:
025: import javax.swing.JSplitPane;
026: import javax.xml.parsers.DocumentBuilder;
027: import javax.xml.parsers.DocumentBuilderFactory;
028: import javax.xml.parsers.ParserConfigurationException;
029:
030: import org.w3c.dom.Document;
031: import org.w3c.dom.NamedNodeMap;
032: import org.w3c.dom.Node;
033: import org.w3c.dom.NodeList;
034: import org.xml.sax.SAXException;
035:
036: /**
037: * Manages configuration files for a JGraph editor, namely XML documents and
038: * properties files and holds references to configured objects.
039: */
040: public class JGraphEditorSettings {
041:
042: /**
043: * Document builder factory for parsing XML files.
044: *
045: * @see javax.xml.parsers.DocumentBuilderFactory#newInstance()
046: */
047: public static DocumentBuilderFactory documentBuilderFactory = DocumentBuilderFactory
048: .newInstance();
049:
050: /**
051: * XML attribute name for the key attribute.
052: */
053: public static final String ATTRIBUTENAME_KEY = "key";
054:
055: /**
056: * XML attribute name for the before attribute.
057: */
058: public static final String ATTRIBUTENAME_BEFORE = "before";
059:
060: /**
061: * List of hooks that are to be called on shutdown.
062: */
063: protected List shutdownHooks = new ArrayList();
064:
065: /**
066: * Holds maps of (document, name), (properties, name) and (key, Object)
067: * pairs respectively.
068: */
069: protected Map documents = new HashMap(),
070: properties = new HashMap(), objects;
071:
072: /**
073: * Constructs new settings using an empty object map.
074: */
075: public JGraphEditorSettings() {
076: this (new HashMap());
077: }
078:
079: /**
080: * Constructs new settings using the passed-in object map.
081: *
082: * @param objects
083: * The map to use as the initial objects map.
084: */
085: public JGraphEditorSettings(Map objects) {
086: this .objects = objects;
087: }
088:
089: /**
090: * Returns the document for the specified name or <code>null</code> if no
091: * such document exists.
092: *
093: * @param name
094: * The name of the document to be returned.
095: * @return Returns the backing document.
096: */
097: public Document getDocument(String name) {
098: return (Document) documents.get(name);
099: }
100:
101: /**
102: * Adds the document under the specified name or merges <code>doc</code>
103: * into an existing document for <code>name</code>.
104: *
105: * @param name
106: * The name under which the document should be added.
107: * @param doc
108: * The document to add.
109: */
110: public void add(String name, Document doc) {
111: Document document = getDocument(name);
112: if (document == null)
113: documents.put(name, doc);
114: else
115: merge(document, document.getDocumentElement(), doc
116: .getDocumentElement().getChildNodes(), true);
117: }
118:
119: /**
120: * Recursively replaces or appends <code>children</code> in
121: * <code>parent</code> for equal keys ({@link #ATTRIBUTENAME_KEY} or node
122: * names, depending on <code>useNames</code>. If the node to be added
123: * provides a before attribute and the node for the specified key exists in
124: * the parent's node list then the new node is inserted before the
125: * referenced node.
126: *
127: * @param document
128: * The document to import the nodes into.
129: * @param parent
130: * The parent node to replace or add ports.
131: * @param children
132: * The children to add or replace.
133: * @param useNames
134: * If names or keys should be used to check for node equality.
135: */
136: protected void merge(Document document, Node parent,
137: NodeList children, boolean useNames) {
138: for (int i = 0; i < children.getLength(); i++) {
139: Node childNode = children.item(i);
140:
141: // Searches for existing node (by key or node name)
142: Node existingNode = (useNames) ? getNodeByName(parent
143: .getChildNodes(), childNode.getNodeName())
144: : getNodeByAttribute(parent.getChildNodes(),
145: ATTRIBUTENAME_KEY, getAttributeValue(
146: childNode, ATTRIBUTENAME_KEY));
147: if (existingNode != null) {
148:
149: // Merges recursively
150: merge(document, existingNode,
151: childNode.getChildNodes(), false);
152: } else {
153:
154: if (existingNode != null) {
155:
156: // Removes from parent
157: existingNode.getParentNode().removeChild(
158: existingNode);
159: }
160:
161: // Appends new child or inserts before referenced node
162: String beforeKey = getAttributeValue(childNode,
163: ATTRIBUTENAME_BEFORE);
164: Node refNode = getNodeByAttribute(parent
165: .getChildNodes(), ATTRIBUTENAME_KEY, beforeKey);
166: Node newNode = document.importNode(childNode, true);
167: if (refNode != null)
168: parent.insertBefore(newNode, refNode);
169: else
170: parent.appendChild(newNode);
171: }
172: }
173: }
174:
175: /**
176: * Returns the first node for <code>name</code> in the document registered
177: * under <code>documentName</code>.
178: *
179: * @param documentName
180: * The string that identifies the document to be used.
181: * @param name
182: * The name of the node to be returned.
183: * @return Returns the first node for <code>name</code> in
184: * <code>documentName</code>.
185: */
186: public Node getNodeByName(String documentName, String name) {
187: return getNodeByName(getDocument(documentName)
188: .getDocumentElement().getChildNodes(), name);
189: }
190:
191: /**
192: * Constructs a document object for an XML input stream using the
193: * {@link #documentBuilderFactory}.
194: *
195: * @param in
196: * The input stream that represents the XML data.
197: * @return Return the parsed input stream as an XML document.
198: */
199: public static Document parse(InputStream in)
200: throws ParserConfigurationException, SAXException,
201: IOException {
202: DocumentBuilder db;
203: synchronized (documentBuilderFactory) {
204: db = documentBuilderFactory.newDocumentBuilder();
205: }
206: return db.parse(in);
207: }
208:
209: /**
210: * Returns the first node in <code>nodeList</code> whos name equals
211: * <code>name</code> or <code>null</code> if no such node exists.
212: *
213: * @param nodeList
214: * The list of nodes to scan for the name.
215: * @param name
216: * The name of the node to search for.
217: * @return Returns the first node for <code>name</code> in <code>nodeList
218: * </code>.
219: *
220: * @see Node#getNodeName()
221: */
222: public static Node getNodeByName(NodeList nodeList, String name) {
223: for (int i = 0; i < nodeList.getLength(); i++) {
224: Node node = nodeList.item(i);
225: if (node.getNodeName().equals(name))
226: return node;
227: }
228: return null;
229: }
230:
231: /**
232: * Returns the node in <code>nodeList</code> whos attribute named
233: * <code>attributeName</code> equals <code>value</code> or
234: * <code>null</code> if no such node exists.
235: *
236: * @param nodeList
237: * The nodes to scan for the attribute value.
238: * @param attributeName
239: * The name of the attribute to scan for.
240: * @param value
241: * The value of the attribute to scan for.
242: * @return Returns the first node that matches the search criteria.
243: *
244: * @see #getAttributeValue(Node, String)
245: */
246: public static Node getNodeByAttribute(NodeList nodeList,
247: String attributeName, String value) {
248: if (value != null && attributeName != null) {
249: for (int i = 0; i < nodeList.getLength(); i++) {
250: Node childNode = nodeList.item(i);
251: String attributeValue = getAttributeValue(childNode,
252: attributeName);
253: if (attributeValue != null
254: && attributeValue.equals(value))
255: return childNode;
256: }
257: }
258: return null;
259: }
260:
261: /**
262: * Returns the textual representation of the attribute in <code>node</code>
263: * whos name is <code>attributeName</code> or <code>null</code> if no
264: * such attribute exists.
265: *
266: * @param node
267: * The node whos attribute should be returned.
268: * @param attributeName
269: * The name of the attribute whos value should be returned.
270: * @return Returns the value of <code>attributeName</code> in
271: * <code>node</code>.
272: *
273: * @see Node#getAttributes()
274: * @see NamedNodeMap#getNamedItem(java.lang.String)
275: * @see Node#getNodeValue()
276: */
277: public static String getAttributeValue(Node node,
278: String attributeName) {
279: if (node != null && node.hasAttributes()) {
280: Node attribute = node.getAttributes().getNamedItem(
281: attributeName);
282: if (attribute != null)
283: return String.valueOf(attribute.getNodeValue());
284: }
285: return null;
286: }
287:
288: /**
289: * Returns the textual representation of the attribute in <code>node</code>
290: * whos name is equal to {@link #ATTRIBUTENAME_KEY}or <code>null</code>
291: * if no such node exists.
292: *
293: * @param node
294: * The node whos attribute should be returned.
295: * @return Returns the value of {@link #ATTRIBUTENAME_KEY}in
296: * <code>node</code>.
297: *
298: * @see #getAttributeValue(Node, String)
299: */
300: public static String getKeyAttributeValue(Node node) {
301: return getAttributeValue(node, ATTRIBUTENAME_KEY);
302: }
303:
304: /**
305: * Returns the properties registered under the specified name or
306: * <code>null</code> if no such properties exist.
307: *
308: * @param name
309: * The name of the properties to return.
310: * @return Returns the properties for <code>name</code>.
311: */
312: public Properties getProperties(String name) {
313: return (Properties) properties.get(name);
314: }
315:
316: /**
317: * Registers the specified properties under the specified name.
318: *
319: * @param name
320: * The name to register the properties under.
321: * @param props
322: * The properties to register.
323: */
324: public void add(String name, Properties props) {
325: properties.put(name, props);
326: }
327:
328: /**
329: * Adds the properties under a specified name from an input stream. This
330: * method creates the properties using
331: * {@link Properties#load(java.io.InputStream)}and adds the properties
332: * using {@link #add(String, Properties)}.
333: *
334: * @param name
335: * The name to register the properties under.
336: * @param s
337: * The input stream to read the properties from.
338: * @throws IOException
339: * If the input stream cannot be read.
340: */
341: public void add(String name, InputStream s) throws IOException {
342: Properties p = new Properties();
343: p.load(s);
344: add(name, p);
345: }
346:
347: /**
348: * Adds the specified object under <code>key</code>.
349: *
350: * @param key
351: * The key to register the object under.
352: * @param obj
353: * The object to register.
354: */
355: public void putObject(String key, Object obj) {
356: objects.put(key, obj);
357: }
358:
359: /**
360: * Returns the object for the specified key.
361: *
362: * @param key
363: * The key to return the object for.
364: * @return Returns the object for <code>key</code>.
365: */
366: public Object getObject(String key) {
367: return objects.get(key);
368: }
369:
370: /**
371: * Restores the window bounds for the window found in objects under
372: * <code>key</code> from the rectangle stored in the properties registered
373: * under <code>name</code> for the <code>key</code> property. This
374: * implementation assumes that the <code>key</code> property is a
375: * rectangle, ie. consists of 4 entries in the file.
376: *
377: * @param name
378: * The name of the properties to use.
379: * @param key
380: * The key of the object and rectangle property.
381: *
382: * @see #getRectangleProperty(String, String)
383: */
384: public void restoreWindow(String name, String key) {
385: Object obj = getObject(key);
386: if (obj instanceof Window && getProperties(name) != null) {
387: Window wnd = (Window) obj;
388: try {
389: Rectangle rect = getRectangleProperty(name, key);
390: if (rect != null) {
391: Dimension screen = Toolkit.getDefaultToolkit()
392: .getScreenSize();
393: if (rect.getX() > screen.getWidth())
394: rect.x = 0;
395: if (rect.getY() > screen.getHeight())
396: rect.y = 0;
397: rect.width = Math.min(screen.width, rect.width);
398: rect.height = Math.min(screen.height, rect.height);
399: wnd.setBounds(rect);
400: }
401: } catch (Exception e) {
402: // ignore
403: }
404: }
405: }
406:
407: /**
408: * Restores the divider location for the split pane found in objects under
409: * <code>key</code> from the integer stored in the properties registered
410: * under <code>name</code> for the <code>key</code> property.
411: *
412: * @param name
413: * The name of the properties to use.
414: * @param key
415: * The key of the object and integer property.
416: *
417: * @see #getRectangleProperty(String, String)
418: */
419: public void restoreSplitPane(String name, String key) {
420: Properties props = getProperties(name);
421: Object obj = getObject(key);
422: if (obj instanceof JSplitPane && props != null) {
423: JSplitPane split = (JSplitPane) obj;
424: try {
425: Integer value = new Integer(props.getProperty(key));
426: split.setDividerLocation(value.intValue());
427: } catch (Exception e) {
428: // ignore
429: }
430: }
431: }
432:
433: /**
434: * Stores the bounds of the window found in objects under <code>key</code>
435: * in the properties called <code>name</code> as a rectangle property
436: * under <code>key</code>.
437: *
438: * @param name
439: * The name of the properties to store the bounds.
440: * @param key
441: * The name of the key to store the rectangle property.
442: */
443: public void storeWindow(String name, String key) {
444: Window wnd = (Window) getObject(key);
445: putRectangleProperty(name, key, wnd.getBounds());
446: }
447:
448: /**
449: * Stores the dividerlocation of the splitpane found in objects under
450: * <code>key</code> in the properties called <code>name</code> as a int
451: * property under <code>key</code>.
452: *
453: * @param name
454: * The name of the properties to store the divider location.
455: * @param key
456: * The name of the key to store the divider location.
457: */
458: public void storeSplitPane(String name, String key) {
459: Properties props = getProperties(name);
460: JSplitPane split = (JSplitPane) getObject(key);
461: if (props != null && split != null)
462: props.put(key, String.valueOf(split.getDividerLocation()));
463: }
464:
465: /**
466: * Pushes a new entry into the list for <code>key</code> in the properties
467: * called <code>name</code> with <code>value</code>, making sure the
468: * list has no more than <code>maxCount</code> entries. If the list is
469: * longer than <code>maxCount</code>, then the oldest entry will be
470: * removed.
471: *
472: * @param name
473: * The name of the properties to store the list.
474: * @param key
475: * The name of the key to store the list entries.
476: * @param value
477: * The value of the new list entry.
478: * @param maxCount
479: * The maximum number of elemnts in the list.
480: */
481: public void pushListEntryProperty(String name, String key,
482: String value, int maxCount) {
483: Properties props = getProperties(name);
484: if (props != null) {
485: List values = new ArrayList(maxCount);
486: int i = 0;
487: String tmp = props.getProperty(key + i++);
488: while (tmp != null) {
489: values.add(tmp);
490: tmp = props.getProperty(key + i++);
491: }
492: if (value != null && !values.contains(value)) {
493: values.add(value);
494: if (values.size() > maxCount)
495: values.remove(0);
496: i = 0;
497: Iterator it = values.iterator();
498: while (it.hasNext())
499: props.setProperty(key + i++, String.valueOf(it
500: .next()));
501: }
502: }
503: }
504:
505: /**
506: * Puts the rectangle as 4 entries for the <code>key</code> -property plus
507: * a suffix of .x, .y, .width and .height into the properties called
508: * <code>name</code>.
509: *
510: * @param name
511: * The name of the properties to store the rectangle.
512: * @param key
513: * The name of the key to store the rectangle size and location.
514: * @param rect
515: * The rectangle to put into the properties.
516: */
517: public void putRectangleProperty(String name, String key,
518: Rectangle rect) {
519: Properties props = getProperties(name);
520: if (props != null && rect != null) {
521: props.put(key + ".x", String.valueOf(rect.x));
522: props.put(key + ".y", String.valueOf(rect.y));
523: props.put(key + ".width", String.valueOf(rect.width));
524: props.put(key + ".height", String.valueOf(rect.height));
525: }
526: }
527:
528: /**
529: * Gets the rectangle made up by 4 entries for the <code>key</code>
530: * -property from the properties called <code>name</code>.
531: *
532: * @param name
533: * The name of the properties to get the rectangle from.
534: * @param key
535: * The key to retrieve the rectangle values.
536: * @return Returns the rectangle for <code>key</code> in <code>name</code>.
537: *
538: * @see #putRectangleProperty(String, String, Rectangle)
539: */
540: public Rectangle getRectangleProperty(String name, String key) {
541: Properties props = getProperties(name);
542: if (props != null) {
543: int x = Integer.parseInt(props.getProperty(key + ".x"));
544: int y = Integer.parseInt(props.getProperty(key + ".y"));
545: int w = Integer.parseInt(props.getProperty(key + ".width"));
546: int h = Integer
547: .parseInt(props.getProperty(key + ".height"));
548: return new Rectangle(x, y, w, h);
549: }
550: return null;
551: }
552:
553: /**
554: * Invokes all hooks that have previously been added using addShutdownHook
555: * in reverse order (last added first).
556: */
557: public void shutdown() {
558: for (int i = shutdownHooks.size() - 1; i >= 0; i--)
559: ((ShutdownHook) shutdownHooks.get(i)).shutdown();
560: }
561:
562: /**
563: * Adds a shutdown hook which is called from {@link #shutdown()} normally
564: * when the program terminates.
565: *
566: * @param hook
567: * The shutdown hook to be added.
568: *
569: */
570: public void addShutdownHook(ShutdownHook hook) {
571: shutdownHooks.add(hook);
572: }
573:
574: /**
575: * Defines the requiements for a class that may act as a shutdown hook, ie.
576: * it is invoked shortly when the program terminates.
577: */
578: public interface ShutdownHook {
579:
580: public void shutdown();
581:
582: }
583: }
|