001package jmri.managers;
002
003import java.beans.PropertyChangeEvent;
004import java.beans.PropertyChangeListener;
005import java.util.ArrayList;
006import java.util.HashMap;
007import java.util.HashSet;
008import java.util.List;
009import java.util.Locale;
010import java.util.Map;
011import java.util.Set;
012import java.util.prefs.BackingStoreException;
013import java.util.prefs.Preferences;
014import javax.annotation.CheckForNull;
015import javax.annotation.Nonnull;
016import jmri.AddressedProgrammerManager;
017import jmri.CommandStation;
018import jmri.ConfigureManager;
019import jmri.ConsistManager;
020import jmri.GlobalProgrammerManager;
021import jmri.InstanceManager;
022import jmri.PowerManager;
023import jmri.ThrottleManager;
024import jmri.SystemConnectionMemo;
025import jmri.jmrix.SystemConnectionMemoManager;
026import jmri.jmrix.internal.InternalSystemConnectionMemo;
027import jmri.profile.Profile;
028import jmri.profile.ProfileUtils;
029import jmri.spi.PreferencesManager;
030import jmri.util.prefs.AbstractPreferencesManager;
031import jmri.util.prefs.InitializationException;
032import org.openide.util.lookup.ServiceProvider;
033
034/**
035 * Records and executes a desired set of defaults for the JMRI InstanceManager
036 * and ProxyManagers.
037 * <p>
038 * Provided that a connection provides a default, this verifies, unless the
039 * per-profile property {@code jmri-managers.allInternalDefaults} is
040 * {@code true}, that a non-Internal connection (other than type None in the
041 * preferences window) is the default for at least one type of manager.
042 * <p>
043 * allInternalDefaults is preserved as a preference when set here, but
044 * {@link #setAllInternalDefaultsValid} is not (originally) invoked from the
045 * GUI.
046 *
047 * @author Bob Jacobsen Copyright (C) 2010
048 * @author Randall Wood Copyright (C) 2015, 2017
049 * @since 2.9.4
050 * @see jmri.SystemConnectionMemo#provides(java.lang.Class)
051 */
052@ServiceProvider(service = PreferencesManager.class)
053public class ManagerDefaultSelector extends AbstractPreferencesManager {
054
055    public final HashMap<Class<?>, String> defaults = new HashMap<>();
056    private PropertyChangeListener memoListener;
057    private boolean allInternalDefaultsValid = false;
058    public final static String ALL_INTERNAL_DEFAULTS = "allInternalDefaults";
059
060    public ManagerDefaultSelector() {
061        memoListener = (PropertyChangeEvent e) -> {
062            log.trace("memoListener fired via {}", e);
063            switch (e.getPropertyName()) {
064                case SystemConnectionMemo.USER_NAME:
065                    String oldName = (String) e.getOldValue();
066                    String newName = (String) e.getNewValue();
067                    log.debug("ConnectionNameChanged from \"{}\" to \"{}\"", oldName, newName);
068                    // Takes a copy of the keys to avoid ConcurrentModificationException.
069                    new HashSet<>(defaults.keySet()).forEach((c) -> {
070                        String connectionName = this.defaults.get(c);
071                        if (connectionName.equals(oldName)) {
072                            ManagerDefaultSelector.this.defaults.put(c, newName);
073                        }
074                    });
075                    this.firePropertyChange("Updated", null, null);
076                    break;
077                case SystemConnectionMemo.DISABLED:
078                    Boolean newState = (Boolean) e.getNewValue();
079                    if (newState) {
080                        String disabledName = ((SystemConnectionMemo) e.getSource()).getUserName();
081                        log.debug("ConnectionDisabled true: \"{}\"", disabledName);
082                        removeConnectionAsDefault(disabledName);
083                    }
084                    break;
085                default:
086                    log.debug("ignoring notification of \"{}\"", e.getPropertyName());
087                    break;
088            }
089        };
090        SystemConnectionMemoManager.getDefault().addPropertyChangeListener((PropertyChangeEvent e) -> {
091            //
092            // Note that when JMRI is starting, this listener does
093            // trigger as connections are added, but that after the
094            // configured connections are set, the defaults are reset
095            // when configure(Profile) is called. We do, however,
096            // want these to be set immediately when a new profile
097            // is launched for the first time, so these listeners
098            // need to be in place as early as possible.
099            //
100            log.trace("addPropertyChangeListener fired via {}", e);
101            switch (e.getPropertyName()) {
102                case SystemConnectionMemoManager.CONNECTION_REMOVED:
103                    if (e.getOldValue() instanceof SystemConnectionMemo) {
104                        SystemConnectionMemo memo = (SystemConnectionMemo) e.getOldValue();
105                        String removedName = ((SystemConnectionMemo) e.getOldValue()).getUserName();
106                        log.debug("ConnectionRemoved for \"{}\"", removedName);
107                        removeConnectionAsDefault(removedName);
108                        memo.removePropertyChangeListener(this.memoListener);
109                    }
110                    break;
111                case SystemConnectionMemoManager.CONNECTION_ADDED:
112                    if (e.getNewValue() instanceof SystemConnectionMemo) {
113                        SystemConnectionMemo memo = (SystemConnectionMemo) e.getNewValue();
114                        memo.addPropertyChangeListener(this.memoListener);
115                        // check for special case of anything else then Internal
116                        // and set first system to be default for all provided defaults
117                        List<SystemConnectionMemo> list = InstanceManager.getList(SystemConnectionMemo.class);
118
119                        if (log.isDebugEnabled()) {
120                            log.debug("Start CONNECTION_ADDED processing with {} existing", list.size());
121                            for (int i = 0; i < list.size(); i++) {
122                                log.debug("    System {}: {} (\"{}\")", i, list.get(i), list.get(i).getUserName());
123                            }
124                        }
125
126                        if ((list.size() == 1 && !(list.get(0) instanceof InternalSystemConnectionMemo)) ||
127                                (list.size() == 2 && !(list.get(0) instanceof InternalSystemConnectionMemo) && list.get(1) instanceof InternalSystemConnectionMemo)) {
128                            // first system added is hardware, gets defaults for everything it supports
129                            log.debug("First real system added, reset defaults");
130                            for (Item item : knownManagers) {
131                                if (memo.provides(item.managerClass)) {
132                                    this.setDefault(item.managerClass, memo.getUserName());
133                                }
134                            }
135                        }
136                        // any new connection that provides a missing default
137                        // gets set as the default for that missing default
138                        // use new HashSet over this.defaults.keySet to avoid
139                        // ConcurrentModificationException on this.defaults
140                        new HashSet<>(defaults.keySet()).forEach((cls) -> {
141                            String userName = defaults.get(cls);
142                            if (userName == null && memo.provides(cls)) {
143                                this.setDefault(cls, memo.getUserName());
144                            }
145                        });
146                    }
147                    break;
148                default:
149                    log.debug("ignoring notification of \"{}\"", e.getPropertyName());
150                    break;
151            }
152        });
153        InstanceManager.getList(SystemConnectionMemo.class).forEach((memo) -> {
154            memo.addPropertyChangeListener(this.memoListener);
155        });
156    }
157
158    // remove connection's record
159    void removeConnectionAsDefault(String removedName) {
160        ArrayList<Class<?>> tmpArray = new ArrayList<>();
161        defaults.keySet().stream().forEach((c) -> {
162            String connectionName = ManagerDefaultSelector.this.defaults.get(c);
163            if (connectionName.equals(removedName)) {
164                log.debug("Connection {} has been removed as the default for {}", removedName, c);
165                tmpArray.add(c);
166            }
167        });
168        tmpArray.stream().forEach((tmpArray1) -> {
169            ManagerDefaultSelector.this.defaults.remove(tmpArray1);
170        });
171        this.firePropertyChange("Updated", null, null);
172    }
173
174    /**
175     * Return the userName of the system that provides the default instance for
176     * a specific class.
177     *
178     * @param managerClass the specific type, for example, TurnoutManager, for
179     *                     which a default system is desired
180     * @return userName of the system, or null if none set
181     */
182    public String getDefault(Class<?> managerClass) {
183        return defaults.get(managerClass);
184    }
185
186    /**
187     * Record the userName of the system that provides the default instance for
188     * a specific class.
189     * <p>
190     * To ensure compatibility of different preference versions, only classes
191     * that are current registered are preserved. This way, reading in an old
192     * file will just have irrelevant items ignored.
193     *
194     * @param managerClass the specific type, for example, TurnoutManager, for
195     *                     which a default system is desired
196     * @param userName     of the system, or null if none set
197     */
198    public void setDefault(Class<?> managerClass, String userName) {
199        for (Item item : knownManagers) {
200            if (item.managerClass.equals(managerClass)) {
201                log.debug("   setting default for \"{}\" to \"{}\" by request", managerClass, userName);
202                defaults.put(managerClass, userName);
203                return;
204            }
205        }
206        log.warn("Ignoring preference for class {} with name {}", managerClass, userName);
207    }
208
209    /**
210     * Load into InstanceManager
211     *
212     * @param profile the profile to configure against
213     * @return an exception that can be passed to the user or null if no errors
214     *         occur
215     */
216    @CheckForNull
217    @SuppressWarnings({"unchecked", "rawtypes"})
218    public InitializationException configure(Profile profile) {
219        InitializationException error = null;
220        List<SystemConnectionMemo> connList = InstanceManager.getList(SystemConnectionMemo.class);
221        log.debug("configure defaults into InstanceManager from {} memos, {} defaults", connList.size(), defaults.keySet().size());
222        // Takes a copy to avoid ConcurrentModificationException.
223        Set<Class<?>> keys = new HashSet<>(defaults.keySet());
224        for (Class<?> c : keys) {
225            // 'c' is the class to load
226            String connectionName = defaults.get(c);
227            // have to find object of that type from proper connection
228            boolean found = false;
229            for (SystemConnectionMemo memo : connList) {
230                String testName = memo.getUserName();
231                if (testName.equals(connectionName)) {
232                    found = true;
233                    // match, store
234                    try {
235                        if (memo.provides(c)) {
236                            log.debug("   setting default for \"{}\" to \"{}\" in configure", c, memo.get(c));
237                            InstanceManager.setDefault((Class)c, (Object)memo.get(c));  // Java generics doesn't work in this case to type cast to (Class) and (Object)
238                        }
239                    } catch (NullPointerException ex) {
240                        String englishMsg = Bundle.getMessage(Locale.ENGLISH, "ErrorNullDefault", memo.getUserName(), c); // NOI18N
241                        String localizedMsg = Bundle.getMessage("ErrorNullDefault", memo.getUserName(), c); // NOI18N
242                        error = new InitializationException(englishMsg, localizedMsg);
243                        log.warn("SystemConnectionMemo for {} ({}) provides a null {} instance", memo.getUserName(), memo.getClass(), c);
244                    }
245                    break;
246                } else {
247                    log.debug("   memo name didn't match: {} vs {}", testName, connectionName);
248                }
249            }
250            /*
251             * If the set connection can not be found then we shall set the manager default to use what
252             * has currently been set.
253             */
254            if (!found) {
255                log.debug("!found, so resetting");
256                String currentName = null;
257                if (c == ThrottleManager.class && InstanceManager.getOptionalDefault(ThrottleManager.class).isPresent()) {
258                    currentName = InstanceManager.throttleManagerInstance().getUserName();
259                } else if (c == PowerManager.class && InstanceManager.getOptionalDefault(PowerManager.class).isPresent()) {
260                    currentName = InstanceManager.getDefault(PowerManager.class).getUserName();
261                }
262                if (currentName != null) {
263                    log.warn("The configured {} for {} can not be found so will use the default {}", connectionName, c, currentName);
264                    this.defaults.put(c, currentName);
265                }
266            }
267        }
268        if (!isPreferencesValid(profile, connList)) {
269            error = new InitializationException(Bundle.getMessage(Locale.ENGLISH, "ManagerDefaultSelector.AllInternal"), Bundle.getMessage("ManagerDefaultSelector.AllInternal"));
270        }
271        return error;
272    }
273
274    // Define set of items that we remember defaults for, manually maintained because
275    // there are lots of JMRI-internal types of no interest to the user and/or not system-specific.
276    // This grows if you add something to the SystemConnectionMemo system
277    final public Item[] knownManagers = new Item[]{
278        new Item("<html>Throttles</html>", ThrottleManager.class),
279        new Item("<html>Power<br>Control</html>", PowerManager.class),
280        new Item("<html>Command<br>Station</html>", CommandStation.class),
281        new Item("<html>Service<br>Programmer</html>", GlobalProgrammerManager.class),
282        new Item("<html>Ops Mode<br>Programmer</html>", AddressedProgrammerManager.class),
283        new Item("<html>Consists</html>", ConsistManager.class)
284    };
285
286    @Override
287    public void initialize(Profile profile) throws InitializationException {
288        if (!this.isInitialized(profile)) {
289            Preferences preferences = ProfileUtils.getPreferences(profile, this.getClass(), true); // NOI18N
290            Preferences defaultsPreferences = preferences.node("defaults");
291            try {
292                for (String name : defaultsPreferences.keys()) {
293                    String connection = defaultsPreferences.get(name, null);
294                    Class<?> cls = this.classForName(name);
295                    log.debug("Loading default {} for {}", connection, name);
296                    if (cls != null) {
297                        this.defaults.put(cls, connection);
298                        log.debug("Loaded default {} for {}", connection, cls);
299                    }
300                }
301                this.allInternalDefaultsValid = preferences.getBoolean(ALL_INTERNAL_DEFAULTS, this.allInternalDefaultsValid);
302            } catch (BackingStoreException ex) {
303                log.info("Unable to read preferences for Default Selector.");
304            }
305            InitializationException ex = this.configure(profile);
306            InstanceManager.getOptionalDefault(ConfigureManager.class).ifPresent((manager) -> {
307                manager.registerPref(this); // allow profile configuration to be written correctly
308            });
309            this.setInitialized(profile, true);
310            if (ex != null) {
311                this.addInitializationException(profile, ex);
312                throw ex;
313            }
314        }
315    }
316
317    @Override
318    public void savePreferences(Profile profile) {
319        Preferences preferences = ProfileUtils.getPreferences(profile, this.getClass(), true); // NOI18N
320        Preferences defaultsPreferences = preferences.node("defaults");
321        try {
322            this.defaults.keySet().stream().forEach((cls) -> {
323                defaultsPreferences.put(this.nameForClass(cls), this.defaults.get(cls));
324            });
325            preferences.putBoolean(ALL_INTERNAL_DEFAULTS, this.allInternalDefaultsValid);
326            preferences.sync();
327        } catch (BackingStoreException ex) {
328            log.error("Unable to save preferences for Default Selector.", ex);
329        }
330    }
331
332    private boolean isPreferencesValid(Profile profile, List<SystemConnectionMemo> connections) {
333        log.trace("isPreferencesValid start");
334        if (allInternalDefaultsValid) {
335            log.trace("allInternalDefaultsValid returns true");
336            return true;
337        }
338        boolean usesExternalConnections = false;
339
340        // classes of managers being provided, and set of which SystemConnectionMemos can provide each
341        Map<Class<?>, Set<SystemConnectionMemo>> providing = new HashMap<>();
342
343        // list of all external providers (i.e. SystemConnectionMemos) that provide at least one known manager type
344        Set<SystemConnectionMemo> providers = new HashSet<>();
345
346        if (connections.size() > 1) {
347            connections.stream().filter((memo) -> (!(memo instanceof InternalSystemConnectionMemo))).forEachOrdered((memo) -> {
348                // populate providers by adding all external (non-internal) connections that provide at least one default
349                for (Item item : knownManagers) {
350                    if (memo.provides(item.managerClass)) {
351                        providers.add(memo);
352                        break;
353                    }
354                }
355            });
356            // if there are no external providers of managers, no further checks are needed
357            if (providers.size() >= 1) {
358                // build a list of defaults provided by external connections
359                providers.stream().forEach((memo) -> {
360                    for (Item item : knownManagers) {
361                        if (memo.provides(item.managerClass)) {
362                            Set<SystemConnectionMemo> provides = providing.getOrDefault(item.managerClass, new HashSet<>());
363                            provides.add(memo);
364                            providing.put(item.managerClass, provides);
365                        }
366                    }
367                });
368
369                if (log.isDebugEnabled()) {
370                    // avoid unneeded overhead of looping through providers
371                    providing.forEach((cls, clsProviders) -> {
372                        log.debug("{} default provider is {}, is provided by:", cls.getName(), defaults.get(cls));
373                        clsProviders.forEach((provider) -> {
374                            log.debug("   user name: {}", provider.getUserName());
375                        });
376                    });
377                }
378
379                for (SystemConnectionMemo memo : providers) {
380                    if (providing.keySet().stream().filter((cls) -> {
381                        Set<SystemConnectionMemo> provides = providing.get(cls);
382                        log.debug("{} is provided by {} out of {} connections", cls.getName(), provides.size(), providers.size());
383                        log.trace("memo stream returns {} due to producers.size() {}", (provides.size() > 0), provides.size());
384                        return (provides.size() > 0);
385                    }).anyMatch((cls) -> {
386                        log.debug("{} has an external default", cls);
387                        if (defaults.get(cls) == null) {
388                            log.trace("memo stream returns true because there's no default defined and an external provider exists");
389                            return true;
390                        }
391                        log.trace("memo stream returns {} due to memo.getUserName() {} and {}", (memo.getUserName().equals(defaults.get(cls))), memo.getUserName(), defaults.get(cls));
392                        return memo.getUserName().equals(defaults.get(cls));
393                    })) {
394                        log.trace("setting usesExternalConnections true");
395                        usesExternalConnections = true;
396                        // no need to check further
397                        break;
398                    }
399                }
400            }
401        }
402        log.trace("method end returns {} due to providers.size() {} and usesExternalConnections {}", (providers.size() >= 1 ? usesExternalConnections : true), providers.size(), usesExternalConnections);
403        return providers.size() >= 1 ? usesExternalConnections : true;
404    }
405
406    public boolean isPreferencesValid(Profile profile) {
407        return isPreferencesValid(profile, InstanceManager.getList(SystemConnectionMemo.class));
408    }
409
410    public static class Item {
411
412        public String typeName;
413        public Class<?> managerClass;
414
415        Item(String typeName, Class<?> managerClass) {
416            this.typeName = typeName;
417            this.managerClass = managerClass;
418        }
419    }
420
421    private String nameForClass(@Nonnull Class<?> cls) {
422        return cls.getCanonicalName().replace('.', '-');
423    }
424
425    private Class<?> classForName(@Nonnull String name) {
426        try {
427            return Class.forName(name.replace('-', '.'));
428        } catch (ClassNotFoundException ex) {
429            log.error("Could not find class for {}", name);
430            return null;
431        }
432    }
433
434    /**
435     * Check if having all defaults assigned to internal connections should be
436     * considered is valid in the presence of an external System Connection.
437     *
438     * @return true if having all internal defaults should be valid; false
439     *         otherwise
440     */
441    public boolean isAllInternalDefaultsValid() {
442        return allInternalDefaultsValid;
443    }
444
445    /**
446     * Set if having all defaults assigned to internal connections should be
447     * considered is valid in the presence of an external System Connection.
448     *
449     * @param isAllInternalDefaultsValid true if having all internal defaults
450     *                                   should be valid; false otherwise
451     */
452    public void setAllInternalDefaultsValid(boolean isAllInternalDefaultsValid) {
453        this.allInternalDefaultsValid = isAllInternalDefaultsValid;
454    }
455
456    private final static org.slf4j.Logger log = org.slf4j.LoggerFactory.getLogger(ManagerDefaultSelector.class);
457}