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 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 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, configuredValue, currentValue); 294 if (configuredValue == null ) { 295 configuredValue = value; 296 } 297 currentValue = value; 298 } 299 300 String getCurrent() { 301 if (currentValue == null) { 302 return options[0]; 303 } 304 return currentValue; 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 (currentValue != null && !currentValue.equals(configuredValue)); 325 } 326 } 327 328 @Override 329 public String getManufacturer() { 330 return manufacturerName; 331 } 332 333 @Override 334 public void setManufacturer(String manufacturer) { 335 log.debug("update manufacturer from {} to {}", this.manufacturerName, manufacturer); 336 this.manufacturerName = manufacturer; 337 } 338 339 @Override 340 public boolean getDisabled() { 341 return this.getSystemConnectionMemo().getDisabled(); 342 } 343 344 /** 345 * Set the connection disabled or enabled. By default connections are 346 * enabled. 347 * 348 * If the implementing class does not use a 349 * {@link SystemConnectionMemo}, this method must be overridden. 350 * Overriding methods must call <code>super.setDisabled(boolean)</code> to 351 * ensure the configuration change state is correctly set. 352 * 353 * @param disabled true if connection should be disabled 354 */ 355 @Override 356 public void setDisabled(boolean disabled) { 357 this.getSystemConnectionMemo().setDisabled(disabled); 358 } 359 360 @Override 361 public String getSystemPrefix() { 362 return this.getSystemConnectionMemo().getSystemPrefix(); 363 } 364 365 @Override 366 public void setSystemPrefix(String systemPrefix) { 367 if (!this.getSystemConnectionMemo().setSystemPrefix(systemPrefix)) { 368 throw new IllegalArgumentException(); 369 } 370 } 371 372 @Override 373 public String getUserName() { 374 return this.getSystemConnectionMemo().getUserName(); 375 } 376 377 @Override 378 public void setUserName(String userName) { 379 if (!this.getSystemConnectionMemo().setUserName(userName)) { 380 throw new IllegalArgumentException(); 381 } 382 } 383 384 protected boolean allowConnectionRecovery = false; 385 386 /** 387 * {@inheritDoc} 388 * After checking the allowConnectionRecovery flag, closes the 389 * connection, resets the open flag and attempts a reconnection. 390 */ 391 @Override 392 public void recover() { 393 if (!allowConnectionRecovery) { 394 return; 395 } 396 opened = false; 397 try { 398 closeConnection(); 399 } 400 catch (RuntimeException e) { 401 log.warn("closeConnection failed"); 402 } 403 reconnect(); 404 } 405 406 /** 407 * Abstract class for controllers to close the connection. 408 * Called prior to any re-connection attempts. 409 */ 410 protected void closeConnection(){} 411 412 /** 413 * Attempts to reconnect to a failed port. 414 * Starts a reconnect thread 415 */ 416 protected void reconnect() { 417 // If the connection is already open, then we shouldn't try a re-connect. 418 if (opened || !allowConnectionRecovery) { 419 return; 420 } 421 Thread thread = jmri.util.ThreadingUtil.newThread(new ReconnectWait(), 422 "Connection Recovery " + getCurrentPortName()); 423 thread.start(); 424 try { 425 thread.join(); 426 } catch (InterruptedException e) { 427 log.error("Unable to join to the reconnection thread"); 428 } 429 } 430 431 /** 432 * Abstract class for controllers to re-setup a connection. 433 * Called on connection reconnect success. 434 */ 435 protected void resetupConnection(){} 436 437 /** 438 * Abstract class for ports to attempt a single re-connection attempt. 439 * Called from within main reconnect thread. 440 * @param retryNum Reconnection attempt number. 441 */ 442 protected void reconnectFromLoop(int retryNum){} 443 444 @edu.umd.cs.findbugs.annotations.SuppressFBWarnings( value="SLF4J_FORMAT_SHOULD_BE_CONST", 445 justification="I18N of Info Message") 446 private class ReconnectWait extends Thread { 447 @Override 448 public void run() { 449 boolean reply = true; 450 int count = 0; 451 int interval = reconnectinterval; 452 int totalsleep = 0; 453 while (reply && allowConnectionRecovery) { 454 safeSleep(interval*1000L, "Waiting"); 455 count++; 456 totalsleep += interval; 457 reconnectFromLoop(count); 458 reply = !opened; 459 if (opened){ 460 log.info(Bundle.getMessage("ReconnectedTo",getCurrentPortName())); 461 resetupConnection(); 462 return; 463 } 464 if (count % 10==0) { 465 //retrying but with twice the retry interval. 466 interval = Math.min(interval * 2, reconnectMaxInterval); 467 log.error(Bundle.getMessage("ReconnectFailRetry", totalsleep, count,interval)); 468 } 469 if ((reconnectMaxAttempts > -1) && (count >= reconnectMaxAttempts)) { 470 log.error(Bundle.getMessage("ReconnectFailAbort",totalsleep,count)); 471 reply = false; 472 } 473 } 474 } 475 } 476 477 /** 478 * Initial interval between reconnection attempts. 479 * Default 1 second. 480 */ 481 protected int reconnectinterval = 1; 482 483 /** 484 * Maximum reconnection attempts that the port should make. 485 * Default 100 attempts. 486 * A value of -1 indicates unlimited attempts. 487 */ 488 protected int reconnectMaxAttempts = 100; 489 490 /** 491 * Maximum interval between reconnection attempts in seconds. 492 * Default 120 seconds. 493 */ 494 protected int reconnectMaxInterval = 120; 495 496 /** 497 * {@inheritDoc} 498 */ 499 @Override 500 public void setReconnectMaxInterval(int maxInterval) { 501 reconnectMaxInterval = maxInterval; 502 } 503 504 /** 505 * {@inheritDoc} 506 */ 507 @Override 508 public void setReconnectMaxAttempts(int maxAttempts) { 509 reconnectMaxAttempts = maxAttempts; 510 } 511 512 /** 513 * {@inheritDoc} 514 */ 515 @Override 516 public int getReconnectMaxInterval() { 517 return reconnectMaxInterval; 518 } 519 520 /** 521 * {@inheritDoc} 522 */ 523 @Override 524 public int getReconnectMaxAttempts() { 525 return reconnectMaxAttempts; 526 } 527 528 protected static void safeSleep(long milliseconds, String s) { 529 try { 530 Thread.sleep(milliseconds); 531 } catch (InterruptedException e) { 532 log.error("Sleep Exception raised during reconnection attempt{}", s); 533 } 534 } 535 536 @Override 537 public boolean isDirty() { 538 boolean isDirty = this.getSystemConnectionMemo().isDirty(); 539 if (!isDirty) { 540 for (Option option : this.options.values()) { 541 isDirty = option.isDirty(); 542 if (isDirty) { 543 break; 544 } 545 } 546 } 547 return isDirty; 548 } 549 550 @Override 551 public boolean isRestartRequired() { 552 // Override if any option should not be considered when determining if a 553 // change requires JMRI to be restarted. 554 return this.isDirty(); 555 } 556 557 /** 558 * Service method to purge a stream of initial contents 559 * while opening the connection. 560 * @param serialStream input data 561 * @throws IOException if the stream is e.g. closed due to failure to open the port completely 562 */ 563 @SuppressFBWarnings(value = "SR_NOT_CHECKED", justification = "skipping all, don't care what skip() returns") 564 public static void purgeStream(@Nonnull java.io.InputStream serialStream) throws IOException { 565 int count = serialStream.available(); 566 log.debug("input stream shows {} bytes available", count); 567 while (count > 0) { 568 serialStream.skip(count); 569 count = serialStream.available(); 570 } 571 } 572 573 /** 574 * Get the {@link SystemConnectionMemo} associated with this 575 * object. 576 * <p> 577 * This method should only be overridden to ensure that a specific subclass 578 * of SystemConnectionMemo is returned. The recommended pattern is: <code> 579 * public MySystemConnectionMemo getSystemConnectionMemo() { 580 * return (MySystemConnectionMemo) super.getSystemConnectionMemo(); 581 * } 582 * </code> 583 * 584 * @return the currently associated SystemConnectionMemo 585 */ 586 @Override 587 public SystemConnectionMemo getSystemConnectionMemo() { 588 return this.connectionMemo; 589 } 590 591 /** 592 * Set the {@link SystemConnectionMemo} associated with this 593 * object. 594 * <p> 595 * Overriding implementations must call 596 * <code>super.setSystemConnectionMemo(memo)</code> at some point to ensure 597 * the SystemConnectionMemo gets set. 598 * 599 * @param connectionMemo the SystemConnectionMemo to associate with this PortController 600 */ 601 @Override 602 @OverridingMethodsMustInvokeSuper 603 public void setSystemConnectionMemo(@Nonnull SystemConnectionMemo connectionMemo) { 604 if (connectionMemo == null) { 605 throw new NullPointerException(); 606 } 607 this.connectionMemo = connectionMemo; 608 } 609 610 private final static org.slf4j.Logger log = org.slf4j.LoggerFactory.getLogger(AbstractPortController.class); 611 612}