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