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 * Request a call-back when a bound property changes. Bound properties are 193 * the known state, commanded state, user and system names. 194 * 195 * @param listener The listener. This may change in the future to be a 196 * subclass of NamedProprtyChangeListener that 197 * carries the name and listenerRef values internally 198 * @param name The name (either system or user) that the listener 199 * uses for this namedBean, this parameter is used to 200 * help determine when which listeners should be 201 * moved when the username is moved from one bean to 202 * another 203 * @param listenerRef A textual reference for the listener, that can be 204 * presented to the user when a delete is called 205 */ 206 void addPropertyChangeListener(@Nonnull PropertyChangeListener listener, String name, String listenerRef); 207 208 /** 209 * Request a call-back when a bound property changes. Bound properties are 210 * the known state, commanded state, user and system names. 211 * 212 * @param propertyName The name of the property to listen to 213 * @param listener The listener. This may change in the future to be a 214 * subclass of NamedProprtyChangeListener that 215 * carries the name and listenerRef values 216 * internally 217 * @param name The name (either system or user) that the listener 218 * uses for this namedBean, this parameter is used 219 * to help determine when which listeners should be 220 * moved when the username is moved from one bean to 221 * another 222 * @param listenerRef A textual reference for the listener, that can be 223 * presented to the user when a delete is called 224 */ 225 void addPropertyChangeListener(@Nonnull String propertyName, @Nonnull PropertyChangeListener listener, 226 String name, String listenerRef); 227 228 void updateListenerRef(@Nonnull PropertyChangeListener l, String newName); 229 230 void vetoableChange(@Nonnull PropertyChangeEvent evt) throws PropertyVetoException; 231 232 /** 233 * Get the textual reference for the specific listener 234 * 235 * @param l the listener of interest 236 * @return the textual reference 237 */ 238 @CheckReturnValue 239 String getListenerRef(@Nonnull PropertyChangeListener l); 240 241 /** 242 * Returns a list of all the listeners references 243 * 244 * @return a list of textual references 245 */ 246 @CheckReturnValue 247 ArrayList<String> getListenerRefs(); 248 249 /** 250 * Number of current listeners. May return -1 if the information is not 251 * available for some reason. 252 * 253 * @return the number of listeners. 254 */ 255 @CheckReturnValue 256 int getNumPropertyChangeListeners(); 257 258 /** 259 * Get a list of all the property change listeners that are registered using 260 * a specific name 261 * 262 * @param name The name (either system or user) that the listener has 263 * registered as referencing this namedBean 264 * @return empty list if none 265 */ 266 @CheckReturnValue 267 @Nonnull 268 PropertyChangeListener[] getPropertyChangeListenersByReference(@Nonnull String name); 269 270 /** 271 * Deactivate this object, so that it releases as many resources as possible 272 * and no longer effects others. 273 * <p> 274 * For example, if this object has listeners, after a call to this method it 275 * should no longer notify those listeners. Any native or system-wide 276 * resources it maintains should be released, including threads, files, etc. 277 * <p> 278 * It is an error to invoke any other methods on this object once dispose() 279 * has been called. Note, however, that there is no guarantee about behavior 280 * in that case. 281 * <p> 282 * Afterwards, references to this object may still exist elsewhere, 283 * preventing its garbage collection. But it's formally dead, and shouldn't 284 * be keeping any other objects alive. Therefore, this method should null 285 * out any references to other objects that this NamedBean contained. 286 */ 287 void dispose(); // remove _all_ connections! 288 289 /** 290 * Provide generic access to internal state. 291 * <p> 292 * This generally shouldn't be used by Java code; use the class-specific 293 * form instead (e.g. setCommandedState in Turnout). This is provided to 294 * make scripts access easier to read. 295 * 296 * @param s the state 297 * @throws JmriException general error when setting the state fails 298 */ 299 @InvokeOnLayoutThread 300 public void setState(int s) throws JmriException; 301 302 /** 303 * Provide generic access to internal state. 304 * <p> 305 * This generally shouldn't be used by Java code; use the class-specific 306 * form instead (e.g. getCommandedState in Turnout). This is provided to 307 * make scripts easier to read. 308 * 309 * @return the state 310 */ 311 @CheckReturnValue 312 public int getState(); 313 314 /** 315 * Provide human-readable, localized version of state value. 316 * <p> 317 * This method is intended for use when presenting to a human operator. 318 * 319 * @param state the state to describe 320 * @return the state in localized form 321 */ 322 @CheckReturnValue 323 public String describeState(int state); 324 325 /** 326 * Get associated comment text. 327 * 328 * @return the comment or null 329 */ 330 @CheckReturnValue 331 @CheckForNull 332 public String getComment(); 333 334 /** 335 * Set associated comment text. 336 * <p> 337 * Comments can be any valid text. 338 * 339 * @param comment the comment or null to remove an existing comment 340 */ 341 public void setComment(@CheckForNull String comment); 342 343 /** 344 * Get a list of references for the specified bean. 345 * 346 * @param bean The bean to be checked. 347 * @return a list of NamedBeanUsageReports or an empty ArrayList. 348 */ 349 default List<NamedBeanUsageReport> getUsageReport(@CheckForNull NamedBean bean) { return (new ArrayList<>()); } 350 351 /** 352 * Attach a key/value pair to the NamedBean, which can be retrieved later. 353 * These are not bound properties as yet, and don't throw events on 354 * modification. Key must not be null. 355 * <p> 356 * Prior to JMRI 4.3, the key was of Object type. It was constrained to 357 * String to make these more like normal Java Beans. 358 * 359 * @param key the property to set 360 * @param value the value of the property 361 */ 362 public void setProperty(@Nonnull String key, Object value); 363 364 /** 365 * Retrieve the value associated with a key. If no value has been set for 366 * that key, returns null. 367 * 368 * @param key the property to get 369 * @return The value of the property or null. 370 */ 371 @CheckReturnValue 372 @CheckForNull 373 public Object getProperty(@Nonnull String key); 374 375 /** 376 * Remove the key/value pair against the NamedBean. 377 * 378 * @param key the property to remove 379 */ 380 public void removeProperty(@Nonnull String key); 381 382 /** 383 * Retrieve the complete current set of keys. 384 * 385 * @return empty set if none 386 */ 387 @CheckReturnValue 388 @Nonnull 389 public java.util.Set<String> getPropertyKeys(); 390 391 /** 392 * For instances in the code where we are dealing with just a bean and a 393 * message needs to be passed to the user or in a log. 394 * 395 * @return a string of the bean type, eg Turnout, Sensor etc 396 */ 397 @CheckReturnValue 398 @Nonnull 399 public String getBeanType(); 400 401 /** 402 * Enforces, and as a user convenience converts to, the standard form for a 403 * user name. 404 * <p> 405 * This implementation just does a trim(), but later versions might e.g. do 406 * more extensive things. 407 * 408 * @param inputName User name to be normalized 409 * @throws BadUserNameException If the inputName can't be converted to 410 * normalized form 411 * @return A user name in standard normalized form or null if inputName was 412 * null 413 */ 414 @CheckReturnValue 415 @CheckForNull 416 static public String normalizeUserName(@CheckForNull String inputName) throws BadUserNameException { 417 String result = inputName; 418 if (result != null) { 419 result = result.trim(); 420 } 421 return result; 422 } 423 424 /** 425 * Provide a comparison between the system names of two beans. This provides 426 * a implementation for e.g. {@link java.util.Comparator}. Returns 0 if the 427 * names are the same, -1 if the first argument orders before the second 428 * argument's name, +1 if the first argument's name orders after the second 429 * argument's name. The comparison is alphanumeric on the system prefix, 430 * then alphabetic on the type letter, then system-specific comparison on 431 * the two suffix parts via the {@link #compareSystemNameSuffix} method. 432 * 433 * @param n2 The second NamedBean in the comparison ("this" is the first 434 * one) 435 * @return -1,0,+1 for ordering if the names are well-formed; may not 436 * provide proper ordering if the names are not well-formed. 437 */ 438 @CheckReturnValue 439 @Override 440 public default int compareTo(NamedBean n2) { 441 Objects.requireNonNull(n2); 442 jmri.util.AlphanumComparator ac = new jmri.util.AlphanumComparator(); 443 String o1 = this.getSystemName(); 444 String o2 = n2.getSystemName(); 445 446 int p1len = Manager.getSystemPrefixLength(o1); 447 int p2len = Manager.getSystemPrefixLength(o2); 448 449 int comp = ac.compare(o1.substring(0, p1len), o2.substring(0, p2len)); 450 if (comp != 0) 451 return comp; 452 453 char c1 = o1.charAt(p1len); 454 char c2 = o2.charAt(p2len); 455 456 if (c1 != c2) { 457 return (c1 > c2) ? +1 : -1; 458 } else { 459 return this.compareSystemNameSuffix(o1.substring(p1len + 1), o2.substring(p2len + 1), n2); 460 } 461 } 462 463 /** 464 * Compare the suffix of this NamedBean's name with the suffix of the 465 * argument NamedBean's name for the {@link #compareTo} operation. This is 466 * intended to be a system-specific comparison that understands the various 467 * formats, etc. 468 * 469 * @param suffix1 The suffix for the 1st bean in the comparison 470 * @param suffix2 The suffix for the 2nd bean in the comparison 471 * @param n2 The other (second) NamedBean in the comparison 472 * @return -1,0,+1 for ordering if the names are well-formed; may not 473 * provide proper ordering if the names are not well-formed. 474 */ 475 @CheckReturnValue 476 public int compareSystemNameSuffix(@Nonnull String suffix1, @Nonnull String suffix2, @Nonnull NamedBean n2); 477 478 /** 479 * Parent class for a set of classes that describe if a user name or system 480 * name is a bad name. 481 */ 482 public class BadNameException extends IllegalArgumentException { 483 484 private final String localizedMessage; 485 486 /** 487 * Create an exception with no message to the user or for logging. 488 */ 489 protected BadNameException() { 490 super(); 491 localizedMessage = super.getMessage(); 492 } 493 494 /** 495 * Create a localized exception, suitable for display to the user.This 496 * takes the non-localized message followed by the localized message. 497 * <p> 498 * Use {@link #getLocalizedMessage()} to display the message to the 499 * user, and use {@link #getMessage()} to record the message in logs. 500 * 501 * @param logging the English message for logging 502 * @param display the localized message for display 503 */ 504 protected BadNameException(String logging, String display) { 505 super(logging); 506 localizedMessage = display; 507 } 508 509 @Override 510 public String getLocalizedMessage() { 511 return localizedMessage; 512 } 513 514 } 515 516 public class BadUserNameException extends BadNameException { 517 518 /** 519 * Create an exception with no message to the user or for logging. Use 520 * only when calling methods likely have alternate mechanism for 521 * allowing user to understand why exception was thrown. 522 */ 523 public BadUserNameException() { 524 super(); 525 } 526 527 /** 528 * Create a localized exception, suitable for display to the user. This 529 * takes the same arguments as 530 * {@link jmri.Bundle#getMessage(java.util.Locale, java.lang.String, java.lang.Object...)} 531 * as it uses that method to create both the localized and loggable 532 * messages. 533 * <p> 534 * Use {@link #getLocalizedMessage()} to display the message to the 535 * user, and use {@link #getMessage()} to record the message in logs. 536 * <p> 537 * <strong>Note</strong> the message must be accessible by 538 * {@link jmri.Bundle}. 539 * 540 * @param locale the locale to be used 541 * @param message bundle key to be translated 542 * @param subs One or more objects to be inserted into the message 543 */ 544 public BadUserNameException(Locale locale, String message, Object... subs) { 545 super(Bundle.getMessage(Locale.ENGLISH, message, subs), 546 Bundle.getMessage(locale, message, subs)); 547 } 548 549 /** 550 * Create a localized exception, suitable for display to the user. This 551 * takes the non-localized message followed by the localized message. 552 * <p> 553 * Use {@link #getLocalizedMessage()} to display the message to the 554 * user, and use {@link #getMessage()} to record the message in logs. 555 * 556 * @param logging the English message for logging 557 * @param display the localized message for display 558 */ 559 public BadUserNameException(String logging, String display) { 560 super(logging, display); 561 } 562 } 563 564 public class BadSystemNameException extends BadNameException { 565 566 /** 567 * Create an exception with no message to the user or for logging. Use 568 * only when calling methods likely have alternate mechanism for 569 * allowing user to understand why exception was thrown. 570 */ 571 public BadSystemNameException() { 572 super(); 573 } 574 575 /** 576 * Create a localized exception, suitable for display to the user. This 577 * takes the same arguments as 578 * {@link jmri.Bundle#getMessage(java.util.Locale, java.lang.String, java.lang.Object...)} 579 * as it uses that method to create both the localized and loggable 580 * messages. 581 * <p> 582 * Use {@link #getLocalizedMessage()} to display the message to the 583 * user, and use {@link #getMessage()} to record the message in logs. 584 * <p> 585 * <strong>Note</strong> the message must be accessible by 586 * {@link jmri.Bundle}. 587 * 588 * @param locale the locale to be used 589 * @param message bundle key to be translated 590 * @param subs One or more objects to be inserted into the message 591 */ 592 public BadSystemNameException(Locale locale, String message, Object... subs) { 593 this(Bundle.getMessage(Locale.ENGLISH, message, subs), 594 Bundle.getMessage(locale, message, subs)); 595 } 596 597 /** 598 * Create a localized exception, suitable for display to the user. This 599 * takes the non-localized message followed by the localized message. 600 * <p> 601 * Use {@link #getLocalizedMessage()} to display the message to the 602 * user, and use {@link #getMessage()} to record the message in logs. 603 * 604 * @param logging the English message for logging 605 * @param display the localized message for display 606 */ 607 public BadSystemNameException(String logging, String display) { 608 super(logging, display); 609 } 610 } 611 612 public class DuplicateSystemNameException extends IllegalArgumentException { 613 614 private final String localizedMessage; 615 616 /** 617 * Create an exception with no message to the user or for logging. Use 618 * only when calling methods likely have alternate mechanism for 619 * allowing user to understand why exception was thrown. 620 */ 621 public DuplicateSystemNameException() { 622 super(); 623 localizedMessage = super.getMessage(); 624 } 625 626 /** 627 * Create a exception. 628 * 629 * @param message bundle key to be translated 630 */ 631 public DuplicateSystemNameException(String message) { 632 super(message); 633 localizedMessage = super.getMessage(); 634 } 635 636 /** 637 * Create a localized exception, suitable for display to the user. This 638 * takes the same arguments as 639 * {@link jmri.Bundle#getMessage(java.util.Locale, java.lang.String, java.lang.Object...)} 640 * as it uses that method to create both the localized and loggable 641 * messages. 642 * <p> 643 * Use {@link #getLocalizedMessage()} to display the message to the 644 * user, and use {@link #getMessage()} to record the message in logs. 645 * <p> 646 * <strong>Note</strong> the message must be accessible by 647 * {@link jmri.Bundle}. 648 * 649 * @param locale the locale to be used 650 * @param message bundle key to be translated 651 * @param subs One or more objects to be inserted into the message 652 */ 653 public DuplicateSystemNameException(Locale locale, String message, Object... subs) { 654 this(Bundle.getMessage(locale, message, subs), 655 Bundle.getMessage(locale, message, subs)); 656 } 657 658 /** 659 * Create a localized exception, suitable for display to the user. This 660 * takes the non-localized message followed by the localized message. 661 * <p> 662 * Use {@link #getLocalizedMessage()} to display the message to the 663 * user, and use {@link #getMessage()} to record the message in logs. 664 * 665 * @param logging the English message for logging 666 * @param display the localized message for display 667 */ 668 public DuplicateSystemNameException(String logging, String display) { 669 super(logging); 670 localizedMessage = display; 671 } 672 673 @Override 674 public String getLocalizedMessage() { 675 return localizedMessage; 676 } 677 } 678 679 /** 680 * Display options for {@link #getDisplayName(DisplayOptions)}. The quoted 681 * forms are intended to be used in sentences and messages, while the 682 * unquoted forms are intended for use in user interface elements like lists 683 * and combo boxes. 684 */ 685 public enum DisplayOptions { 686 /** 687 * Display the user name; if the user name is null or empty, display the 688 * system name. 689 */ 690 DISPLAYNAME, 691 /** 692 * Display the user name in quotes; if the user name is null or empty, 693 * display the system name in quotes. 694 */ 695 QUOTED_DISPLAYNAME, 696 /** 697 * Display the user name; if the user name is null or empty, display the 698 * system name. 699 */ 700 USERNAME, 701 /** 702 * Display the user name in quotes; if the user name is null or empty, 703 * display the system name in quotes. 704 */ 705 QUOTED_USERNAME, 706 /** 707 * Display the system name. This should be used only when the context 708 * would cause displaying the user name to be more confusing than not or 709 * in text input fields for editing the system name. 710 */ 711 SYSTEMNAME, 712 /** 713 * Display the system name in quotes. This should be used only when the 714 * context would cause displaying the user name to be more confusing 715 * than not or in text input fields for editing the system name. 716 */ 717 QUOTED_SYSTEMNAME, 718 /** 719 * Display the user name followed by the system name in parenthesis. If 720 * the user name is null or empty, display the system name without 721 * parenthesis. 722 */ 723 USERNAME_SYSTEMNAME, 724 /** 725 * Display the user name in quotes followed by the system name in 726 * parenthesis. If the user name is null or empty, display the system 727 * name in quotes. 728 */ 729 QUOTED_USERNAME_SYSTEMNAME; 730 } 731 732}