001package jmri.jmrit.logix; 002 003import java.beans.PropertyChangeListener; 004import java.beans.PropertyChangeSupport; 005import java.util.ArrayList; 006import java.util.List; 007import jmri.Block; 008import jmri.InstanceManager; 009import jmri.NamedBean; 010import jmri.SignalHead; 011import jmri.SignalMast; 012import jmri.implementation.SignalSpeedMap; 013 014import javax.annotation.Nonnull; 015import javax.annotation.CheckForNull; 016import javax.annotation.OverridingMethodsMustInvokeSuper; 017 018/** 019 * A Portal is a boundary between two Blocks. 020 * <p> 021 * A Portal has Lists of the {@link OPath}s that connect through it. 022 * The direction of trains passing through the portal is managed from the 023 * BlockOrders of the Warrant the train is running under. 024 * The Portal fires a PropertyChangeEvent that a 025 * {@link jmri.jmrit.display.controlPanelEditor.PortalIcon} can listen 026 * for to set direction arrows for a given route. 027 * 028 * The Portal also supplies speed information from any signals set at its 029 * location that the Warrant passes on the Engineer. 030 * 031 * @author Pete Cressman Copyright (C) 2009 032 */ 033public class Portal { 034 035 private static final String NAME_CHANGE = "NameChange"; 036 private static final String SIGNAL_CHANGE = "signalChange"; 037 private static final String ENTRANCE = "entrance"; 038 private final ArrayList<OPath> _fromPaths = new ArrayList<>(); 039 private OBlock _fromBlock; 040 private NamedBean _fromSignal; // may be either SignalHead or SignalMast 041 private float _fromSignalOffset; // adjustment distance for speed change 042 private final ArrayList<OPath> _toPaths = new ArrayList<>(); 043 private OBlock _toBlock; 044 private NamedBean _toSignal; // may be either SignalHead or SignalMast 045 private float _toSignalOffset; // adjustment distance for speed change 046 private String _name; 047 private int _state = UNKNOWN; 048 private final PropertyChangeSupport pcs = new PropertyChangeSupport(this); 049 050 public static final int UNKNOWN = 0x01; 051 public static final int ENTER_TO_BLOCK = 0x02; 052 public static final int ENTER_FROM_BLOCK = 0x04; 053 054 public Portal(String uName) { 055 _name = uName; 056 } 057 058 /** 059 * Determine which list the Path belongs to and add it to that list. 060 * 061 * @param path OPath to add 062 * @return false if Path does not have a matching block for this Portal 063 */ 064 public boolean addPath(@Nonnull OPath path) { 065 Block block = path.getBlock(); 066 if (block == null) { 067 log.error("Path \"{}\" has no block.", path.getName()); 068 return false; 069 } 070 if (!this.equals(path.getFromPortal()) 071 && !this.equals(path.getToPortal())) { 072 return false; 073 } 074 if ((_fromBlock != null) && _fromBlock.equals(block)) { 075 return addPath(_fromPaths, path); 076 } else if ((_toBlock != null) && _toBlock.equals(block)) { 077 return addPath(_toPaths, path); 078 } 079 // portal is incomplete or path block not in this portal 080 return false; 081 } 082 083 /** 084 * Utility for both path lists. 085 * Checks for duplicate name. 086 */ 087 private boolean addPath(@Nonnull List<OPath> list, @Nonnull OPath path) { 088 String pName = path.getName(); 089 for (OPath p : list) { 090 if (p.equals(path)) { 091 if (pName.equals(p.getName())) { 092 return true; // OK, everything equal 093 } else { 094 log.warn("Path \"{}\" is duplicate of path \"{}\" in Portal \"{}\" from block {}.", 095 path.getName(), p.getName(), _name, path.getBlock().getDisplayName()); 096 return false; 097 } 098 } else if (pName.equals(p.getName())) { 099 log.warn("Path \"{}\" is duplicate name for another path in Portal \"{}\" from block {}.", 100 path.getName(), _name, path.getBlock().getDisplayName()); 101 return false; 102 } 103 } 104 list.add(path); 105 return true; 106 } 107 108 /** 109 * Remove an OPath from this Portal. 110 * Checks both the _fromBlock list as the _toBlock list. 111 * 112 * @param path the OPath to remove 113 */ 114 public void removePath(@Nonnull OPath path) { 115 Block block = path.getBlock(); 116 if (block == null) { 117 log.error("Path \"{}\" has no block.", path.getName()); 118 return; 119 } 120 log.debug("removePath: {}", this); 121 if (!this.equals(path.getFromPortal()) 122 && !this.equals(path.getToPortal())) { 123 return; 124 } 125 if (_fromBlock != null && _fromBlock.equals(block)) { 126 _fromPaths.remove(path); 127 } else if (_toBlock != null && _toBlock.equals(block)) { 128 _toPaths.remove(path); 129 } 130// pcs.firePropertyChange("RemovePath", block, path); not needed 131 } 132 133 /** 134 * Set userName of this Portal. Checks if name is available. 135 * 136 * @param newName name for path 137 * @return return error message, null if name change is OK 138 */ 139 @CheckForNull 140 public String setName(@CheckForNull String newName) { 141 if (newName == null || newName.length() == 0) { 142 return null; 143 } 144 String oldName = _name; 145 if (newName.equals(oldName)) { 146 return null; 147 } 148 Portal p = InstanceManager.getDefault(PortalManager.class).getPortal(newName); 149 if (p != null) { 150 return Bundle.getMessage("DuplicatePortalName", newName, p.getDescription()); 151 } 152 _name = newName; 153 InstanceManager.getDefault(WarrantManager.class).portalNameChange(oldName, newName); 154 155 // for some unknown reason, PortalManager firePropertyChange is not read by PortalTableModel 156 // so let OBlock do it 157 if (_toBlock != null) { 158 _toBlock.pseudoPropertyChange(NAME_CHANGE, oldName, this); 159 } else if (_fromBlock != null) { 160 _fromBlock.pseudoPropertyChange(NAME_CHANGE, oldName, this); 161 } 162 // CircuitBuilder PortalList needs this property change 163 pcs.firePropertyChange(NAME_CHANGE, oldName, newName); 164 return null; 165 } 166 167 public String getName() { 168 return _name; 169 } 170 171 /** 172 * Set this portal's toBlock. Remove this portal from old toBlock, if any. 173 * Add this portal in the new toBlock's list of portals. 174 * 175 * @param block to be the new toBlock 176 * @param changePaths if true, set block in paths. If false, 177 * verify that all toPaths are contained in the block. 178 * @return false if paths are not in the block 179 */ 180 public boolean setToBlock(@CheckForNull OBlock block, boolean changePaths) { 181 if (((block != null) && block.equals(_toBlock)) || ((block == null) && (_toBlock == null))) { 182 return true; 183 } 184 if (changePaths) { 185 // Switch paths to new block. User will need to verify connections 186 for (OPath opa : _toPaths) { 187 opa.setBlock(block); 188 } 189 } else if (!verify(_toPaths, block)) { 190 return false; 191 } 192 log.debug("setToBlock: oldBlock= \"{}\" newBlock \"{}\".", getToBlockName(), 193 (block != null ? block.getDisplayName() : null)); 194 OBlock oldBlock = _toBlock; 195 if (_toBlock != null) { 196 _toBlock.removePortal(this); // may should not 197 } 198 _toBlock = block; 199 if (_toBlock != null) { 200 _toBlock.addPortal(this); 201 } 202 pcs.firePropertyChange("BlockChanged", oldBlock, _toBlock); 203 return true; 204 } 205 206 public OBlock getToBlock() { 207 return _toBlock; 208 } 209 210 // @CheckForNull needs further dev 211 public String getToBlockName() { 212 return (_toBlock != null ? _toBlock.getDisplayName() : null); 213 } 214 215 public List<OPath> getToPaths() { 216 return _toPaths; 217 } 218 219 /** 220 * Set this portal's fromBlock. Remove this portal from old fromBlock, if any. 221 * Add this portal in the new toBlock's list of portals. 222 * 223 * @param block to be the new fromBlock 224 * @param changePaths if true, set block in paths. If false, 225 * verify that all toPaths are contained in the block. 226 * @return false if paths are not in the block 227 */ 228 public boolean setFromBlock(@CheckForNull OBlock block, boolean changePaths) { 229 if ((block != null && block.equals(_fromBlock)) || (block == null && _fromBlock == null)) { 230 return true; 231 } 232 if (changePaths) { 233 //Switch paths to new block. User will need to verify connections 234 for (OPath fromPath : _fromPaths) { 235 fromPath.setBlock(block); 236 } 237 } else if (!verify(_fromPaths, block)) { 238 return false; 239 } 240 log.debug("setFromBlock: oldBlock= \"{}\" newBlock \"{}\".", getFromBlockName(), 241 (block != null ? block.getDisplayName() : null)); 242 OBlock oldBlock = _fromBlock; 243 if (_fromBlock != null) { 244 _fromBlock.removePortal(this); 245 } 246 _fromBlock = block; 247 if (_fromBlock != null) { 248 _fromBlock.addPortal(this); 249 } 250 pcs.firePropertyChange("BlockChanged", oldBlock, _fromBlock); 251 return true; 252 } 253 254 public OBlock getFromBlock() { 255 return _fromBlock; 256 } 257 258 // @CheckForNull needs further dev 259 public String getFromBlockName() { 260 return (_fromBlock != null ? _fromBlock.getDisplayName() : null); 261 } 262 263 public List<OPath> getFromPaths() { 264 return _fromPaths; 265 } 266 267 /** 268 * Set a signal to protect an OBlock. Warrants look ahead for speed changes 269 * and change the train speed accordingly. 270 * 271 * @param signal either a SignalMast or a SignalHead. Set to null to remove (previous) signal from Portal 272 * @param length offset length in millimeters. This is additional 273 * entrance space for the block. This distance added to or subtracted 274 * from the calculation of the ramp distance when a warrant must slow 275 * the train in response to the aspect or appearance of the signal. 276 * @param protectedBlock OBlock the signal protects 277 * @return true if signal is set 278 */ 279 public boolean setProtectSignal(@CheckForNull NamedBean signal, float length, @CheckForNull OBlock protectedBlock) { 280 if (protectedBlock == null) { 281 return false; 282 } 283 boolean ret = false; 284 if ((_fromBlock != null) && _fromBlock.equals(protectedBlock)) { 285 _toSignal = signal; 286 _toSignalOffset = length; 287 log.debug("OPortal FromBlock Offset set to {} on signal {}", _toSignalOffset, 288 (_toSignal != null ? _toSignal.getDisplayName() : "<removed>")); 289 ret = true; 290 } 291 if ((_toBlock != null) && _toBlock.equals(protectedBlock)) { 292 _fromSignal = signal; 293 _fromSignalOffset = length; 294 log.debug("OPortal ToBlock Offset set to {} on signal {}", _fromSignalOffset, 295 (_fromSignal != null ? _fromSignal.getDisplayName() : "<removed>")); 296 ret = true; 297 } 298 if (ret) { 299 protectedBlock.pseudoPropertyChange(SIGNAL_CHANGE, false, true); 300 pcs.firePropertyChange(SIGNAL_CHANGE, false, true); 301 log.debug("setProtectSignal: \"{}\" for Block= {} at Portal {}", 302 (signal != null ? signal.getDisplayName() : "null"), protectedBlock.getDisplayName(), _name); 303 } 304 return ret; 305 } 306 307 /** 308 * Get the signal (either a SignalMast or a SignalHead) protecting an OBlock. 309 * 310 * @param block is the direction of entry, i.e. the protected block 311 * @return signal protecting block, if block is protected, otherwise null. 312 */ 313 @CheckForNull 314 public NamedBean getSignalProtectingBlock(@Nonnull OBlock block) { 315 if (block.equals(_toBlock)) { 316 return _fromSignal; 317 } else if (block.equals(_fromBlock)) { 318 return _toSignal; 319 } 320 return null; 321 } 322 323 /** 324 * Get the OBlock protected by a signal. 325 * 326 * @param signal is the signal, either a SignalMast or a SignalHead 327 * @return Protected OBlock, if it is protected, otherwise null. 328 */ 329 @CheckForNull 330 public OBlock getProtectedBlock(@CheckForNull NamedBean signal) { 331 if (signal == null) { 332 return null; 333 } 334 if (signal.equals(_fromSignal)) { 335 return _toBlock; 336 } else if (signal.equals(_toSignal)) { 337 return _fromBlock; 338 } 339 return null; 340 } 341 342 public NamedBean getFromSignal() { 343 return _fromSignal; 344 } 345 346 public String getFromSignalName() { 347 return (_fromSignal != null ? _fromSignal.getDisplayName() : null); 348 } 349 350 public float getFromSignalOffset() { 351 return _fromSignalOffset; // it seems clear that this method should return what is asks 352 } 353 354 public NamedBean getToSignal() { 355 return _toSignal; 356 } 357 358 @CheckForNull 359 public String getToSignalName() { 360 return (_toSignal != null ? _toSignal.getDisplayName() : null); 361 } 362 363 public float getToSignalOffset() { 364 return _toSignalOffset; 365 } 366 367 public void deleteSignal(@Nonnull NamedBean signal) { 368 if (signal.equals(_toSignal)) { 369 _toSignal = null; // set the 2 _tos 370 _toSignalOffset = 0; 371 if (_fromBlock != null) { 372 _fromBlock.pseudoPropertyChange(SIGNAL_CHANGE, false, false); 373 pcs.firePropertyChange(SIGNAL_CHANGE, false, false); 374 } 375 } else if (signal.equals(_fromSignal)) { 376 _fromSignal = null; // set the 2 _froms 377 _fromSignalOffset = 0; 378 if (_toBlock != null) { 379 _toBlock.pseudoPropertyChange(SIGNAL_CHANGE, false, false); 380 pcs.firePropertyChange(SIGNAL_CHANGE, false, false); 381 } 382 } 383 } 384 385 @CheckForNull 386 public static NamedBean getSignal(String name) { 387 NamedBean signal = InstanceManager.getDefault(jmri.SignalMastManager.class).getSignalMast(name); 388 if (signal == null) { 389 signal = InstanceManager.getDefault(jmri.SignalHeadManager.class).getSignalHead(name); 390 } 391 return signal; 392 } 393 394 /** 395 * Get the paths to the portal within the connected OBlock i.e. the paths in 396 * this (the param) block through the Portal. 397 * 398 * @param block OBlock 399 * @return null if portal does not connect to block 400 */ 401 // @CheckForNull requires further dev 402 public List<OPath> getPathsWithinBlock(@CheckForNull OBlock block) { 403 if (block == null) { 404 return null; 405 } 406 if (block.equals(_fromBlock)) { 407 return _fromPaths; 408 } else if (block.equals(_toBlock)) { 409 return _toPaths; 410 } 411 return null; 412 } 413 414 /** 415 * Get the OBlock on the other side of the Portal from the given 416 * OBlock. 417 * 418 * @param block starting OBlock 419 * @return the opposite block 420 */ 421 // @CheckForNull needs further dev 422 public OBlock getOpposingBlock(@Nonnull OBlock block) { 423 if (block.equals(_fromBlock)) { 424 return _toBlock; 425 } else if (block.equals(_toBlock)) { 426 return _fromBlock; 427 } 428 return null; 429 } 430 431 /** 432 * Get the paths from the portal in the next connected OBlock i.e. paths in 433 * the block on the other side of the portal from this (the param) block. 434 * 435 * @param block OBlock 436 * @return null if portal does not connect to block 437 */ 438 // @CheckForNull requires further dev 439 public List<OPath> getPathsFromOpposingBlock(@Nonnull OBlock block) { 440 if (block.equals(_fromBlock)) { 441 return _toPaths; 442 } else if (block.equals(_toBlock)) { 443 return _fromPaths; 444 } 445 return null; 446 } 447 448 /** 449 * Call is from BlockOrder when setting the path. 450 * 451 * @param block OBlock 452 */ 453 protected void setEntryState(@CheckForNull OBlock block) { 454 if (block == null) { 455 _state = UNKNOWN; 456 } else if (block.equals(_fromBlock)) { 457 setState(ENTER_FROM_BLOCK); 458 } else if (block.equals(_toBlock)) { 459 setState(ENTER_TO_BLOCK); 460 } 461 } 462 463 public void setState(int s) { 464 int old = _state; 465 _state = s; 466 pcs.firePropertyChange("Direction", old, _state); 467 } 468 469 public int getState() { 470 return _state; 471 } 472 473 @OverridingMethodsMustInvokeSuper 474 public synchronized void addPropertyChangeListener(PropertyChangeListener listener) { 475 pcs.addPropertyChangeListener(listener); 476 } 477 478 @OverridingMethodsMustInvokeSuper 479 public synchronized void removePropertyChangeListener(PropertyChangeListener listener) { 480 pcs.removePropertyChangeListener(listener); 481 } 482 483 /** 484 * Set the distance (plus or minus) in millimeters from the portal gap 485 * where the speed change indicated by the signal should be completed. 486 * 487 * @param block a protected OBlock 488 * @param distance length in millimeters, called Offset in the OBlock Signal Table 489 */ 490 public void setEntranceSpaceForBlock(@Nonnull OBlock block, float distance) { 491 if (block.equals(_toBlock)) { 492 if (_fromSignal != null) { 493 _fromSignalOffset = distance; 494 } 495 } else if (block.equals(_fromBlock)) { 496 if (_toSignal != null) { 497 _toSignalOffset = distance; 498 } 499 } 500 } 501 502 /** 503 * Get the distance (plus or minus) in millimeters from the portal gap 504 * where the speed change indicated by the signal should be completed. 505 * Property is called Offset in the OBlock Signal Table. 506 * 507 * @param block a protected OBlock 508 * @return distance 509 */ 510 public float getEntranceSpaceForBlock(@Nonnull OBlock block) { 511 if (block.equals(_toBlock)) { 512 if (_fromSignal != null) { 513 return _fromSignalOffset; 514 } 515 } else if (block.equals(_fromBlock)) { 516 if (_toSignal != null) { 517 return _toSignalOffset; 518 } 519 } 520 return 0; 521 } 522 523 /** 524 * Check signals, if any, for speed into/out of a given block. The signal that protects 525 * the "to" block is the signal facing the "from" Block, i.e. the "from" 526 * signal. (and vice-versa) 527 * 528 * @param block is the direction of entry, "from" block 529 * @param entrance true for EntranceSpeed, false for ExitSpeed 530 * @return permissible speed, null if no signal 531 */ 532 public String getPermissibleSpeed(@Nonnull OBlock block, boolean entrance) { 533 String speed = null; 534 String blockName = block.getDisplayName(); 535 if (block.equals(_toBlock)) { 536 if (_fromSignal != null) { 537 if (_fromSignal instanceof SignalHead) { 538 speed = getPermissibleSignalSpeed((SignalHead) _fromSignal, entrance); 539 } else { 540 speed = getPermissibleSignalSpeed((SignalMast) _fromSignal, entrance); 541 } 542 } 543 } else if (block.equals(_fromBlock)) { 544 if (_toSignal != null) { 545 if (_toSignal instanceof SignalHead) { 546 speed = getPermissibleSignalSpeed((SignalHead) _toSignal, entrance); 547 } else { 548 speed = getPermissibleSignalSpeed((SignalMast) _toSignal, entrance); 549 } 550 } 551 } else { 552 log.error("Block \"{}\" is not in Portal \"{}\".", blockName, _name); 553 } 554 if ( log.isDebugEnabled() && speed != null ) { 555 log.debug("Portal \"{}\" has {} speed= {} into \"{}\" from signal.", 556 _name, (entrance ? "ENTRANCE" : "EXIT"), speed, blockName); 557 } 558 // no signals, proceed at recorded speed 559 return speed; 560 } 561 562 /** 563 * Get entrance or exit speed set on signal head. 564 * 565 * @param signal signal head to query 566 * @param entrance true for EntranceSpeed, false for ExitSpeed 567 * @return permissible speed, Restricted if no speed set on signal 568 */ 569 private static @Nonnull String getPermissibleSignalSpeed(@Nonnull SignalHead signal, boolean entrance) { 570 int appearance = signal.getAppearance(); 571 String speed = InstanceManager.getDefault(SignalSpeedMap.class). 572 getAppearanceSpeed(signal.getAppearanceName(appearance)); 573 // on head, speed is the same for entry and exit 574 if (speed == null) { 575 log.error("SignalHead \"{}\" has no {} speed specified for appearance \"{}\"! - Restricting Movement!", 576 signal.getDisplayName(), (entrance ? ENTRANCE : "exit"), signal.getAppearanceName(appearance)); 577 speed = "Restricted"; 578 } 579 log.debug("SignalHead \"{}\" has {} speed notch= {} from appearance \"{}\"", 580 signal.getDisplayName(), (entrance ? ENTRANCE : "exit"), speed, signal.getAppearanceName(appearance)); 581 return speed; 582 } 583 584 /** 585 * Get entrance or exit speed set on signal mast. 586 * 587 * @param signal signal mast to query 588 * @param entrance true for EntranceSpeed, false for ExitSpeed 589 * @return permissible speed, Restricted if no speed set on signal 590 */ 591 private static @Nonnull String getPermissibleSignalSpeed(@Nonnull SignalMast signal, boolean entrance) { 592 String aspect = signal.getAspect(); 593 String signalAspect = ( aspect == null ? "" : aspect ); 594 String speed; 595 if (entrance) { 596 speed = InstanceManager.getDefault(SignalSpeedMap.class). 597 getAspectSpeed(signalAspect, signal.getSignalSystem()); 598 } else { 599 speed = InstanceManager.getDefault(SignalSpeedMap.class). 600 getAspectExitSpeed(signalAspect, signal.getSignalSystem()); 601 } 602 if (speed == null) { 603 log.error("SignalMast \"{}\" has no {} speed specified for aspect \"{}\"! - Restricting Movement!", 604 signal.getDisplayName(), (entrance ? ENTRANCE : "exit"), aspect); 605 speed = "Restricted"; 606 } 607 log.debug("SignalMast \"{}\" has {} speed notch= {} from aspect \"{}\"", 608 signal.getDisplayName(), (entrance ? ENTRANCE : "exit"), speed, aspect); 609 return speed; 610 } 611 612 /** 613 * Verify that each path has this potential block as its owning block. 614 * Block is a potential _toBlock and Paths are the current _toPaths 615 * or 616 * Block is a potential _fromBlock and Paths are the current _fromPaths 617 */ 618 private static boolean verify(@Nonnull List<OPath> paths, @CheckForNull OBlock block) { 619 if (block == null) { 620 return (paths.isEmpty()); 621 } 622 String name = block.getSystemName(); 623 for (OPath path : paths) { 624 Block blk = path.getBlock(); 625 if (blk == null) { 626 log.error("Path \"{}\" belongs to null block. Cannot verify set block to \"{}\"", 627 path.getName(), name); 628 return false; 629 } 630 String pathName = blk.getSystemName(); 631 if (!pathName.equals(name)) { 632 log.warn("Path \"{}\" belongs to block \"{}\". Cannot verify set block to \"{}\"", 633 path.getName(), pathName, name); 634 return false; 635 } 636 } 637 return true; 638 } 639 640 /** 641 * Check if path connects to Portal. 642 * 643 * @param path OPath to test 644 * @return true if valid 645 */ 646 public boolean isValidPath(@Nonnull OPath path) { 647 String name = path.getName(); 648 for (OPath toPath : _toPaths) { 649 if (toPath.getName().equals(name)) { 650 return true; 651 } 652 } 653 for (OPath fromPath : _fromPaths) { 654 if (fromPath.getName().equals(name)) { 655 return true; 656 } 657 } 658 return false; 659 } 660 661 /** 662 * Check portal has both blocks and they are different blocks. 663 * 664 * @return true if valid 665 */ 666 public boolean isValid() { 667 if (_toBlock == null || _fromBlock==null) { 668 return false; 669 } 670 return (!_toBlock.equals(_fromBlock)); 671 } 672 673 @OverridingMethodsMustInvokeSuper 674 public boolean dispose() { 675 if (!InstanceManager.getDefault(WarrantManager.class).okToRemovePortal(this)) { 676 return false; 677 } 678 if (_toBlock != null) { 679 _toBlock.removePortal(this); 680 } 681 if (_fromBlock != null) { 682 _fromBlock.removePortal(this); 683 } 684 pcs.firePropertyChange("portalDelete", true, false); 685 PropertyChangeListener[] listeners = pcs.getPropertyChangeListeners(); 686 for (PropertyChangeListener l : listeners) { 687 pcs.removePropertyChangeListener(l); 688 } 689 return true; 690 } 691 692 public String getDescription() { 693 return Bundle.getMessage("PortalDescription", 694 _name, getFromBlockName(), getToBlockName()); 695 } 696 697 @Override 698 @Nonnull 699 public String toString() { 700 StringBuilder sb = new StringBuilder("Portal \""); 701 sb.append(_name); 702 sb.append("\" from block \""); 703 sb.append(getFromBlockName()); 704 sb.append("\" to block \""); 705 sb.append(getToBlockName()); 706 sb.append("\""); 707 return sb.toString(); 708 } 709 710 private static final org.slf4j.Logger log = org.slf4j.LoggerFactory.getLogger(Portal.class); 711 712}