001package jmri.jmrix; 002 003import edu.umd.cs.findbugs.annotations.SuppressFBWarnings; 004 005import java.io.DataInputStream; 006import java.io.DataOutputStream; 007import java.io.IOException; 008import java.util.HashMap; 009import java.util.Set; 010 011import javax.annotation.CheckForNull; 012import javax.annotation.Nonnull; 013import javax.annotation.OverridingMethodsMustInvokeSuper; 014 015import jmri.SystemConnectionMemo; 016 017/** 018 * Provide an abstract base for *PortController classes. 019 * <p> 020 * This is complicated by the lack of multiple inheritance. SerialPortAdapter is 021 * an Interface, and its implementing classes also inherit from various 022 * PortController types. But we want some common behaviors for those, so we put 023 * them here. 024 * 025 * @see jmri.jmrix.SerialPortAdapter 026 * 027 * @author Bob Jacobsen Copyright (C) 2001, 2002 028 */ 029abstract public class AbstractPortController implements PortAdapter { 030 031 /** 032 * {@inheritDoc} 033 */ 034 @Override 035 public abstract DataInputStream getInputStream(); 036 037 /** 038 * {@inheritDoc} 039 */ 040 @Override 041 public abstract DataOutputStream getOutputStream(); 042 043 protected String manufacturerName = null; 044 045 // By making this private, and not protected, we are able to require that 046 // all access is through the getter and setter, and that subclasses that 047 // override the getter and setter must call the super implementations of the 048 // getter and setter. By channelling setting through a single method, we can 049 // ensure this is never null. 050 private SystemConnectionMemo connectionMemo; 051 052 protected AbstractPortController(SystemConnectionMemo connectionMemo) { 053 AbstractPortController.this.setSystemConnectionMemo(connectionMemo); 054 } 055 056 /** 057 * Clean up before removal. 058 * 059 * Overriding methods must call <code>super.dispose()</code> or document why 060 * they are not calling the overridden implementation. In most cases, 061 * failure to call the overridden implementation will cause user-visible 062 * error. 063 */ 064 @Override 065 @OverridingMethodsMustInvokeSuper 066 public void dispose() { 067 allowConnectionRecovery = false; 068 this.getSystemConnectionMemo().dispose(); 069 } 070 071 /** 072 * {@inheritDoc} 073 */ 074 @Override 075 public boolean status() { 076 return opened; 077 } 078 079 protected boolean opened = false; 080 081 protected void setOpened() { 082 opened = true; 083 } 084 085 protected void setClosed() { 086 opened = false; 087 } 088 089 //These are to support the old legacy files. 090 protected String option1Name = "1"; 091 protected String option2Name = "2"; 092 protected String option3Name = "3"; 093 protected String option4Name = "4"; 094 095 @Override 096 abstract public String getCurrentPortName(); 097 098 /* 099 * The next set of configureOptions are to support the old configuration files. 100 */ 101 102 @Override 103 public void configureOption1(String value) { 104 if (options.containsKey(option1Name)) { 105 options.get(option1Name).configure(value); 106 } 107 } 108 109 @Override 110 public void configureOption2(String value) { 111 if (options.containsKey(option2Name)) { 112 options.get(option2Name).configure(value); 113 } 114 } 115 116 @Override 117 public void configureOption3(String value) { 118 if (options.containsKey(option3Name)) { 119 options.get(option3Name).configure(value); 120 } 121 } 122 123 @Override 124 public void configureOption4(String value) { 125 if (options.containsKey(option4Name)) { 126 options.get(option4Name).configure(value); 127 } 128 } 129 130 /* 131 * The next set of getOption Names are to support legacy configuration files 132 */ 133 134 @Override 135 public String getOption1Name() { 136 return option1Name; 137 } 138 139 @Override 140 public String getOption2Name() { 141 return option2Name; 142 } 143 144 @Override 145 public String getOption3Name() { 146 return option3Name; 147 } 148 149 @Override 150 public String getOption4Name() { 151 return option4Name; 152 } 153 154 /** 155 * Get a list of all the options configured against this adapter. 156 * 157 * @return Array of option identifier strings 158 */ 159 @Override 160 public String[] getOptions() { 161 Set<String> keySet = options.keySet(); 162 String[] result = keySet.toArray(String[]::new); 163 java.util.Arrays.sort(result); 164 return result; 165 } 166 167 /** 168 * Set the value of an option. 169 * 170 * @param option the name string of the option 171 * @param value the string value to set the option to 172 */ 173 @Override 174 public void setOptionState(String option, String value) { 175 log.trace("setOptionState({},{})", option, value); 176 if (options.containsKey(option)) { 177 options.get(option).configure(value); 178 } else { 179 log.warn("Couldn't find option \"{}\", can't set to \"{}\"", option, value); 180 } 181 } 182 183 /** 184 * Get the string value of a specific option. 185 * 186 * @param option the name of the option to query 187 * @return the option value 188 */ 189 @Override 190 public String getOptionState(String option) { 191 if (options.containsKey(option)) { 192 return options.get(option).getCurrent(); 193 } 194 return null; 195 } 196 197 /** 198 * Get a list of the various choices allowed with a given option. 199 * 200 * @param option the name of the option to query 201 * @return list of valid values for the option, null if none are available 202 */ 203 @Override 204 @edu.umd.cs.findbugs.annotations.SuppressFBWarnings(value = "PZLA_PREFER_ZERO_LENGTH_ARRAYS", 205 justification = "availability was checked before, should never get here") 206 public String[] getOptionChoices(String option) { 207 if (options.containsKey(option)) { 208 return options.get(option).getOptions(); 209 } 210 return null; 211 } 212 213 214 @Override 215 public boolean isOptionTypeText(String option) { 216 if (options.containsKey(option)) { 217 return options.get(option).getType() == Option.Type.TEXT; 218 } 219 log.error("did not find option {} for type", option); 220 return false; 221 } 222 223 @Override 224 public boolean isOptionTypePassword(String option) { 225 if (options.containsKey(option)) { 226 return options.get(option).getType() == Option.Type.PASSWORD; 227 } 228 log.error("did not find option {} for type", option); 229 return false; 230 } 231 232 @Override 233 public String getOptionDisplayName(String option) { 234 if (options.containsKey(option)) { 235 return options.get(option).getDisplayText(); 236 } 237 return null; 238 } 239 240 @Override 241 public boolean isOptionAdvanced(String option) { 242 if (options.containsKey(option)) { 243 return options.get(option).isAdvanced(); 244 } 245 return false; 246 } 247 248 protected HashMap<String, Option> options = new HashMap<>(); 249 250 protected static class Option { 251 252 public enum Type { 253 JCOMBOBOX, 254 TEXT, 255 PASSWORD 256 } 257 258 private String currentValue = null; 259 260 /** 261 * As a heuristic, we consider the 1st non-null 262 * currentValue as the configured value. Changes away from that 263 * mark an Option object as "dirty". 264 */ 265 private String configuredValue = null; 266 267 String displayText; 268 String[] options; 269 private final String defaultChoice; 270 Type type; 271 272 boolean advancedOption = true; // added options in advanced section by default 273 274 public Option(String displayText, @Nonnull String[] options, 275 boolean advanced, Type type, @CheckForNull String defaultValue ) { 276 this.displayText = displayText; 277 this.options = java.util.Arrays.copyOf(options, options.length); 278 this.advancedOption = advanced; 279 this.type = type; 280 this.defaultChoice = defaultValue; 281 } 282 283 public Option(String displayText, @Nonnull String[] options, boolean advanced, Type type) { 284 this(displayText, options, advanced, type, null); 285 } 286 287 public Option(String displayText, String[] options, boolean advanced) { 288 this(displayText, options, advanced, Type.JCOMBOBOX); 289 } 290 291 public Option(String displayText, String[] options, Type type) { 292 this(displayText, options, true, type); 293 } 294 295 public Option(String displayText, String[] options) { 296 this(displayText, options, true, Type.JCOMBOBOX); 297 } 298 299 public Option(String displayText, String[] options, @CheckForNull String defaultValue) { 300 this(displayText, options, true, Type.JCOMBOBOX, defaultValue); 301 } 302 303 void configure(String value) { 304 log.trace("Option.configure({}) with \"{}\", \"{}\"", value, getConfiguredValue(), getCurrentValue()); 305 if (getConfiguredValue() == null ) { 306 setConfiguredValue(value); 307 } 308 setCurrentValue(value); 309 } 310 311 String getCurrent() { 312 if (getCurrentValue() == null) { 313 return defaultChoice != null ? defaultChoice : options[0]; 314 } 315 return getCurrentValue(); 316 } 317 318 String[] getOptions() { 319 return options; 320 } 321 322 Type getType() { 323 return type; 324 } 325 326 String getDisplayText() { 327 return displayText; 328 } 329 330 boolean isAdvanced() { 331 return advancedOption; 332 } 333 334 boolean isDirty() { 335 return (getCurrentValue() != null && !getCurrentValue().equals(getConfiguredValue())); 336 } 337 338 public String getCurrentValue() { 339 return currentValue; 340 } 341 342 public void setCurrentValue(String currentValue) { 343 this.currentValue = currentValue; 344 } 345 346 public String getConfiguredValue() { 347 return configuredValue; 348 } 349 350 public void setConfiguredValue(String configuredValue) { 351 this.configuredValue = configuredValue; 352 } 353 } 354 355 @Override 356 public String getManufacturer() { 357 return manufacturerName; 358 } 359 360 @Override 361 public void setManufacturer(String manufacturer) { 362 log.debug("update manufacturer from {} to {}", this.manufacturerName, manufacturer); 363 this.manufacturerName = manufacturer; 364 } 365 366 @Override 367 public boolean getDisabled() { 368 return this.getSystemConnectionMemo().getDisabled(); 369 } 370 371 /** 372 * Set the connection disabled or enabled. By default connections are 373 * enabled. 374 * 375 * If the implementing class does not use a 376 * {@link SystemConnectionMemo}, this method must be overridden. 377 * Overriding methods must call <code>super.setDisabled(boolean)</code> to 378 * ensure the configuration change state is correctly set. 379 * 380 * @param disabled true if connection should be disabled 381 */ 382 @Override 383 public void setDisabled(boolean disabled) { 384 this.getSystemConnectionMemo().setDisabled(disabled); 385 } 386 387 @Override 388 public String getSystemPrefix() { 389 return this.getSystemConnectionMemo().getSystemPrefix(); 390 } 391 392 @Override 393 public void setSystemPrefix(String systemPrefix) { 394 if (!this.getSystemConnectionMemo().setSystemPrefix(systemPrefix)) { 395 throw new IllegalArgumentException(); 396 } 397 } 398 399 @Override 400 public String getUserName() { 401 return this.getSystemConnectionMemo().getUserName(); 402 } 403 404 @Override 405 public void setUserName(String userName) { 406 if (!this.getSystemConnectionMemo().setUserName(userName)) { 407 throw new IllegalArgumentException(); 408 } 409 } 410 411 protected boolean allowConnectionRecovery = false; 412 413 /** 414 * {@inheritDoc} 415 * After checking the allowConnectionRecovery flag, closes the 416 * connection, resets the open flag and attempts a reconnection. 417 */ 418 @Override 419 public void recover() { 420 if (!allowConnectionRecovery) { 421 return; 422 } 423 opened = false; 424 try { 425 closeConnection(); 426 } 427 catch (RuntimeException e) { 428 log.warn("closeConnection failed"); 429 } 430 reconnect(); 431 } 432 433 /** 434 * Abstract class for controllers to close the connection. 435 * Called prior to any re-connection attempts. 436 */ 437 protected void closeConnection(){} 438 439 /** 440 * Attempts to reconnect to a failed port. 441 * Starts a reconnect thread 442 */ 443 protected void reconnect() { 444 // If the connection is already open, then we shouldn't try a re-connect. 445 if (opened || !allowConnectionRecovery) { 446 return; 447 } 448 Thread thread = jmri.util.ThreadingUtil.newThread(new ReconnectWait(), 449 "Connection Recovery " + getCurrentPortName()); 450 thread.start(); 451 try { 452 thread.join(); 453 } catch (InterruptedException e) { 454 log.error("Unable to join to the reconnection thread"); 455 } 456 } 457 458 /** 459 * Abstract class for controllers to re-setup a connection. 460 * Called on connection reconnect success. 461 */ 462 protected void resetupConnection(){} 463 464 /** 465 * Abstract class for ports to attempt a single re-connection attempt. 466 * Called from within main reconnect thread. 467 * @param retryNum Reconnection attempt number. 468 */ 469 protected void reconnectFromLoop(int retryNum){} 470 471 @edu.umd.cs.findbugs.annotations.SuppressFBWarnings( value="SLF4J_FORMAT_SHOULD_BE_CONST", 472 justification="I18N of Info Message") 473 private class ReconnectWait extends Thread { 474 @Override 475 public void run() { 476 boolean reply = true; 477 int count = 0; 478 int interval = reconnectinterval; 479 int totalsleep = 0; 480 while (reply && allowConnectionRecovery) { 481 safeSleep(interval*1000L, "Waiting"); 482 count++; 483 totalsleep += interval; 484 reconnectFromLoop(count); 485 reply = !opened; 486 if (opened){ 487 log.info(Bundle.getMessage("ReconnectedTo",getCurrentPortName())); 488 resetupConnection(); 489 return; 490 } 491 if (count % 10==0) { 492 //retrying but with twice the retry interval. 493 interval = Math.min(interval * 2, reconnectMaxInterval); 494 log.error(Bundle.getMessage("ReconnectFailRetry", totalsleep, count,interval)); 495 } 496 if ((reconnectMaxAttempts > -1) && (count >= reconnectMaxAttempts)) { 497 log.error(Bundle.getMessage("ReconnectFailAbort",totalsleep,count)); 498 reply = false; 499 } 500 } 501 } 502 } 503 504 /** 505 * Initial interval between reconnection attempts. 506 * Default 1 second. 507 */ 508 protected int reconnectinterval = 1; 509 510 /** 511 * Maximum reconnection attempts that the port should make. 512 * Default 100 attempts. 513 * A value of -1 indicates unlimited attempts. 514 */ 515 protected int reconnectMaxAttempts = 100; 516 517 /** 518 * Maximum interval between reconnection attempts in seconds. 519 * Default 120 seconds. 520 */ 521 protected int reconnectMaxInterval = 120; 522 523 /** 524 * {@inheritDoc} 525 */ 526 @Override 527 public void setReconnectMaxInterval(int maxInterval) { 528 reconnectMaxInterval = maxInterval; 529 } 530 531 /** 532 * {@inheritDoc} 533 */ 534 @Override 535 public void setReconnectMaxAttempts(int maxAttempts) { 536 reconnectMaxAttempts = maxAttempts; 537 } 538 539 /** 540 * {@inheritDoc} 541 */ 542 @Override 543 public int getReconnectMaxInterval() { 544 return reconnectMaxInterval; 545 } 546 547 /** 548 * {@inheritDoc} 549 */ 550 @Override 551 public int getReconnectMaxAttempts() { 552 return reconnectMaxAttempts; 553 } 554 555 protected static void safeSleep(long milliseconds, String s) { 556 try { 557 Thread.sleep(milliseconds); 558 } catch (InterruptedException e) { 559 log.error("Sleep Exception raised during reconnection attempt{}", s); 560 } 561 } 562 563 @Override 564 public boolean isDirty() { 565 boolean isDirty = this.getSystemConnectionMemo().isDirty(); 566 if (!isDirty) { 567 for (Option option : this.options.values()) { 568 isDirty = option.isDirty(); 569 if (isDirty) { 570 break; 571 } 572 } 573 } 574 return isDirty; 575 } 576 577 @Override 578 public boolean isRestartRequired() { 579 // Override if any option should not be considered when determining if a 580 // change requires JMRI to be restarted. 581 return this.isDirty(); 582 } 583 584 /** 585 * Service method to purge a stream of initial contents 586 * while opening the connection. 587 * @param serialStream input data 588 * @throws IOException if the stream is e.g. closed due to failure to open the port completely 589 */ 590 @SuppressFBWarnings(value = "SR_NOT_CHECKED", justification = "skipping all, don't care what skip() returns") 591 public static void purgeStream(@Nonnull java.io.InputStream serialStream) throws IOException { 592 int count = serialStream.available(); 593 log.debug("input stream shows {} bytes available", count); 594 while (count > 0) { 595 serialStream.skip(count); 596 count = serialStream.available(); 597 } 598 } 599 600 /** 601 * Get the {@link SystemConnectionMemo} associated with this 602 * object. 603 * <p> 604 * This method should only be overridden to ensure that a specific subclass 605 * of SystemConnectionMemo is returned. The recommended pattern is: <code> 606 * public MySystemConnectionMemo getSystemConnectionMemo() { 607 * return (MySystemConnectionMemo) super.getSystemConnectionMemo(); 608 * } 609 * </code> 610 * 611 * @return the currently associated SystemConnectionMemo 612 */ 613 @Override 614 public SystemConnectionMemo getSystemConnectionMemo() { 615 return this.connectionMemo; 616 } 617 618 /** 619 * Set the {@link SystemConnectionMemo} associated with this 620 * object. 621 * <p> 622 * Overriding implementations must call 623 * <code>super.setSystemConnectionMemo(memo)</code> at some point to ensure 624 * the SystemConnectionMemo gets set. 625 * 626 * @param connectionMemo the SystemConnectionMemo to associate with this PortController 627 */ 628 @Override 629 @OverridingMethodsMustInvokeSuper 630 public void setSystemConnectionMemo(@Nonnull SystemConnectionMemo connectionMemo) { 631 if (connectionMemo == null) { 632 throw new NullPointerException(); 633 } 634 this.connectionMemo = connectionMemo; 635 } 636 637 private final static org.slf4j.Logger log = org.slf4j.LoggerFactory.getLogger(AbstractPortController.class); 638 639}