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    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    @edu.umd.cs.findbugs.annotations.SuppressFBWarnings(value = "PZLA_PREFER_ZERO_LENGTH_ARRAYS",
223    justification = "availability was checked before, should never get here")
224    public String getOptionDisplayName(String option) {
225        if (options.containsKey(option)) {
226            return options.get(option).getDisplayText();
227        }
228        return null;
229    }
230
231    @Override
232    public boolean isOptionAdvanced(String option) {
233        if (options.containsKey(option)) {
234            return options.get(option).isAdvanced();
235        }
236        return false;
237    }
238
239    protected HashMap<String, Option> options = new HashMap<>();
240
241    static protected class Option {
242
243        public enum Type {
244            JCOMBOBOX,
245            TEXT
246        }
247        
248        String currentValue = null;
249        
250        /** 
251         * As a heuristic, we consider the 1st non-null
252         * currentValue as the configured value. Changes away from that
253         * mark an Option object as "dirty".
254         */
255        String configuredValue = null;
256        
257        String displayText;
258        String[] options;
259        Type type;
260        
261        Boolean advancedOption = true;  // added options in advanced section by default
262
263        public Option(String displayText, @Nonnull String[] options, boolean advanced, Type type) {
264            this.displayText = displayText;
265            this.options = java.util.Arrays.copyOf(options, options.length);
266            this.advancedOption = advanced;
267            this.type = type;            
268        }
269
270        public Option(String displayText, String[] options, boolean advanced) {
271            this(displayText, options, advanced, Type.JCOMBOBOX);
272        }
273
274        public Option(String displayText, String[] options, Type type) {
275            this(displayText, options, true, type);
276        }
277
278        public Option(String displayText, String[] options) {
279            this(displayText, options, true, Type.JCOMBOBOX);
280        }
281
282        void configure(String value) {
283            log.trace("Option.configure({}) with \"{}\", \"{}\"", value, configuredValue, currentValue);
284            if (configuredValue == null ) {
285                configuredValue = value;
286            }
287            currentValue = value;
288        }
289
290        String getCurrent() {
291            if (currentValue == null) {
292                return options[0];
293            }
294            return currentValue;
295        }
296
297        String[] getOptions() {
298            return options;
299        }
300
301        Type getType() {
302            return type;
303        }
304
305        String getDisplayText() {
306            return displayText;
307        }
308
309        boolean isAdvanced() {
310            return advancedOption;
311        }
312
313        boolean isDirty() {
314            return (currentValue != null && !currentValue.equals(configuredValue));
315        }
316    }
317
318    @Override
319    public String getManufacturer() {
320        return manufacturerName;
321    }
322
323    @Override
324    public void setManufacturer(String manufacturer) {
325        log.debug("update manufacturer from {} to {}", this.manufacturerName, manufacturer);
326        this.manufacturerName = manufacturer;
327    }
328
329    @Override
330    public boolean getDisabled() {
331        return this.getSystemConnectionMemo().getDisabled();
332    }
333
334    /**
335     * Set the connection disabled or enabled. By default connections are
336     * enabled.
337     *
338     * If the implementing class does not use a
339     * {@link SystemConnectionMemo}, this method must be overridden.
340     * Overriding methods must call <code>super.setDisabled(boolean)</code> to
341     * ensure the configuration change state is correctly set.
342     *
343     * @param disabled true if connection should be disabled
344     */
345    @Override
346    public void setDisabled(boolean disabled) {
347        this.getSystemConnectionMemo().setDisabled(disabled);
348    }
349
350    @Override
351    public String getSystemPrefix() {
352        return this.getSystemConnectionMemo().getSystemPrefix();
353    }
354
355    @Override
356    public void setSystemPrefix(String systemPrefix) {
357        if (!this.getSystemConnectionMemo().setSystemPrefix(systemPrefix)) {
358            throw new IllegalArgumentException();
359        }
360    }
361
362    @Override
363    public String getUserName() {
364        return this.getSystemConnectionMemo().getUserName();
365    }
366
367    @Override
368    public void setUserName(String userName) {
369        if (!this.getSystemConnectionMemo().setUserName(userName)) {
370            throw new IllegalArgumentException();
371        }
372    }
373
374    protected boolean allowConnectionRecovery = false;
375
376    /**
377     * {@inheritDoc}
378     * After checking the allowConnectionRecovery flag, closes the 
379     * connection, resets the open flag and attempts a reconnection.
380     */
381    @Override
382    public void recover() {
383        if (!allowConnectionRecovery) {
384            return;
385        }
386        opened = false;
387        try {
388            closeConnection();
389        } 
390        catch (RuntimeException e) {
391            log.warn("closeConnection failed");
392        }
393        reconnect();
394    }
395    
396    /**
397     * Abstract class for controllers to close the connection.
398     * Called prior to any re-connection attempts.
399     */
400    protected void closeConnection(){}
401    
402    /**
403     * Attempts to reconnect to a failed port.
404     * Starts a reconnect thread
405     */
406    protected void reconnect() {
407        // If the connection is already open, then we shouldn't try a re-connect.
408        if (opened || !allowConnectionRecovery) {
409            return;
410        }
411        Thread thread = jmri.util.ThreadingUtil.newThread(new ReconnectWait(),
412            "Connection Recovery " + getCurrentPortName());
413        thread.start();
414        try {
415            thread.join();
416        } catch (InterruptedException e) {
417            log.error("Unable to join to the reconnection thread");
418        }
419    }
420    
421    /**
422     * Abstract class for controllers to re-setup a connection.
423     * Called on connection reconnect success.
424     */
425    protected void resetupConnection(){}
426    
427    /**
428     * Abstract class for ports to attempt a single re-connection attempt.
429     * Called from within main reconnect thread.
430     * @param retryNum Reconnection attempt number.
431     */
432    protected void reconnectFromLoop(int retryNum){}
433    
434    private class ReconnectWait extends Thread {
435        @Override
436        public void run() {
437            boolean reply = true;
438            int count = 0;
439            int interval = reconnectinterval;
440            int totalsleep = 0;
441            while (reply && allowConnectionRecovery) {
442                safeSleep(interval*1000L, "Waiting");
443                count++;
444                totalsleep += interval;
445                reconnectFromLoop(count);
446                reply = !opened;
447                if (opened){
448                    log.info(Bundle.getMessage("ReconnectedTo",getCurrentPortName()));
449                    resetupConnection();
450                    return;
451                }
452                if (count % 10==0) {
453                    //retrying but with twice the retry interval.
454                    interval = Math.min(interval * 2, reconnectMaxInterval);
455                    log.error(Bundle.getMessage("ReconnectFailRetry", totalsleep, count,interval));
456                }
457                if ((reconnectMaxAttempts > -1) && (count >= reconnectMaxAttempts)) {
458                    log.error(Bundle.getMessage("ReconnectFailAbort",totalsleep,count));
459                    reply = false;
460                }
461            }
462        }
463    }
464    
465    /**
466     * Initial interval between reconnection attempts.
467     * Default 1 second.
468     */
469    protected int reconnectinterval = 1;
470    
471    /**
472     * Maximum reconnection attempts that the port should make.
473     * Default 100 attempts.
474     * A value of -1 indicates unlimited attempts.
475     */
476    protected int reconnectMaxAttempts = 100;
477
478    /**
479     * Maximum interval between reconnection attempts in seconds.
480     * Default 120 seconds.
481     */
482    protected int reconnectMaxInterval = 120;
483    
484    /**
485     * {@inheritDoc}
486     */
487    @Override
488    public void setReconnectMaxInterval(int maxInterval) {
489        reconnectMaxInterval = maxInterval;
490    }
491    
492    /**
493     * {@inheritDoc}
494     */
495    @Override
496    public void setReconnectMaxAttempts(int maxAttempts) {
497        reconnectMaxAttempts = maxAttempts;
498    }
499    
500    /**
501     * {@inheritDoc}
502     */
503    @Override
504    public int getReconnectMaxInterval() {
505        return reconnectMaxInterval;
506    }
507    
508    /**
509     * {@inheritDoc}
510     */
511    @Override
512    public int getReconnectMaxAttempts() {
513        return reconnectMaxAttempts;
514    }
515    
516    protected static void safeSleep(long milliseconds, String s) {
517        try {
518            Thread.sleep(milliseconds);
519        } catch (InterruptedException e) {
520            log.error("Sleep Exception raised during reconnection attempt{}", s);
521        }
522    }
523
524    @Override
525    public boolean isDirty() {
526        boolean isDirty = this.getSystemConnectionMemo().isDirty();
527        if (!isDirty) {
528            for (Option option : this.options.values()) {
529                isDirty = option.isDirty();
530                if (isDirty) {
531                    break;
532                }
533            }
534        }
535        return isDirty;
536    }
537
538    @Override
539    public boolean isRestartRequired() {
540        // Override if any option should not be considered when determining if a
541        // change requires JMRI to be restarted.
542        return this.isDirty();
543    }
544
545    /**
546     * Service method to purge a stream of initial contents
547     * while opening the connection.
548     * @param serialStream input data
549     * @throws java.io.IOException from underlying operations
550     */
551     @SuppressFBWarnings(value = "SR_NOT_CHECKED", justification = "skipping all, don't care what skip() returns")
552     protected void purgeStream(@Nonnull java.io.InputStream serialStream) throws java.io.IOException {
553        int count = serialStream.available();
554         log.debug("input stream shows {} bytes available", count);
555        while (count > 0) {
556            serialStream.skip(count);
557            count = serialStream.available();
558        }
559    }
560    
561    /**
562     * Get the {@link SystemConnectionMemo} associated with this
563     * object.
564     * <p>
565     * This method should only be overridden to ensure that a specific subclass
566     * of SystemConnectionMemo is returned. The recommended pattern is: <code>
567     * public MySystemConnectionMemo getSystemConnectionMemo() {
568     *  return (MySystemConnectionMemo) super.getSystemConnectionMemo();
569     * }
570     * </code>
571     *
572     * @return the currently associated SystemConnectionMemo
573     */
574    @Override
575    public SystemConnectionMemo getSystemConnectionMemo() {
576        return this.connectionMemo;
577    }
578
579    /**
580     * Set the {@link SystemConnectionMemo} associated with this
581     * object.
582     * <p>
583     * Overriding implementations must call
584     * <code>super.setSystemConnectionMemo(memo)</code> at some point to ensure
585     * the SystemConnectionMemo gets set.
586     *
587     * @param connectionMemo the SystemConnectionMemo to associate with this PortController
588     */
589    @Override
590    @OverridingMethodsMustInvokeSuper
591    public void setSystemConnectionMemo(@Nonnull SystemConnectionMemo connectionMemo) {
592        if (connectionMemo == null) {
593            throw new NullPointerException();
594        }
595        this.connectionMemo = connectionMemo;
596    }
597
598    private final static Logger log = LoggerFactory.getLogger(AbstractPortController.class);
599
600}