001package jmri; 002 003import java.beans.PropertyChangeEvent; 004import java.beans.PropertyChangeListener; 005import java.beans.PropertyVetoException; 006import java.util.ArrayList; 007import java.util.List; 008import java.util.Locale; 009import java.util.Objects; 010 011import javax.annotation.CheckForNull; 012import javax.annotation.CheckReturnValue; 013import javax.annotation.Nonnull; 014 015import jmri.beans.PropertyChangeProvider; 016 017/** 018 * Provides common services for classes representing objects on the layout, and 019 * allows a common form of access by their Managers. 020 * <p> 021 * Each object has two types of names: 022 * <p> 023 * The "system" name is provided by the system-specific implementations, and 024 * provides a unique mapping to the layout control system (for example LocoNet 025 * or NCE) and address within that system. It must be present and unique across 026 * the JMRI instance. Two beans are identical if they have the same system name; 027 * if not, not. 028 * <p> 029 * The "user" name is optional. It's free form text except for two restrictions: 030 * <ul> 031 * <li>It can't be the empty string "". (A non-existant user name is coded as a 032 * null) 033 * <li>And eventually, we may insist on normalizing user names to a specific 034 * form, e.g. remove leading and trailing white space; see the 035 * {@link #normalizeUserName(java.lang.String)} method 036 * </ul> 037 * <p> 038 * Each of these two names must be unique for every NamedBean of the same type 039 * on the layout and a single NamedBean cannot have a user name that is the same 040 * as the system name of another NamedBean of the same type. (The complex 041 * wording is saying that a single NamedBean object is allowed to have its 042 * system name and user name be the same, but that's the only non-uniqueness 043 * that's allowed within a specific type). Note that the uniqueness restrictions 044 * are currently not completely enforced, only warned about; a future version of 045 * JMRI will enforce this restriction. 046 * <p> 047 * For more information, see the 048 * <a href="http://jmri.org/help/en/html/doc/Technical/Names.shtml">Names and 049 * Naming</a> page in the 050 * <a href="http://jmri.org/help/en/html/doc/Technical/index.shtml">Technical 051 * Info</a> pages. 052 * <hr> 053 * This file is part of JMRI. 054 * <p> 055 * JMRI is free software; you can redistribute it and/or modify it under the 056 * terms of version 2 of the GNU General Public License as published by the Free 057 * Software Foundation. See the "COPYING" file for a copy of this license. 058 * <p> 059 * JMRI is distributed in the hope that it will be useful, but WITHOUT ANY 060 * WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR 061 * A PARTICULAR PURPOSE. See the GNU General Public License for more details. 062 * 063 * @author Bob Jacobsen Copyright (C) 2001, 2002, 2003, 2004 064 * @see jmri.Manager 065 */ 066public interface NamedBean extends Comparable<NamedBean>, PropertyChangeProvider { 067 068 /** 069 * Constant representing an "unknown" state, indicating that the object's 070 * state is not necessarily that of the actual layout hardware. This is the 071 * initial state of a newly created object before communication with the 072 * layout. 073 */ 074 static final int UNKNOWN = 0x01; 075 076 /** 077 * Constant representing an "inconsistent" state, indicating that some 078 * inconsistency has been detected in the hardware readback. 079 */ 080 static final int INCONSISTENT = 0x08; 081 082 /** 083 * Format used for {@link #getDisplayName(DisplayOptions)} when displaying 084 * the user name and system name without quoation marks around the user 085 * name. 086 */ 087 final static String DISPLAY_NAME_FORMAT = "%s (%s)"; 088 089 /** 090 * Format used for {@link #getDisplayName(DisplayOptions)} when displaying 091 * the user name and system name with quoation marks around the user name. 092 */ 093 final static String QUOTED_NAME_FORMAT = "\"%s\" (%s)"; 094 095 /** 096 * Property of changed state. 097 */ 098 final static String PROPERTY_STATE = "state"; 099 100 /** 101 * User's identification for the item. Bound parameter so manager(s) can 102 * listen to changes. Any given user name must be unique within the layout. 103 * Must not match the system name. 104 * 105 * @return null if not set 106 */ 107 @CheckReturnValue 108 @CheckForNull 109 String getUserName(); 110 111 /** 112 * Set the user name, normalizing it if needed. 113 * 114 * @param s the new user name 115 * @throws jmri.NamedBean.BadUserNameException if the user name can not be 116 * normalized 117 */ 118 void setUserName(@CheckForNull String s) throws BadUserNameException; 119 120 /** 121 * Get a system-specific name. This encodes the hardware addressing 122 * information. Any given system name must be unique within the layout. 123 * 124 * @return the system-specific name 125 */ 126 @CheckReturnValue 127 @Nonnull 128 String getSystemName(); 129 130 /** 131 * Display the system-specific name. 132 * <p>Note that this is a firm contract: toString() in 133 * all implementing classes must return the system name 134 * followed by optional additional information. 135 * Using code can assume that the result of toString() will always be 136 * or start with the system name followed by some kind of separator character. 137 * 138 * @return the system-specific name 139 */ 140 @Nonnull 141 @Override 142 String toString(); 143 144 /** 145 * Get user name if it exists, otherwise return System name. 146 * 147 * @return the user name or system-specific name 148 */ 149 @CheckReturnValue 150 @Nonnull 151 default String getDisplayName() { 152 return getDisplayName(DisplayOptions.DISPLAYNAME); 153 } 154 155 /** 156 * Get the name to display, formatted per {@link NamedBean.DisplayOptions}. 157 * 158 * @param options the DisplayOptions to use 159 * @return the display name formatted per options 160 */ 161 @CheckReturnValue 162 @Nonnull 163 default String getDisplayName(DisplayOptions options) { 164 String userName = getUserName(); 165 String systemName = getSystemName(); 166 // since there are two undisplayable states for the user name, 167 // empty or null, if user name is empty, make it null to avoid 168 // repeatedly checking for both those states later 169 if (userName != null && userName.isEmpty()) { 170 userName = null; 171 } 172 switch (options) { 173 case USERNAME_SYSTEMNAME: 174 return userName != null ? String.format(DISPLAY_NAME_FORMAT, userName, systemName) : systemName; 175 case QUOTED_USERNAME_SYSTEMNAME: 176 return userName != null ? String.format(QUOTED_NAME_FORMAT, userName, systemName) : getDisplayName(DisplayOptions.QUOTED_SYSTEMNAME); 177 case SYSTEMNAME: 178 return systemName; 179 case QUOTED_SYSTEMNAME: 180 return String.format("\"%s\"", systemName); 181 case QUOTED_USERNAME: 182 case QUOTED_DISPLAYNAME: 183 return String.format("\"%s\"", userName != null ? userName : systemName); 184 case USERNAME: 185 case DISPLAYNAME: 186 default: 187 return userName != null ? userName : systemName; 188 } 189 } 190 191 /** 192 * Get a recommended text for a tooltip when displaying 193 * the NamedBean, e.g. in a list or table. 194 * 195 * By default, this is the comment from the NamedBean, on the theory 196 * that the system name and/or user name are being displayed directly. 197 * Specific system implementations may override that. 198 */ 199 @CheckReturnValue 200 @Nonnull 201 default String getRecommendedToolTip() { 202 String retval = getComment(); 203 if (retval == null) return ""; 204 return retval; 205 } 206 207 /** 208 * Request a call-back when a bound property changes. Bound properties are 209 * the known state, commanded state, user and system names. 210 * 211 * @param listener The listener. This may change in the future to be a 212 * subclass of NamedProprtyChangeListener that 213 * carries the name and listenerRef values internally 214 * @param name The name (either system or user) that the listener 215 * uses for this namedBean, this parameter is used to 216 * help determine when which listeners should be 217 * moved when the username is moved from one bean to 218 * another 219 * @param listenerRef A textual reference for the listener, that can be 220 * presented to the user when a delete is called 221 */ 222 void addPropertyChangeListener(@Nonnull PropertyChangeListener listener, String name, String listenerRef); 223 224 /** 225 * Request a call-back when a bound property changes. Bound properties are 226 * the known state, commanded state, user and system names. 227 * 228 * @param propertyName The name of the property to listen to 229 * @param listener The listener. This may change in the future to be a 230 * subclass of NamedProprtyChangeListener that 231 * carries the name and listenerRef values 232 * internally 233 * @param name The name (either system or user) that the listener 234 * uses for this namedBean, this parameter is used 235 * to help determine when which listeners should be 236 * moved when the username is moved from one bean to 237 * another 238 * @param listenerRef A textual reference for the listener, that can be 239 * presented to the user when a delete is called 240 */ 241 void addPropertyChangeListener(@Nonnull String propertyName, @Nonnull PropertyChangeListener listener, 242 String name, String listenerRef); 243 244 void updateListenerRef(@Nonnull PropertyChangeListener l, String newName); 245 246 void vetoableChange(@Nonnull PropertyChangeEvent evt) throws PropertyVetoException; 247 248 /** 249 * Get the textual reference for the specific listener 250 * 251 * @param l the listener of interest 252 * @return the textual reference 253 */ 254 @CheckReturnValue 255 String getListenerRef(@Nonnull PropertyChangeListener l); 256 257 /** 258 * Returns a list of all the listeners references 259 * 260 * @return a list of textual references 261 */ 262 @CheckReturnValue 263 ArrayList<String> getListenerRefs(); 264 265 /** 266 * Number of current listeners. May return -1 if the information is not 267 * available for some reason. 268 * 269 * @return the number of listeners. 270 */ 271 @CheckReturnValue 272 int getNumPropertyChangeListeners(); 273 274 /** 275 * Get a list of all the property change listeners that are registered using 276 * a specific name 277 * 278 * @param name The name (either system or user) that the listener has 279 * registered as referencing this namedBean 280 * @return empty list if none 281 */ 282 @CheckReturnValue 283 @Nonnull 284 PropertyChangeListener[] getPropertyChangeListenersByReference(@Nonnull String name); 285 286 /** 287 * Deactivate this object, so that it releases as many resources as possible 288 * and no longer effects others. 289 * <p> 290 * For example, if this object has listeners, after a call to this method it 291 * should no longer notify those listeners. Any native or system-wide 292 * resources it maintains should be released, including threads, files, etc. 293 * <p> 294 * It is an error to invoke any other methods on this object once dispose() 295 * has been called. Note, however, that there is no guarantee about behavior 296 * in that case. 297 * <p> 298 * Afterwards, references to this object may still exist elsewhere, 299 * preventing its garbage collection. But it's formally dead, and shouldn't 300 * be keeping any other objects alive. Therefore, this method should null 301 * out any references to other objects that this NamedBean contained. 302 */ 303 void dispose(); // remove _all_ connections! 304 305 /** 306 * Provide generic access to internal state. 307 * <p> 308 * This generally shouldn't be used by Java code; use the class-specific 309 * form instead (e.g. setCommandedState in Turnout). This is provided to 310 * make scripts access easier to read. 311 * 312 * @param s the state 313 * @throws JmriException general error when setting the state fails 314 */ 315 @InvokeOnLayoutThread 316 public void setState(int s) throws JmriException; 317 318 /** 319 * Provide generic access to internal state. 320 * <p> 321 * This generally shouldn't be used by Java code; use the class-specific 322 * form instead (e.g. getCommandedState in Turnout). This is provided to 323 * make scripts easier to read. 324 * 325 * @return the state 326 */ 327 @CheckReturnValue 328 public int getState(); 329 330 /** 331 * Provide human-readable, localized version of state value. 332 * <p> 333 * This method is intended for use when presenting to a human operator. 334 * 335 * @param state the state to describe 336 * @return the state in localized form 337 */ 338 @CheckReturnValue 339 public String describeState(int state); 340 341 /** 342 * Get associated comment text. 343 * 344 * @return the comment or null 345 */ 346 @CheckReturnValue 347 @CheckForNull 348 public String getComment(); 349 350 /** 351 * Set associated comment text. 352 * <p> 353 * Comments can be any valid text. 354 * 355 * @param comment the comment or null to remove an existing comment 356 */ 357 public void setComment(@CheckForNull String comment); 358 359 /** 360 * Get a list of references for the specified bean. 361 * 362 * @param bean The bean to be checked. 363 * @return a list of NamedBeanUsageReports or an empty ArrayList. 364 */ 365 default List<NamedBeanUsageReport> getUsageReport(@CheckForNull NamedBean bean) { return (new ArrayList<>()); } 366 367 /** 368 * Attach a key/value pair to the NamedBean, which can be retrieved later. 369 * These are not bound properties as yet, and don't throw events on 370 * modification. Key must not be null. 371 * <p> 372 * The key is constrained to 373 * String to make these behave like normal Java Beans. 374 * 375 * @param key the property to set 376 * @param value the value of the property 377 */ 378 public void setProperty(@Nonnull String key, Object value); 379 380 /** 381 * Retrieve the value associated with a key. If no value has been set for 382 * that key, returns null. 383 * 384 * @param key the property to get 385 * @return The value of the property or null. 386 */ 387 @CheckReturnValue 388 @CheckForNull 389 public Object getProperty(@Nonnull String key); 390 391 /** 392 * Remove the key/value pair against the NamedBean. 393 * 394 * @param key the property to remove 395 */ 396 public void removeProperty(@Nonnull String key); 397 398 /** 399 * Retrieve the complete current set of keys. 400 * 401 * @return empty set if none 402 */ 403 @CheckReturnValue 404 @Nonnull 405 public java.util.Set<String> getPropertyKeys(); 406 407 /** 408 * For instances in the code where we are dealing with just a bean and a 409 * message needs to be passed to the user or in a log. 410 * 411 * @return a string of the bean type, eg Turnout, Sensor etc 412 */ 413 @CheckReturnValue 414 @Nonnull 415 public String getBeanType(); 416 417 /** 418 * Enforces, and as a user convenience converts to, the standard form for a 419 * user name. 420 * <p> 421 * This implementation just does a trim(), but later versions might e.g. do 422 * more extensive things. 423 * 424 * @param inputName User name to be normalized 425 * @throws BadUserNameException If the inputName can't be converted to 426 * normalized form 427 * @return A user name in standard normalized form or null if inputName was 428 * null 429 */ 430 @CheckReturnValue 431 @CheckForNull 432 static public String normalizeUserName(@CheckForNull String inputName) throws BadUserNameException { 433 String result = inputName; 434 if (result != null) { 435 result = result.trim(); 436 } 437 return result; 438 } 439 440 /** 441 * Provide a comparison between the system names of two beans. This provides 442 * a implementation for e.g. {@link java.util.Comparator}. Returns 0 if the 443 * names are the same, -1 if the first argument orders before the second 444 * argument's name, +1 if the first argument's name orders after the second 445 * argument's name. The comparison is alphanumeric on the system prefix, 446 * then alphabetic on the type letter, then system-specific comparison on 447 * the two suffix parts via the {@link #compareSystemNameSuffix} method. 448 * 449 * @param n2 The second NamedBean in the comparison ("this" is the first 450 * one) 451 * @return -1,0,+1 for ordering if the names are well-formed; may not 452 * provide proper ordering if the names are not well-formed. 453 */ 454 @CheckReturnValue 455 @Override 456 public default int compareTo(NamedBean n2) { 457 Objects.requireNonNull(n2); 458 jmri.util.AlphanumComparator ac = new jmri.util.AlphanumComparator(); 459 String o1 = this.getSystemName(); 460 String o2 = n2.getSystemName(); 461 462 int p1len = Manager.getSystemPrefixLength(o1); 463 int p2len = Manager.getSystemPrefixLength(o2); 464 465 int comp = ac.compare(o1.substring(0, p1len), o2.substring(0, p2len)); 466 if (comp != 0) 467 return comp; 468 469 char c1 = o1.charAt(p1len); 470 char c2 = o2.charAt(p2len); 471 472 if (c1 != c2) { 473 return (c1 > c2) ? +1 : -1; 474 } else { 475 return this.compareSystemNameSuffix(o1.substring(p1len + 1), o2.substring(p2len + 1), n2); 476 } 477 } 478 479 /** 480 * Compare the suffix of this NamedBean's name with the suffix of the 481 * argument NamedBean's name for the {@link #compareTo} operation. This is 482 * intended to be a system-specific comparison that understands the various 483 * formats, etc. 484 * 485 * @param suffix1 The suffix for the 1st bean in the comparison 486 * @param suffix2 The suffix for the 2nd bean in the comparison 487 * @param n2 The other (second) NamedBean in the comparison 488 * @return -1,0,+1 for ordering if the names are well-formed; may not 489 * provide proper ordering if the names are not well-formed. 490 */ 491 @CheckReturnValue 492 public int compareSystemNameSuffix(@Nonnull String suffix1, @Nonnull String suffix2, @Nonnull NamedBean n2); 493 494 /** 495 * Parent class for a set of classes that describe if a user name or system 496 * name is a bad name. 497 */ 498 public class BadNameException extends IllegalArgumentException { 499 500 private final String localizedMessage; 501 502 /** 503 * Create an exception with no message to the user or for logging. 504 */ 505 protected BadNameException() { 506 super(); 507 localizedMessage = super.getMessage(); 508 } 509 510 /** 511 * Create a localized exception, suitable for display to the user.This 512 * takes the non-localized message followed by the localized message. 513 * <p> 514 * Use {@link #getLocalizedMessage()} to display the message to the 515 * user, and use {@link #getMessage()} to record the message in logs. 516 * 517 * @param logging the English message for logging 518 * @param display the localized message for display 519 */ 520 protected BadNameException(String logging, String display) { 521 super(logging); 522 localizedMessage = display; 523 } 524 525 @Override 526 public String getLocalizedMessage() { 527 return localizedMessage; 528 } 529 530 } 531 532 public class BadUserNameException extends BadNameException { 533 534 /** 535 * Create an exception with no message to the user or for logging. Use 536 * only when calling methods likely have alternate mechanism for 537 * allowing user to understand why exception was thrown. 538 */ 539 public BadUserNameException() { 540 super(); 541 } 542 543 /** 544 * Create a localized exception, suitable for display to the user. This 545 * takes the same arguments as 546 * {@link jmri.Bundle#getMessage(java.util.Locale, java.lang.String, java.lang.Object...)} 547 * as it uses that method to create both the localized and loggable 548 * messages. 549 * <p> 550 * Use {@link #getLocalizedMessage()} to display the message to the 551 * user, and use {@link #getMessage()} to record the message in logs. 552 * <p> 553 * <strong>Note</strong> the message must be accessible by 554 * {@link jmri.Bundle}. 555 * 556 * @param locale the locale to be used 557 * @param message bundle key to be translated 558 * @param subs One or more objects to be inserted into the message 559 */ 560 public BadUserNameException(Locale locale, String message, Object... subs) { 561 super(Bundle.getMessage(Locale.ENGLISH, message, subs), 562 Bundle.getMessage(locale, message, subs)); 563 } 564 565 /** 566 * Create a localized exception, suitable for display to the user. This 567 * takes the non-localized message followed by the localized message. 568 * <p> 569 * Use {@link #getLocalizedMessage()} to display the message to the 570 * user, and use {@link #getMessage()} to record the message in logs. 571 * 572 * @param logging the English message for logging 573 * @param display the localized message for display 574 */ 575 public BadUserNameException(String logging, String display) { 576 super(logging, display); 577 } 578 } 579 580 public class BadSystemNameException extends BadNameException { 581 582 /** 583 * Create an exception with no message to the user or for logging. Use 584 * only when calling methods likely have alternate mechanism for 585 * allowing user to understand why exception was thrown. 586 */ 587 public BadSystemNameException() { 588 super(); 589 } 590 591 /** 592 * Create a localized exception, suitable for display to the user. This 593 * takes the same arguments as 594 * {@link jmri.Bundle#getMessage(java.util.Locale, java.lang.String, java.lang.Object...)} 595 * as it uses that method to create both the localized and loggable 596 * messages. 597 * <p> 598 * Use {@link #getLocalizedMessage()} to display the message to the 599 * user, and use {@link #getMessage()} to record the message in logs. 600 * <p> 601 * <strong>Note</strong> the message must be accessible by 602 * {@link jmri.Bundle}. 603 * 604 * @param locale the locale to be used 605 * @param message bundle key to be translated 606 * @param subs One or more objects to be inserted into the message 607 */ 608 public BadSystemNameException(Locale locale, String message, Object... subs) { 609 this(Bundle.getMessage(Locale.ENGLISH, message, subs), 610 Bundle.getMessage(locale, message, subs)); 611 } 612 613 /** 614 * Create a localized exception, suitable for display to the user. This 615 * takes the non-localized message followed by the localized message. 616 * <p> 617 * Use {@link #getLocalizedMessage()} to display the message to the 618 * user, and use {@link #getMessage()} to record the message in logs. 619 * 620 * @param logging the English message for logging 621 * @param display the localized message for display 622 */ 623 public BadSystemNameException(String logging, String display) { 624 super(logging, display); 625 } 626 } 627 628 public class DuplicateSystemNameException extends IllegalArgumentException { 629 630 private final String localizedMessage; 631 632 /** 633 * Create an exception with no message to the user or for logging. Use 634 * only when calling methods likely have alternate mechanism for 635 * allowing user to understand why exception was thrown. 636 */ 637 public DuplicateSystemNameException() { 638 super(); 639 localizedMessage = super.getMessage(); 640 } 641 642 /** 643 * Create a exception. 644 * 645 * @param message bundle key to be translated 646 */ 647 public DuplicateSystemNameException(String message) { 648 super(message); 649 localizedMessage = super.getMessage(); 650 } 651 652 /** 653 * Create a localized exception, suitable for display to the user. This 654 * takes the same arguments as 655 * {@link jmri.Bundle#getMessage(java.util.Locale, java.lang.String, java.lang.Object...)} 656 * as it uses that method to create both the localized and loggable 657 * messages. 658 * <p> 659 * Use {@link #getLocalizedMessage()} to display the message to the 660 * user, and use {@link #getMessage()} to record the message in logs. 661 * <p> 662 * <strong>Note</strong> the message must be accessible by 663 * {@link jmri.Bundle}. 664 * 665 * @param locale the locale to be used 666 * @param message bundle key to be translated 667 * @param subs One or more objects to be inserted into the message 668 */ 669 public DuplicateSystemNameException(Locale locale, String message, Object... subs) { 670 this(Bundle.getMessage(locale, message, subs), 671 Bundle.getMessage(locale, message, subs)); 672 } 673 674 /** 675 * Create a localized exception, suitable for display to the user. This 676 * takes the non-localized message followed by the localized message. 677 * <p> 678 * Use {@link #getLocalizedMessage()} to display the message to the 679 * user, and use {@link #getMessage()} to record the message in logs. 680 * 681 * @param logging the English message for logging 682 * @param display the localized message for display 683 */ 684 public DuplicateSystemNameException(String logging, String display) { 685 super(logging); 686 localizedMessage = display; 687 } 688 689 @Override 690 public String getLocalizedMessage() { 691 return localizedMessage; 692 } 693 } 694 695 /** 696 * Display options for {@link #getDisplayName(DisplayOptions)}. The quoted 697 * forms are intended to be used in sentences and messages, while the 698 * unquoted forms are intended for use in user interface elements like lists 699 * and combo boxes. 700 */ 701 public enum DisplayOptions { 702 /** 703 * Display the user name; if the user name is null or empty, display the 704 * system name. 705 */ 706 DISPLAYNAME, 707 /** 708 * Display the user name in quotes; if the user name is null or empty, 709 * display the system name in quotes. 710 */ 711 QUOTED_DISPLAYNAME, 712 /** 713 * Display the user name; if the user name is null or empty, display the 714 * system name. 715 */ 716 USERNAME, 717 /** 718 * Display the user name in quotes; if the user name is null or empty, 719 * display the system name in quotes. 720 */ 721 QUOTED_USERNAME, 722 /** 723 * Display the system name. This should be used only when the context 724 * would cause displaying the user name to be more confusing than not or 725 * in text input fields for editing the system name. 726 */ 727 SYSTEMNAME, 728 /** 729 * Display the system name in quotes. This should be used only when the 730 * context would cause displaying the user name to be more confusing 731 * than not or in text input fields for editing the system name. 732 */ 733 QUOTED_SYSTEMNAME, 734 /** 735 * Display the user name followed by the system name in parenthesis. If 736 * the user name is null or empty, display the system name without 737 * parenthesis. 738 */ 739 USERNAME_SYSTEMNAME, 740 /** 741 * Display the user name in quotes followed by the system name in 742 * parenthesis. If the user name is null or empty, display the system 743 * name in quotes. 744 */ 745 QUOTED_USERNAME_SYSTEMNAME; 746 } 747 748}