001    /*
002     * Node.java
003     *
004     * Developed for the "Rethinking CS101" project. See http://www.cs101.org, the
005     * CS101 homepage or email las@olin.edu.
006     *
007     * Please do not redistribute without obtaining permission.
008     */
009    
010    package nodenet;
011    
012    //import com.sun.java.swing.*;
013    import javax.swing.*;
014    import java.util.*;
015    import java.io.*;
016    import java.awt.*;
017    
018    import java.beans.PropertyVetoException;
019    import java.awt.image.BufferedImage;
020    import java.awt.font.FontRenderContext;
021    import java.awt.font.TextLayout;
022    import java.awt.geom.Rectangle2D;
023    
024    
025    /**
026     * This class implements NodeNetElement.  It is responsible for the
027     * GUI and maintains its own thread.  It relies on a class that
028     * implements NodeBehavior to do anything.  It repeatedly calls that
029     * class's act method from within a <code>while(true) {}
030     * loop.</code></p>
031     *
032     * <p>Note: the following methods are synchronized to prevent addition
033     * or subtraction of a channel during
034     * {@link nodenet.NodeBehavior#transmitPacket(InputChannelVector, OutputChannelVector)}
035     *
036     * <ul>
037     *   <li><code>public void addInputChannel(InputChannel c)</code>
038     *   <li><code>public void removeInputChannel(InputChannel c)</code>
039     *   <li><code>public void addOutputChannel(OutputChannel c)</code>
040     *   <li><code>public void removeOutputChannel(OutputChannel c)</code>
041     *   <li><code>public void run()</code>
042     *   <li>
043     * </ul>
044     *
045     * This is a change from earlier versions of nodeNet where students
046     * could have written code that never returned from the transmitPacket call
047     * (formerly known as act).
048     *
049     * @see NodeNetElement
050     * @see NodeBehavior
051     * @author Todd C. Parnell, tparnell@ai.mit.edu
052     * @author Patrick G. Heck, gus.heck@olin.edu
053     * @version $Id: Node.java,v 1.15 2004/01/14 21:43:17 gus Exp $
054     */
055    public class Node implements NodeNetElement, Resettable, Serializable {
056        
057        /** Diameter of the circle used to paint nodes. */
058        public final static int GUISIZE = 20;
059        public final static Color HIGHLIGHT_COLOR = Color.black;
060        
061        private final static BufferedImage dummyImage = makeImage(1,1);
062        
063        // This is needed for the stupid image observer stuff.
064        private final static Panel dummyPanel = new Panel();
065        
066        private static Color[] COLORS = {
067            Color.black, Color.blue, Color.gray, Color.green, Color.red,
068            Color.yellow, Color.pink
069        };
070        
071        /**
072         * mapping between NodeBehaviors and Colors.  needs to be static so
073         * Nodes can be serialized.
074         */
075        private static Hashtable nbToColorMap;
076        static { Node.nbToColorMap = new Hashtable(); }
077        
078        final static int SLEEP_INTERVAL = 100;
079        final static int DISABLED_SLEEP_INTERVAL = 1000;
080        
081        private static int ID;
082        transient private final int myID = Node.ID++;
083        
084        transient private Thread spirit;
085        
086        
087        transient private Color myColor;
088        private Point pos;
089        transient boolean selected;             // highlighted in GUI?
090        private boolean enabled = true;         // calling act?
091        private boolean destroyed;              // see note below
092        private boolean stopped = true;         // signal to Thread in run()
093        private String name;
094        transient private NodeBehavior myBehavior;
095        private String myBehaviorStr;
096        private InputChannelVector inputChannels;
097        private OutputChannelVector outputChannels;
098        
099        private NodeConfigurationBean myConfig;
100        
101        transient private Object lock = new Object(); // see note below
102        
103        //
104        // Static methods
105        //
106        
107        /**
108         * Returns the current Color associated with the given NodeBehavior.
109         * If a mapping does not currently exist, a new one is created.
110         *
111         * @param behavior the NodeBehavior whose Color will be returned
112         */
113        public static Color getColorFromBehavior(String behavior) {
114            if (!Node.nbToColorMap.containsKey(behavior)) {
115                Node.nbToColorMap.put(behavior,
116                Node.COLORS[Node.nbToColorMap.size()]);
117            }
118            return (Color)Node.nbToColorMap.get(behavior);
119        }
120        
121        /**
122         * Gets an Enumeration of all currently known NodeBehaviors.
123         */
124        public static Enumeration getBehaviors() {
125            return Node.nbToColorMap.keys();
126        }
127        
128        private static BufferedImage makeImage(int x, int y) {
129            return     GraphicsEnvironment.
130            getLocalGraphicsEnvironment().
131            getDefaultScreenDevice().
132            getDefaultConfiguration().
133            createCompatibleImage(x,y);
134            
135        }
136        
137        //
138        // Constructors
139        //
140        
141        public Node(NodeBehavior nb) {
142            this.myConfig = new NodeConfigurationBean();
143            
144            myConfig.getRegistrar().nameAndRegister(this);
145            
146            this.name = "*UNNAMED NODE*";
147            this.inputChannels = new InputChannelVector(this);
148            this.outputChannels = new OutputChannelVector(this);
149            this.myColor = getColorFromBehavior(nb.getClass().getName());
150            this.myBehavior = nb;
151            this.myBehaviorStr = this.myBehavior.getClass().getName();
152        }
153        
154        /**
155         * Build a node setting various properties. This is used when recreating
156         * a network from a saved template. ID can't be set so a map of saved ID
157         * to created nodes must be kept during parsing so that channels can find
158         * their nodes.
159         */
160        public Node(Class nbc, int x, int y, String name, boolean showName,
161        boolean enabled) {
162            NodeBehavior nb = null;
163            try {
164                nb = (NodeBehavior)nbc.newInstance();
165            } catch (InstantiationException ie) {
166                // this should never happen, due to constructor checks
167                throw new RuntimeException("InstantiationException in" +
168                " createNodeType.newInstance()");
169            } catch (IllegalAccessException iae) {
170                // should never happen, due to constructor checks
171                throw new RuntimeException("IllegalAccessException in" +
172                " createNodeType.newInstance()");
173            }
174            
175            this.myConfig = new NodeConfigurationBean();
176            
177            myConfig.getRegistrar().nameAndRegister(this);
178            
179            this.name = "*UNNAMED NODE*";
180            this.inputChannels = new InputChannelVector(this);
181            this.outputChannels = new OutputChannelVector(this);
182            this.myColor = getColorFromBehavior(nb.getClass().getName());
183            this.myBehavior = nb;
184            this.myBehaviorStr = this.myBehavior.getClass().getName();
185            setPos(x, y);
186            setName(name);
187            this.myConfig.setDisplayNodeName(showName);
188        }
189        
190        //
191        // Public methods
192        //
193        
194        public void setPos(Point p) { this.pos = p; }
195        public void setPos(int a, int b) { this.setPos(new Point(a, b)); }
196        public Point getPos() { return(this.pos); }
197        
198        public void setSelected(boolean newState) { this.selected = newState; }
199        public boolean isSelected() { return(this.selected); }
200        
201        public void setEnabled(boolean b) { this.enabled = b; }
202        public boolean isEnabled() { return this.enabled; }
203        
204        public NodeConfigurationBean getConfig() { return myConfig; }
205        
206        public void setName(String n) {
207            try {
208                myConfig.setNodeName(n);
209            } catch (PropertyVetoException pve) {
210                JOptionPane.showMessageDialog(null,
211                "Invalid Name:" + pve.getMessage(),
212                "Node Naming Error",
213                JOptionPane.ERROR_MESSAGE);
214            }
215        }
216        
217        public String getName() {
218            return myConfig.getNodeName();
219        }
220        
221        public boolean contains(Point p) { return(this.contains(p.x, p.y)); }
222        public boolean contains(int x, int y) {
223            return ((x > (this.pos.x - GUISIZE)) && (x < (this.pos.x + GUISIZE)) &&
224            (y > (this.pos.y - GUISIZE)) && (y < (this.pos.y + GUISIZE)));
225        }
226        
227        public boolean isEditable() { return false; }
228        
229        
230        public void configure() {
231            Object[] concurrentMessage =
232            { "Concurrent modification detected!",
233              "Please try again."};
234              
235              boolean configured = false;
236              while (!configured) {
237                  myConfig.refreshComponents();
238                  //System.out.println(myConfig.getNodeName());
239                  int result = JOptionPane.showConfirmDialog(null,myConfig,"Node Configuration",JOptionPane.OK_CANCEL_OPTION);
240                  if (result == JOptionPane.OK_OPTION) {
241                      try {
242                          myConfig.implementUserChanges();
243                          configured = true;
244                      } catch (PropertyVetoException pve) {
245                          JOptionPane.showMessageDialog(null, pve.getMessage());
246                      } catch (ConcurrentModificationException cme) {
247                          JOptionPane.showMessageDialog(null, concurrentMessage);
248                      }
249                  } else {
250                      configured = true;
251                  }
252              }
253        }
254        
255        
256        /**
257         * Adds a new InputChannel.  If the specified InputChannel already
258         * exists (as determined by the <code>equals</code> method), it is
259         * not added again.  This condition is silently ignored.
260         *
261         * @param c the InputChannel to connect
262         */
263        public void addInputChannel(InputChannel c) {
264            synchronized (this.lock) {
265                if (!this.inputChannels.contains(c)) {
266                    this.inputChannels.addElement(c);
267                }
268            }
269        }
270        
271        /**
272         * Removes an InputChannel.  If the specified InputChannel is not a
273         * current InputChannel for this Node (as determined by the
274         * <code>equals</code> method), this method does nothing.
275         *
276         * @param c the InputChannel to disconnect
277         */
278        public void removeInputChannel(InputChannel c) {
279            synchronized (this.lock) {
280                boolean b = this.inputChannels.removeElement(c);
281            }
282        }
283        
284        /**
285         * Adds a new OutputChannel.  If the specified OutputChannel already
286         * exists (as determined by the <code>equals</code> method), it is
287         * not added again.  This condition is silently ignored.
288         *
289         * @param c the OutputChannel to connect
290         */
291        public void addOutputChannel(OutputChannel c) {
292            synchronized (this.lock) {
293                if (!this.outputChannels.contains(c)) {
294                    this.outputChannels.addElement(c);
295                }
296            }
297        }
298        
299        /**
300         * Removes an OutputChannel.  If the specified OutputChannel is not a
301         * current OutputChannel for this Node (as determined by the
302         * <code>equals</code> method), this method does nothing.
303         *
304         * @param c the OutputChannel to disconnect
305         */
306        public void removeOutputChannel(OutputChannel c) {
307            synchronized (this.lock) {
308                boolean b = this.outputChannels.removeElement(c);
309            }
310        }
311        
312         /*
313          * destroy() and notifyOfDestruction() use the destroyed field to
314          * prevent cycles of destruction notifications to occur.  Use of the
315          * field feels like a hack, but due to the distributed nature of
316          * this app, is a good solution.
317          */
318        
319        /**
320         * Destroy the Node.  <code>destroy()</code> should be called
321         * whenever a Node will no longer be used.  When destroyed, Nodes
322         * notify all connected input and output channels of the impending
323         * destruction and remove refrences to those channels.  Typically,
324         * this will force their destruction as well.  <I>Note:
325         * <code>Node.finalize()</code> automatically calls
326         * <code>destroy()</code></I>.
327         */
328        public void destroy() {
329            this.stop();
330            this.destroyed = true; //eliminate concurrency problems
331            synchronized (this.lock) {
332                //tell channels we're going away
333                Enumeration enum = this.inputChannels.elements();
334                while (enum.hasMoreElements()) {
335                    ((Channel)enum.nextElement()).notifyOfDestruction((NodeNetElement)this);
336                }
337                
338                enum = this.outputChannels.elements();
339                while (enum.hasMoreElements()) {
340                    ((Channel)enum.nextElement()).notifyOfDestruction((NodeNetElement)this);
341                }
342                this.myConfig.getRegistrar().unregister(this);
343            }
344        }
345        
346        public boolean isDestroyed() { return this.destroyed; }
347        
348        /** Getter for property myID.
349         * @return Value of property myID.
350         *
351         */
352        public int getMyID() {
353            return myID;
354        }
355        
356        /**
357         * Generate an xml element representing a template of this node. Active
358         * state such as packet counts should not be preserved. Only information
359         * that is needed to recreate a network in it's pristine state
360         * (i.e. before the user starts the simulation) should be included.
361         *
362         * @return String An xml representiation of the node.
363         */
364        public String templateXML() {
365            return
366            "<node id=\"node" + this.myID +
367            "\" x=\"" + this.getPos().x +
368            "\" y=\"" + this.getPos().y +
369            "\" type=\"" + this.myBehavior.getClass().getName() +
370            "\" enabled=\"" + this.enabled +
371            "\" name=\"" + this.getName() +
372            "\" showName=\"" + this.getConfig().getDisplayNodeName() +
373            "\"/>";
374        }
375        
376        /**
377         * Notify this that a NodeNetElement has been destroyed.  Typically,
378         * this method will be called by an input or output channel
379         * connected to the node.  Once notified of destruction, all
380         * references to the element are removed.
381         */
382        public void notifyOfDestruction(NodeNetElement bse) {
383            // first see whether we've already been destroyed
384            if (this.destroyed) return;
385            if (bse instanceof InputChannel) {
386                this.removeInputChannel( (InputChannel)bse );
387            }
388            if (bse instanceof OutputChannel) {
389                this.removeOutputChannel( (OutputChannel)bse );
390            }
391        }
392        
393        /*
394         * Render text onto a BufferedImage so that we can know it's size before
395         * drawing it in the paint method.
396         */
397        private BufferedImage renderedText(String str, Font font, Color color, Color bgColor, int border) {
398            Graphics2D G = dummyImage.createGraphics();
399            FontRenderContext context = G.getFontRenderContext();
400            TextLayout layout = new TextLayout(str,font,context);
401            
402            Rectangle2D textBounds = layout.getBounds();
403            double width = textBounds.getWidth();
404            double height = textBounds.getHeight();
405            
406            width += 3; // fudge factor... getBounds seems to be off by a little bit
407            height++;
408            
409            int intWidth = new Double(width).intValue();
410            int intHeight = new Double(height).intValue();
411            
412            //intHeight = Math.max(intHeight,new Float(layout.getAscent()+layout.getDescent()).intValue());
413            
414            intWidth += 2*border;
415            intHeight += 2*border;
416            
417            BufferedImage rend = makeImage(intWidth,intHeight);
418            
419            G = rend.createGraphics();
420            
421            G.setPaint(bgColor);
422            
423            G.fill(new Rectangle2D.Double(0,0,rend.getWidth(),rend.getHeight()));
424            
425            G.setColor(color);
426            G.setBackground(bgColor);
427            
428            layout.draw(G,border,intHeight-(border+layout.getDescent()/2));
429            
430            return rend;
431        }
432        
433        public void paint(Graphics g) {
434            if (this.destroyed || this.pos == null) return;
435            // be polite & save current color
436            Color tempColor = g.getColor();
437            g.setColor(this.myColor);
438            g.fillOval(this.pos.x - GUISIZE / 2, this.pos.y - GUISIZE / 2,
439            GUISIZE, GUISIZE);
440            if (this.selected) {
441                g.setColor(HIGHLIGHT_COLOR);
442                g.drawRect(this.pos.x - GUISIZE / 2, this.pos.y - GUISIZE / 2,
443                GUISIZE, GUISIZE);
444            }
445            if (!this.enabled) {
446                g.setColor(HIGHLIGHT_COLOR);
447                g.drawLine(this.pos.x - GUISIZE / 2 - 5, this.pos.y - GUISIZE / 2 - 5,
448                this.pos.x + GUISIZE / 2 + 5, this.pos.y + GUISIZE / 2 + 5);
449            }
450            
451            if (this.myBehavior instanceof Counter) {
452                String s = "" + ((Counter)this.myBehavior).getCount();
453                g.setColor(Color.black);
454                g.drawString(s, this.pos.x - GUISIZE, this.pos.y + GUISIZE);
455            }
456            
457            if (this.myConfig.getDisplayNodeName()) {
458                BufferedImage rendName = renderedText(this.getName(),
459                g.getFont(),Color.black, Color.white, 3);
460                int x = this.pos.x - rendName.getWidth()/2;
461                int y = this.pos.y - ((GUISIZE / 2) + 1 + rendName.getHeight());
462                
463                g.drawImage(rendName,x,y,dummyPanel);
464                
465                
466            }
467            // restore color
468            g.setColor(tempColor);
469        }
470        
471        /**
472         * Starts the node running.  Once running, a node will repeatedly
473         * call the <code>act</code> method for it's associated
474         * NodeBehavior.
475         */
476        public void start() {
477            if (this.spirit == null) {
478                this.stopped = false;
479                this.spirit = new Thread(this);
480                this.spirit.start();
481            }
482        }
483        
484        /**
485         * Stops the node.
486         */
487        public void stop()  {
488            if (this.spirit == null) return;
489            this.stopped = true;  // signal spirit
490            // now wait until spirit has exited before returning
491            while (this.spirit.isAlive()) {
492                try { Thread.sleep(SLEEP_INTERVAL); }
493                catch (InterruptedException ie) {
494                    // Do nothing...
495                }
496            }
497            this.spirit = null;     // help the gc
498        }
499        
500        /**
501         * Repeatedly calls the <code>transmitPacket</code> method for the
502         * associated NodeBehavior.  <I>Note: This method should never be
503         * called directly.  Instead, use the <code>start()</code> method.</I>
504         */
505        public void run() {
506            while(!this.stopped) {
507                try {
508                    if (this.enabled) Thread.sleep(SLEEP_INTERVAL);
509                    else Thread.sleep(DISABLED_SLEEP_INTERVAL);
510                } catch(InterruptedException e) {}
511                
512                if (this.enabled && this.myBehavior != null) {
513                    synchronized (this.lock) {
514                        this.myBehavior.transmitPacket(this.inputChannels, this.outputChannels);
515                    }
516                }
517            }
518        } // run()
519        
520        public boolean equals(Object o) {
521            if (!(o instanceof Node)) return false;
522            Node n = (Node)o;
523            return (n.myID == this.myID);
524        }
525        
526        //
527        // Protected instance methods
528        //
529        
530        protected void finalize() throws IOException {
531            this.destroy();
532            
533        }
534        
535        //
536        // Private methods
537        //
538        
539        /**
540         * Custom de-serialization.  Get NodeBehavior & color, then start
541         * Thread.
542         */
543        private void readObject(ObjectInputStream in)
544        throws IOException, ClassNotFoundException {
545            in.defaultReadObject();
546            this.lock = new Object();
547            try {
548                this.myBehavior =
549                (NodeBehavior)Class.forName(this.myBehaviorStr).newInstance();
550                this.myColor = Node.getColorFromBehavior(this.myBehaviorStr);
551            } catch (Exception e) {
552                System.err.println("Error getting specified behavior. '" +
553                this.myBehaviorStr + "'.");
554            }
555            try {
556                this.myConfig.getRegistrar().register(this);
557            } catch (IllegalArgumentException iae) {
558                JOptionPane.showMessageDialog(null,"The name "+myConfig.getNodeName()+" exists, renaming the new one");
559                this.myConfig.getRegistrar().nameAndRegister(this);
560            }
561        }
562        
563        public void reset() {
564            this.selected = false;
565            if (this.myBehavior instanceof Counter) {
566                ((Counter)this.myBehavior).resetCount();
567            }
568            if (this.myBehavior instanceof Resettable) {
569                ((Resettable)this.myBehavior).reset();
570            }
571        }
572        
573        
574    } // class Node
575    
576    /*
577     * $Log: Node.java,v $
578     * Revision 1.15  2004/01/14 21:43:17  gus
579     * more javadoc, plus reformat
580     *
581     * Revision 1.14  2004/01/14 21:04:16  gus
582     * More javadoc fixes
583     *
584     * Revision 1.13  2004/01/14 20:23:21  gus
585     * Javadoc and comment cleanup
586     *
587     * Revision 1.12  2004/01/13 19:35:27  gus
588     * Simulation Panel, channel and node are all Resettable now.
589     * CountingNodeBehavior refactored into Counter
590     *
591     * Revision 1.11  2004/01/08 22:48:08  gus
592     * Can now read xml template file (only tested breifly)
593     *
594     * Revision 1.10  2004/01/08 19:35:09  gus
595     * further tweaks to get the xml template to contain type info.
596     *
597     * Revision 1.9  2004/01/08 15:46:50  gus
598     * eliminate needless color argument in constructor.
599     * Generate xml template
600     * Convenience constructor to match template.
601     *
602     * Revision 1.8  2003/02/25 21:47:42  gus
603     * register nodes as they are deserialized, and unregister nodes when they are destroyed
604     *
605     * Revision 1.7  2003/02/24 16:06:16  gus
606     * let the world find out what our configuration is.
607     *
608     * Revision 1.6  2003/02/24 16:04:15  gus
609     * Make input and output channel vectors aware of their owners, and able to report
610     * who their owner is.
611     *
612     * Revision 1.5  2003/02/21 22:41:54  gus
613     * Nodes now have configureable displayable names
614     *  Modified Files:
615     *      nodenet/Node.java nodenet/NodeConfigurationBean.form
616     *      nodenet/NodeConfigurationBean.java
617     *      nodenet/registrar/UniqueNodeNamePolicy.java
618     *
619     * Revision 1.4  2003/02/21 18:15:36  gus
620     * Reformat and make use of NodeConfigurationBean
621     *
622     * Revision 1.3  2003/02/14 22:19:47  gus
623     * reformatted
624     *
625     * Revision 1.2  2002/06/13 19:32:43  gus
626     * Commented out com.sun.java.swing package references, and updated obsolete
627     * imorts and references into cs101 package.
628     *
629     * Revision 1.1  2002/06/13 17:33:21  gus
630     * Moved all java files into the nodenet directory (who let them out anyway?)
631     *
632     * Revision 1.1.1.1  2002/06/05 21:56:35  root
633     * CS101 comes to Olin finally.
634     *
635     * Revision 1.4  2000/05/09 06:03:54  mharder
636     * Changed packagename from nodeNet to nodenet.
637     *
638     * Revision 1.3  1999/08/04 09:08:53  jsmthng
639     * Added javadoc comments to InputChannelVector and OutputChannelVector;
640     * finished updating the rest of the nodeNet package to reflect new
641     * changes in name and code.
642     *
643     * Modified index.html to reflect the new nodeNet code, as well as to
644     * clarify some parts of the problem set.
645     *
646     * Revision 1.1  1999/07/30 01:09:22  jsmthng
647     * Renaming BinSort package to nodeNet; moving directories and files as
648     * necessary.
649     *
650     * Revision 1.7  1999/01/21 20:57:06  tparnell
651     * it worked
652     *
653     * Revision 1.6  1998/08/12 19:29:38  tparnell
654     * Another pass after comments from las & natashao.  Added support to
655     * dynamically add NodeBehaviors.  Add keyboard shortcuts to menus.  Added
656     * workaround to jdk bug relating to lighweight components.  Misc other
657     * bugfixes.
658     *
659     * Revision 1.3  1998/08/10 17:45:50  tparnell
660     * Revision to use JDK1.2 and Swing.  Redesign of GUI.  Removed old kludge
661     * for file I/O and replaced with object serialization.  Channel no longer
662     * requires animacy.  Removed unnessary dependencies between classes.
663     * Added ability to configure channel's latency and capacity.  Added
664     * javadoc to all files.  General code cleanup.
665     *
666     */
667    
668    
669