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