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}