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}