001package jmri;
002
003import java.lang.reflect.InvocationTargetException;
004import java.util.Collection;
005import java.util.Iterator;
006import java.util.LinkedList;
007import java.util.List;
008import java.util.Objects;
009import java.util.SortedMap;
010import java.util.TreeMap;
011import javax.annotation.Nonnull;
012
013/**
014 * class to look after the collection of TurnoutOperation subclasses Unlike the
015 * other xxxManager, this does not inherit from AbstractManager since the
016 * resources it deals with are not DCC system resources but rather purely
017 * internal state.
018 *
019 * @author John Harper Copyright 2005
020 *
021 */
022public class TurnoutOperationManager implements InstanceManagerAutoDefault {
023
024    private final SortedMap<String, TurnoutOperation> turnoutOperations = new TreeMap<>();
025    private List<TurnoutOperation> operationTypes = new LinkedList<>(); // array of the defining instances of each class, held in order of appearance
026    boolean doOperations = false; // global on/off switch
027
028    public TurnoutOperationManager() {
029    }
030
031    private boolean initialized = false;
032
033    /** 
034     * Does deferred initialization.
035     * <p>
036     * This is deferred because it invokes
037     * loadOperationTypes, which gets the current turnout manager, often the
038     * proxy manager, which in turn can invoke loadOperationTypes again. 
039     */
040    private void initialize() {
041        if (!initialized) {
042            initialized = true;
043            // create the default instances of each of the known operation types
044            loadOperationTypes();
045        }
046    }
047
048    public void dispose() {
049    }
050
051    public TurnoutOperation[] getTurnoutOperations() {
052        synchronized (this) {
053            initialize();
054            Collection<TurnoutOperation> entries = turnoutOperations.values();
055            return entries.toArray(new TurnoutOperation[0]);
056        }
057    }
058
059    /**
060     * add a new operation Silently replaces any existing operation with the
061     * same name
062     *
063     * @param op {@link TurnoutOperation} to add/replace
064     */
065    protected void addOperation(@Nonnull TurnoutOperation op) {
066        Objects.requireNonNull(op, "TurnoutOperations cannot be null");
067        TurnoutOperation previous;
068        synchronized (this) {
069            initialize();
070            previous = turnoutOperations.put(op.getName(), op);
071            if (op.isDefinitive()) {
072                updateTypes(op);
073            }
074        }
075        if (previous != null) {
076            log.debug("replaced existing operation called {}", previous.getName());
077        }
078        firePropertyChange("Content", null, null);
079    }
080
081    protected void removeOperation(@Nonnull TurnoutOperation op) {
082        Objects.requireNonNull(op, "TurnoutOperations cannot be null");
083        synchronized (this) {
084            initialize();
085            turnoutOperations.remove(op.getName());
086        }
087        firePropertyChange("Content", null, null);
088    }
089
090    /**
091     * Find a TurnoutOperation by its name.
092     *
093     * @param name name of {@link TurnoutOperation} to retrieve.
094     * @return the operation
095     */
096    public TurnoutOperation getOperation(@Nonnull String name) {
097        synchronized (this) {
098            initialize();
099            return turnoutOperations.get(name);
100        }
101    }
102
103    /**
104     * update the list of types to include a new or updated definitive instance.
105     * since order is important we retain the existing order, placing a new type
106     * at the end if necessary
107     *
108     * @param op new or updated operation
109     */
110    private void updateTypes(@Nonnull TurnoutOperation op) {
111        initialize();
112        LinkedList<TurnoutOperation> newTypes = new LinkedList<>();
113        Iterator<TurnoutOperation> iter = operationTypes.iterator();
114        boolean found = false;
115        while (iter.hasNext()) {
116            TurnoutOperation item = iter.next();
117            if (item.getClass() == op.getClass()) {
118                newTypes.add(op);
119                found = true;
120                log.debug("replacing definitive instance of {}", item.getClass());
121            } else {
122                newTypes.add(item);
123            }
124        }
125        if (!found) {
126            newTypes.add(op);
127            log.debug("adding definitive instance of {}", op.getClass());
128        }
129        operationTypes = newTypes;
130    }
131
132    /**
133     * Load the operation types given by the current TurnoutManager instance, in
134     * the order given.
135     * <p>
136     * The order is important because the acceptable feedback modes may overlap.
137     * All we do is instantiate the classes. The constructors take care of
138     * putting everything in the right places. We allow multiple occurrences of
139     * the same name without complaining so the Proxy stuff works.
140     *
141     * There's a threading problem here, because this invokes gets the current
142     * turnout manager, often the proxy manager, which in turn invokes
143     * loadOperationTypes again. This is bad. It's not clear why it even works.
144     *
145     */
146    public void loadOperationTypes() {
147        String[] validTypes = InstanceManager.turnoutManagerInstance().getValidOperationTypes();
148        for (int i = 0; i < validTypes.length; ++i) {
149            String thisClassName = "jmri." + validTypes[i] + "TurnoutOperation";
150            if (validTypes[i] == null) {
151                log.warn("null operation name in loadOperationTypes");
152            } else if (getOperation(validTypes[i]) == null) {
153                try {
154                    Class<?> thisClass = Class.forName(thisClassName);
155                    // creating the instance invokes the TurnoutOperation ctor,
156                    // which calls addOperation here, which adds it to the 
157                    // turnoutOperations map.
158                    thisClass.getDeclaredConstructor().newInstance();
159                    log.debug("loaded TurnoutOperation class {}", thisClassName);
160                } catch (ClassNotFoundException | InstantiationException | IllegalAccessException | NoSuchMethodException | InvocationTargetException e1) {
161                    log.error("during loadOperationTypes", e1);
162                }
163            }
164        }
165    }
166
167    /**
168     * Find a suitable operation for this turnout, based on its feedback type.
169     * The mode is passed separately so the caller can transform it
170     *
171     * @param t            turnout
172     * @param apparentMode Turnout Feedback mode(s) to be used when finding a matching operation
173     * @return the turnout operation
174     */
175    public TurnoutOperation getMatchingOperationAlways(@Nonnull Turnout t, int apparentMode) {
176        initialize();
177        Iterator<TurnoutOperation> iter = operationTypes.iterator();
178        TurnoutOperation currentMatch = null;
179        /* The loop below always returns the LAST operation 
180         that matches.  In the standard feedback modes, 
181         This currently results in returning the NoFeedback 
182         operation, since it is the last one added to 
183         operationTypes */
184        while (iter.hasNext()) {
185            TurnoutOperation oper = iter.next();
186            if (oper.matchFeedbackMode(apparentMode)) {
187                currentMatch = oper;
188            }
189        }
190        if (currentMatch != null) {
191            return currentMatch;
192        } else {
193            return null;
194        }
195    }
196
197    /**
198     * Find the correct operation for this turnout.
199     * If operations are globally disabled, return null.
200     *
201     * @param t            turnout
202     * @param apparentMode mode(s) to be used when finding a matching operation
203     * @return operation
204     */
205    public TurnoutOperation getMatchingOperation(@Nonnull Turnout t, int apparentMode) {
206        initialize();
207        if (doOperations) {
208            return getMatchingOperationAlways(t, apparentMode);
209        }
210        return null;
211    }
212
213    public TurnoutOperation getMatchingOperationAlways(@Nonnull Turnout t) {
214        return getMatchingOperationAlways(t, t.getFeedbackMode());
215    }
216
217    /**
218     * Get ( potentially update ) status of whether operations are in use.
219     * @return true if in use, else false.
220     */
221    public boolean getDoOperations() {
222        initialize();
223        return doOperations;
224    }
225
226    /**
227     * Set that Turnout Operations are in use.
228     * @param b true to use, else false to disable.
229     */
230    public void setDoOperations(boolean b) {
231        initialize();
232        boolean oldValue = doOperations;
233        doOperations = b;
234        firePropertyChange("doOperations", oldValue, b);
235    }
236
237    /**
238     * Proxy support. Take a concatenation of operation type lists from multiple
239     * systems and turn it into a single list, by eliminating duplicates and
240     * ensuring that NoFeedback - which matches anything - comes at the end if
241     * it is present at all.
242     *
243     * @param types list of types possibly containing duplicates
244     * @return list reduced as described above
245     */
246    static public String[] concatenateTypeLists(@Nonnull String[] types) {
247        List<String> outTypes = new LinkedList<>();
248        boolean noFeedbackWanted = false;
249        for (String type : types) {
250            if ("NoFeedback".equals(type)) {
251                noFeedbackWanted = true;
252            } else if (type == null || type.isEmpty()) {
253                log.warn("null or empty operation name returned from turnout manager");
254            } else if (!outTypes.contains(type)) {
255                outTypes.add(type);
256            }
257        }
258        if (noFeedbackWanted) {
259            outTypes.add("NoFeedback");
260        }
261        return outTypes.toArray(new String[0]);
262    }
263
264    /**
265     * Property change support.
266     */
267    java.beans.PropertyChangeSupport pcs = new java.beans.PropertyChangeSupport(this);
268
269    public synchronized void addPropertyChangeListener(@Nonnull java.beans.PropertyChangeListener l) {
270        pcs.addPropertyChangeListener(l);
271    }
272
273    public synchronized void removePropertyChangeListener(@Nonnull java.beans.PropertyChangeListener l) {
274        pcs.removePropertyChangeListener(l);
275    }
276
277    protected void firePropertyChange(@Nonnull String p, Object old, Object n) {
278        pcs.firePropertyChange(p, old, n);
279    }
280
281    /**
282     * Get a ToolTip or descriptive comment for the Operator.
283     * @param operatorName name of the Turnout Operator
284     * @param t The Turnout that the Operator would operate
285     * @return Descriptive String, or null.
286     */
287    public String getTooltipForOperator(String operatorName, Turnout t){
288        if (operatorName == null){
289            return null;
290        }
291        if ( operatorName.equals(Bundle.getMessage("TurnoutOperationOff"))) {
292            return Bundle.getMessage("TurnoutOperationOffTip");
293        }
294        if ( t != null && operatorName.equals(Bundle.getMessage("TurnoutOperationDefault"))) {
295            return Bundle.getMessage("UseGlobal", getMatchingOperationAlways(t).getName());
296        }
297        for ( TurnoutOperation to : getTurnoutOperations() ) {
298            if (operatorName.equals(to.getName())) {
299                return to.getToolTip();
300            }
301        }
302        return null;
303    }
304
305    private final static org.slf4j.Logger log = org.slf4j.LoggerFactory.getLogger(TurnoutOperationManager.class);
306}