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     protected 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}