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