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}