001: /*
002: * $Id: JGraphEditorModel.java,v 1.6 2007/07/27 14:15:59 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.beans.BeanInfo;
013: import java.beans.DefaultPersistenceDelegate;
014: import java.beans.Encoder;
015: import java.beans.ExceptionListener;
016: import java.beans.Expression;
017: import java.beans.IntrospectionException;
018: import java.beans.Introspector;
019: import java.beans.PersistenceDelegate;
020: import java.beans.PropertyDescriptor;
021: import java.beans.XMLDecoder;
022: import java.beans.XMLEncoder;
023: import java.io.BufferedInputStream;
024: import java.io.FileNotFoundException;
025: import java.io.IOException;
026: import java.io.InputStream;
027: import java.io.OutputStream;
028: import java.net.MalformedURLException;
029: import java.util.ArrayList;
030: import java.util.Enumeration;
031: import java.util.Hashtable;
032: import java.util.Iterator;
033: import java.util.LinkedList;
034: import java.util.List;
035: import java.util.Map;
036: import java.util.zip.GZIPInputStream;
037: import java.util.zip.GZIPOutputStream;
038:
039: import javax.swing.event.TreeModelEvent;
040: import javax.swing.event.TreeModelListener;
041: import javax.swing.tree.DefaultMutableTreeNode;
042: import javax.swing.tree.DefaultTreeModel;
043: import javax.swing.tree.MutableTreeNode;
044: import javax.swing.tree.TreeNode;
045: import javax.swing.tree.TreePath;
046:
047: import org.jgraph.event.GraphLayoutCacheEvent;
048: import org.jgraph.event.GraphLayoutCacheListener;
049: import org.jgraph.event.GraphModelEvent;
050: import org.jgraph.event.GraphModelListener;
051: import org.jgraph.graph.DefaultEdge;
052: import org.jgraph.graph.DefaultGraphModel;
053: import org.jgraph.graph.EdgeView;
054: import org.jgraph.graph.GraphConstants;
055: import org.jgraph.graph.GraphLayoutCache;
056: import org.jgraph.graph.PortView;
057: import org.jgraph.graph.VertexView;
058:
059: import com.jgraph.JGraphpad;
060:
061: /**
062: * Default document model for JGraph editors. This class is in charge of
063: * preparing the bean info classes and has a map of persistence delegate for XML
064: * encoding. You should use the writeObject and readObject method of this class
065: * for file I/O to properly use the registered peristence delegates. <br>
066: * Note: The API refers to the children of the {@link #rootNode}as roots.
067: */
068: public class JGraphEditorModel extends DefaultTreeModel {
069:
070: /**
071: * Prepares class bean infos for XML encoding. Note: To encode cell views
072: * they must have an empty constructor and all fields (except cell and
073: * attributes) must be marked transient in the BeanInfo. To do this for a
074: * new cell view, use {@link #makeCellViewFieldsTransient(Class)}.
075: */
076: static {
077: makeCellViewFieldsTransient(PortView.class);
078: makeCellViewFieldsTransient(VertexView.class);
079: makeCellViewFieldsTransient(EdgeView.class);
080: }
081:
082: /**
083: * Holds the (class, persistence delegate) pairs.
084: */
085: protected Map persistenceDelegates = new Hashtable();
086:
087: /**
088: * Reference to the mutable root node.
089: */
090: protected DefaultMutableTreeNode rootNode;
091:
092: /**
093: * Constructs a new JGraph editor model adding persistence delegates for the
094: * DefaultGraphModel, GraphLayoutCache and DefaultEdge.DefaultRouting
095: * classes. This also adds a tree model listener to this model to update the
096: * modified state of parent files for child changes.
097: */
098: public JGraphEditorModel() {
099: super (new DefaultMutableTreeNode());
100: rootNode = (DefaultMutableTreeNode) getRoot();
101:
102: // Adds a listener to update parent file states
103: // on child changes.
104: addTreeModelListener(new TreeModelListener() {
105:
106: public void treeNodesChanged(TreeModelEvent e) {
107: Object[] children = e.getChildren();
108: for (int i = 0; i < children.length; i++) {
109: JGraphEditorFile file = getParentFile((TreeNode) children[i]);
110:
111: if (JGraphpad.INNER_LIBRARIES
112: && file.getParent() instanceof JGraphEditorFile) {
113: file = (JGraphEditorFile) file.getParent();
114: }
115:
116: if (file != children[i])
117: setModified(file, true);
118: }
119: }
120:
121: public void treeNodesInserted(TreeModelEvent e) {
122: Object[] children = e.getChildren();
123: for (int i = 0; i < children.length; i++) {
124: JGraphEditorFile file = getParentFile((TreeNode) children[i]);
125:
126: if (JGraphpad.INNER_LIBRARIES
127: && file.getParent() instanceof JGraphEditorFile) {
128: file = (JGraphEditorFile) file.getParent();
129: }
130:
131: if (file != children[i])
132: setModified(file, true);
133: }
134: }
135:
136: public void treeNodesRemoved(TreeModelEvent e) { // empty
137: }
138:
139: public void treeStructureChanged(TreeModelEvent e) { // empty
140: }
141:
142: });
143:
144: // Adds default persistence delegates
145: addPersistenceDelegate(DefaultGraphModel.class,
146: new DefaultPersistenceDelegate(new String[] { "roots",
147: "attributes" }));
148: addPersistenceDelegate(GraphLayoutCache.class,
149: new DefaultPersistenceDelegate(new String[] { "model",
150: "factory", "cellViews", "hiddenCellViews",
151: "partial" }));
152:
153: // DefaultEdge.DefaultRouting has a shared instance which may
154: // be retrieved using GraphConstants.getROUTING_SIMPLE().
155: addPersistenceDelegate(DefaultEdge.DefaultRouting.class,
156: new PersistenceDelegate() {
157: protected Expression instantiate(
158: Object oldInstance, Encoder out) {
159: return new Expression(oldInstance,
160: GraphConstants.class,
161: "getROUTING_SIMPLE", null);
162: }
163: });
164:
165: addPersistenceDelegate(DefaultEdge.LoopRouting.class,
166: new PersistenceDelegate() {
167: protected Expression instantiate(
168: Object oldInstance, Encoder out) {
169: return new Expression(oldInstance,
170: GraphConstants.class,
171: "getROUTING_DEFAULT", null);
172: }
173: });
174: }
175:
176: /**
177: * Returns the first generation of childs, aka roots. This usually returns
178: * the documents and repositories, eg files that are currently open.
179: *
180: * @return Returns the children of {@link #rootNode}.
181: */
182: public Enumeration roots() {
183: return rootNode.children();
184: }
185:
186: /**
187: * Associates the specified persistence delegate with <code>clazz</code>
188: * for XML encoding.
189: *
190: * @param clazz
191: * The class to associate the delegate with.
192: * @return Returns the previous delegate for <code>clazz</code>.
193: */
194: public Object addPersistenceDelegate(Class clazz,
195: PersistenceDelegate delegate) {
196: if (delegate != null)
197: return persistenceDelegates.put(clazz, delegate);
198: return null;
199: }
200:
201: /**
202: * Returns the associated persistence delegate for <code>clazz</code> or
203: * <code>null</code> if no association exists.
204: *
205: * @param clazz
206: * The clazz to return the delegate for.
207: * @return Returns the persistence delegate for <code>clazz</code> or
208: * <code>null</code>.
209: */
210: public PersistenceDelegate getPersistenceDelegate(Class clazz) {
211: if (clazz != null)
212: return (PersistenceDelegate) persistenceDelegates
213: .get(clazz);
214: return null;
215: }
216:
217: /**
218: * Adds the specified root as a child to {@link #rootNode}. Calls
219: * {@link #installListeners(TreeNode)}on the node.
220: *
221: * @param node
222: * The node to add to {@link #rootNode}.
223: * @return Returns the node that has been added.
224: */
225: public MutableTreeNode addRoot(MutableTreeNode node) {
226: insertNodeInto(node, rootNode, getChildCount(getRoot()));
227: installListeners(node);
228: return node;
229: }
230:
231: /**
232: * Adds the specified child to <code>parent</code>. Calls
233: * {@link #installListeners(TreeNode)}on the child.
234: *
235: * @param child
236: * The node to add to <code>parent</code>.
237: * @param parent
238: * The parent to add <code>child</code> to.
239: * @return Returns the child that has been added.
240: */
241: public MutableTreeNode addChild(MutableTreeNode child,
242: MutableTreeNode parent) {
243: insertNodeInto(child, parent, getChildCount(parent));
244: installListeners(child);
245: return child;
246: }
247:
248: /**
249: * Reads the specified URI and adds it as a root.
250: *
251: * @param uri
252: * The URI to read the object from.
253: * @return Returns the object stat was added.
254: */
255: public Object addFile(String uri) throws MalformedURLException,
256: IOException {
257: if (uri != null) {
258: Object file = getFileByFilename(uri);
259: if (file == null) {
260: InputStream in = getInputStream(uri);
261: file = readObject(in);
262: in.close();
263: if (file instanceof JGraphEditorFile) {
264: ((JGraphEditorFile) file).setFilename(uri);
265: addRoot((JGraphEditorFile) file);
266: }
267: return file;
268: }
269: }
270: return null;
271: }
272:
273: /**
274: * Hook for subclassers to install the required listeners in new tree nodes.
275: * This is invoked recursively for tree nodes and calls
276: * {@link #installDiagramListeners(JGraphEditorDiagram)}on all diagrams
277: * that are found along the invocation chain.
278: *
279: * @param node
280: * The node to scan for diagrams.
281: */
282: protected void installListeners(TreeNode node) {
283: if (node instanceof TreeNode) {
284: for (int i = 0; i < getChildCount(node); i++) {
285: Object child = getChild(node, i);
286: if (child instanceof TreeNode)
287: installListeners((TreeNode) child);
288: }
289: }
290: if (node instanceof JGraphEditorDiagram) {
291: installDiagramListeners((JGraphEditorDiagram) node);
292: }
293: }
294:
295: /**
296: * Installs the listeners required to update the modified state of the
297: * parent file node to <code>diagram</code>. This implementation adds a
298: * graph layout cache listener and a graph model listener.
299: *
300: * @param diagram
301: * The diagram to install the listeners to.
302: */
303: protected void installDiagramListeners(
304: final JGraphEditorDiagram diagram) {
305: diagram.getGraphLayoutCache().addGraphLayoutCacheListener(
306: new GraphLayoutCacheListener() {
307:
308: public void graphLayoutCacheChanged(
309: GraphLayoutCacheEvent e) {
310: JGraphEditorFile file = getParentFile(diagram);
311: if (JGraphpad.INNER_LIBRARIES
312: && file.getParent() instanceof JGraphEditorFile) {
313: file = (JGraphEditorFile) file.getParent();
314: }
315: setModified(file, true);
316: }
317:
318: });
319: diagram.getGraphLayoutCache().getModel().addGraphModelListener(
320: new GraphModelListener() {
321:
322: public void graphChanged(GraphModelEvent e) {
323: JGraphEditorFile file = getParentFile(diagram);
324: if (JGraphpad.INNER_LIBRARIES
325: && file.getParent() instanceof JGraphEditorFile) {
326: file = (JGraphEditorFile) file.getParent();
327: }
328: setModified(file, true);
329: }
330: });
331: }
332:
333: /**
334: * Sets the user object of the specified node and dispatches a notification
335: * event.
336: *
337: * @param node
338: * The node to change the user object for.
339: * @param userObject
340: * The new user object.
341: */
342: public void setUserObject(TreeNode node, Object userObject) {
343: TreePath path = new TreePath(getPathToRoot(node));
344: valueForPathChanged(path, userObject);
345: }
346:
347: /**
348: * Sets the filename of the specified file and dispatches a notification
349: * event.
350: *
351: * @param file
352: * The file to change the filename for.
353: * @param filename
354: * The new filename.
355: */
356: public void setFilename(JGraphEditorFile file, String filename) {
357: file.setFilename(filename);
358: nodeChanged(file);
359: }
360:
361: /**
362: * Sets the name of the specified diagram and dispatches a notification
363: * event.
364: *
365: * @param diagram
366: * The diagram to change the name for.
367: * @param name
368: * The new name.
369: */
370: public void setName(JGraphEditorDiagram diagram, String name) {
371: diagram.setName(name);
372: nodeChanged(diagram);
373: }
374:
375: /**
376: * Sets the modified state of the specified file and dispatches a
377: * notification event.
378: *
379: * @param file
380: * The file to change the modified state for.
381: * @param modified
382: * The new modified state.
383: */
384: public void setModified(JGraphEditorFile file, boolean modified) {
385: if (file != null) {
386: file.setModified(modified);
387: nodeChanged(file);
388: }
389: }
390:
391: /**
392: * Returns the file for the specified filename if it is in the model or
393: * <code>null</code> if no such file exists.
394: *
395: * @param filename
396: * The filename to return the file for.
397: * @return Returns the file for <code>filename</code> or <code>null</code>.
398: */
399: public JGraphEditorFile getFileByFilename(String filename) {
400: int childCount = getChildCount(rootNode);
401: for (int i = 0; i < childCount; i++) {
402: Object child = getChild(rootNode, i);
403: if (child instanceof JGraphEditorFile) {
404: JGraphEditorFile file = (JGraphEditorFile) child;
405: if (file.getFilename() != null
406: && file.getFilename().equals(filename)) {
407: return file;
408: }
409: }
410: }
411: return null;
412: }
413:
414: /**
415: * Writes the specified object to the output stream using an xml encoder
416: * which was configured using {@link #configureEncoder(XMLEncoder)}. The
417: * exceptions that are thrown during encoding are caught by a local handler
418: * and passed to the caller as a RuntimeException with description of the
419: * encoding problems. <br>
420: * Note: You should use this method as a global hook to write all XML files.
421: *
422: * @param object
423: * The object to be written.
424: * @param out
425: * The output strem to write to.
426: *
427: * @throws RuntimeException
428: * If there are problems during encoding.
429: */
430: public void writeObject(Object object, OutputStream out) {
431: final List problems = new LinkedList();
432: if (object != null) {
433: XMLEncoder enc = new XMLEncoder(out);
434: enc.setExceptionListener(new ExceptionListener() {
435: public void exceptionThrown(Exception e) {
436: // Uncomment this line for debugging
437: // XML encoding:
438: e.printStackTrace();
439: problems.add(e);
440: }
441: });
442: configureEncoder(enc);
443: enc.writeObject(object);
444: enc.close();
445: }
446: if (!problems.isEmpty())
447: throw new RuntimeException(problems.size()
448: + " errors while writing " + object + " ("
449: + problems.get(0) + ")");
450: }
451:
452: /**
453: * Hook for subclassers to configure a new XML encoder for writing an
454: * object. This implementation sets all registered persistence delegates and
455: * installs default mappings for classes (eg. it assigns the list
456: * persistence delegates to array lists).
457: *
458: * @param enc
459: * The encoder to be configured.
460: */
461: protected void configureEncoder(XMLEncoder enc) {
462: Iterator it = persistenceDelegates.entrySet().iterator();
463: while (it.hasNext()) {
464: Map.Entry entry = (Map.Entry) it.next();
465: enc.setPersistenceDelegate((Class) entry.getKey(),
466: (PersistenceDelegate) entry.getValue());
467: }
468: enc.setPersistenceDelegate(ArrayList.class, enc
469: .getPersistenceDelegate(List.class));
470: }
471:
472: /**
473: * Hook for subclassers to create an input stream for the specified URI.
474: * This implementation creates an input stream using
475: * {@link JGraphEditorResources#getInputStream(String)} and wraps it in a
476: * {@link GZIPInputStream} if the URI ends with <code>.gz</code>.
477: *
478: * @param uri
479: * The URI to return the input stream for.
480: * @return Return an input stream for the specified URI.
481: */
482: public InputStream getInputStream(String uri)
483: throws MalformedURLException, FileNotFoundException,
484: IOException {
485: InputStream in = JGraphEditorResources.getInputStream(uri);
486: if (uri.toLowerCase().endsWith(".gz"))
487: in = new GZIPInputStream(in);
488: return new BufferedInputStream(in);
489: }
490:
491: /**
492: * Hook for subclassers to create an output stream for the specified URI.
493: * This implementation creates an output stream using
494: * {@link JGraphEditorResources#getOutputStream(String)} and wraps it in a
495: * {@link java.util.zip.GZIPOutputStream} if the URI ends with
496: * <code>.gz</code>.
497: *
498: * @param uri
499: * The URI to return the output stream for.
500: * @return Returns an output stream for the specified URI.
501: */
502: public OutputStream getOutputStream(String uri) throws IOException {
503: OutputStream out = JGraphEditorResources.getOutputStream(uri);
504: if (uri.toLowerCase().endsWith(".gz"))
505: out = new GZIPOutputStream(out);
506: return out;
507: }
508:
509: /**
510: * Returns a new object from the specified stream using a new XML decoder.
511: * This method does nothing special. Subclassers can override this method if
512: * they need to do anything special with opened files. <br>
513: * Note: You should use this method as a global hook to read all XML files.
514: *
515: * @return Returns a new object from the specified stream.
516: */
517: public Object readObject(InputStream in) {
518: XMLDecoder dec = new XMLDecoder(in);
519: if (dec != null) {
520: Object obj = dec.readObject();
521: dec.close();
522: return obj;
523: }
524: return null;
525: }
526:
527: /**
528: * Makes the specified field transient in the bean info of
529: * <code>clazz</code>.
530: *
531: * @param clazz
532: * The class whos field should be made transient.
533: * @param field
534: * The name of the field that should be made transient.
535: */
536: public static void makeTransient(Class clazz, String field) {
537: try {
538: BeanInfo info = Introspector.getBeanInfo(clazz);
539: PropertyDescriptor[] propertyDescriptors = info
540: .getPropertyDescriptors();
541: for (int i = 0; i < propertyDescriptors.length; ++i) {
542: PropertyDescriptor pd = propertyDescriptors[i];
543: if (pd.getName().equals(field)) {
544: pd.setValue("transient", Boolean.TRUE);
545: }
546: }
547: } catch (IntrospectionException e) {
548: // ignore
549: }
550: }
551:
552: /**
553: * Makes all fields but <code>cell</code> and <code>attributes</code>
554: * transient in the bean info of <code>clazz</code>.
555: *
556: * @param clazz
557: * The cell view class who fields should be made transient.
558: */
559: public static void makeCellViewFieldsTransient(Class clazz) {
560: try {
561: BeanInfo info = Introspector.getBeanInfo(clazz);
562: PropertyDescriptor[] propertyDescriptors = info
563: .getPropertyDescriptors();
564: for (int i = 0; i < propertyDescriptors.length; ++i) {
565: PropertyDescriptor pd = propertyDescriptors[i];
566: if (!pd.getName().equals("cell")
567: && !pd.getName().equals("attributes")) {
568: pd.setValue("transient", Boolean.TRUE);
569: }
570: }
571: } catch (IntrospectionException e) {
572: e.printStackTrace();
573: }
574: }
575:
576: /**
577: * Returns the parent file for <code>node</code> or the node itself, if it
578: * is a file. This method returns <code>null</code> if no parent file is
579: * found for <code>node</code>.
580: *
581: * @param node
582: * The node to find the parent file for.
583: * @return Returns the parent file for node, the node itself or
584: * <code>null</code>.
585: */
586: public static JGraphEditorFile getParentFile(TreeNode node) {
587: while (node != null) {
588: if (node instanceof JGraphEditorFile)
589: return (JGraphEditorFile) node;
590: node = node.getParent();
591: }
592: return null;
593: }
594:
595: }
|