001package jmri.util.startup;
002
003import java.beans.PropertyChangeEvent;
004import java.beans.PropertyChangeListener;
005import java.util.ArrayList;
006import java.util.Arrays;
007import java.util.HashMap;
008import java.util.HashSet;
009import java.util.Map.Entry;
010import java.util.ServiceLoader;
011import javax.annotation.Nonnull;
012import javax.annotation.CheckForNull;
013import jmri.Disposable;
014import jmri.InstanceManager;
015import jmri.beans.Bean;
016import jmri.SystemConnectionMemo;
017import jmri.jmrix.swing.SystemConnectionAction;
018import org.slf4j.Logger;
019import org.slf4j.LoggerFactory;
020
021/**
022 * Maintain a list of actions that can be used by
023 * {@link jmri.util.startup.AbstractActionModel} and its descendants. This list is
024 * populated by {@link StartupActionFactory} instances
025 * registered with a {@link java.util.ServiceLoader}.
026 *
027 * @author Randall Wood Copyright 2016, 2020
028 */
029public class StartupActionModelUtil extends Bean implements Disposable {
030
031    private HashMap<Class<?>, ActionAttributes> actions = null;
032    private HashMap<String, Class<?>> overrides = null;
033    private HashSet<Class<?>> serviceClasses = null; // offered by ServiceLoader factories, never removed
034    private ArrayList<String> actionNames = null; // built on demand, invalidated in changes to actions
035    private final PropertyChangeListener memosListener = this::memoChanged;
036    private final PropertyChangeListener actionFactoryListener = this::actionFactoryChanged;
037    private static final Logger log = LoggerFactory.getLogger(StartupActionModelUtil.class);
038
039    /**
040     * Get the default StartupActionModelUtil instance, creating it if
041     * necessary.
042     *
043     * @return the default instance
044     */
045    @Nonnull
046    public static StartupActionModelUtil getDefault() {
047        return InstanceManager.getOptionalDefault(StartupActionModelUtil.class).orElseGet(() -> {
048            return InstanceManager.setDefault(StartupActionModelUtil.class, new StartupActionModelUtil());
049        });
050    }
051
052    public StartupActionModelUtil() {
053        InstanceManager.addPropertyChangeListener(InstanceManager.getListPropertyName(SystemConnectionMemo.class), memosListener);
054    }
055
056    @CheckForNull
057    public String getActionName(@Nonnull Class<?> clazz) {
058        this.prepareActionsHashMap();
059        ActionAttributes attrs = this.actions.get(clazz);
060        return attrs != null ? attrs.name : null;
061    }
062
063    @CheckForNull
064    public String getActionName(@Nonnull String className) {
065        if (!className.isEmpty()) {
066            try {
067                return this.getActionName(Class.forName(className));
068            } catch (ClassNotFoundException ex) {
069                log.error("Did not find class \"{}\"", className);
070            }
071        }
072        return null;
073    }
074
075    public boolean isSystemConnectionAction(@Nonnull Class<?> clazz) {
076        this.prepareActionsHashMap();
077        if (this.actions.containsKey(clazz)) {
078            return this.actions.get(clazz).isSystemConnectionAction;
079        }
080        return false;
081    }
082
083    public boolean isSystemConnectionAction(@Nonnull String className) {
084        if (!className.isEmpty()) {
085            try {
086                return this.isSystemConnectionAction(Class.forName(className));
087            } catch (ClassNotFoundException ex) {
088                log.error("Did not find class \"{}\"", className);
089            }
090        }
091        return false;
092    }
093
094    @CheckForNull
095    public String getClassName(@CheckForNull String name) {
096        if (name != null && !name.isEmpty()) {
097            this.prepareActionsHashMap();
098            for (Entry<Class<?>, ActionAttributes> entry : this.actions.entrySet()) {
099                if (entry.getValue().name.equals(name)) {
100                    return entry.getKey().getName();
101                }
102            }
103        }
104        return null;
105    }
106
107    @CheckForNull
108    public String[] getNames() {
109        this.prepareActionsHashMap();
110        if (this.actionNames == null) {
111            this.actionNames = new ArrayList<>();
112            this.actions.values().stream().forEach((attrs) -> {
113                this.actionNames.add(attrs.name);
114            });
115            this.actionNames.sort(null);
116        }
117        return this.actionNames.toArray(new String[this.actionNames.size()]);
118    }
119
120    @Nonnull
121    public Class<?>[] getClasses() {
122        this.prepareActionsHashMap();
123        return actions.keySet().toArray(new Class<?>[actions.size()]);
124    }
125
126    private void prepareActionsHashMap() {
127        if (this.actions == null) {
128            this.actions = new HashMap<>();
129            this.overrides = new HashMap<>();
130
131            ServiceLoader<StartupActionFactory> jusLoader = ServiceLoader.load(StartupActionFactory.class);
132            this.serviceClasses = new HashSet<>();
133            jusLoader.forEach(factory -> {
134                addActions(factory);
135                serviceClasses.addAll(Arrays.asList(factory.getActionClasses()));
136            });
137            jusLoader.reload(); // allow factories to be garbage collected
138
139            InstanceManager.getList(SystemConnectionMemo.class).forEach(memo -> addActions(memo.getActionFactory()));
140            InstanceManager.getList(SystemConnectionMemo.class).forEach(memo -> listenTo(memo));
141
142            firePropertyChange("length", 0, actions.size());
143        }
144    }
145
146    // add the listener once only, even if both prepareActionsHashMap() and
147    // memoChanged() see the same new memo
148    private void listenTo(SystemConnectionMemo memo) {
149        memo.removePropertyChangeListener("actionFactory", actionFactoryListener);
150        memo.addPropertyChangeListener("actionFactory", actionFactoryListener);
151    }
152
153    private void addActions(StartupActionFactory factory) {
154        Arrays.stream(factory.getActionClasses()).forEach(clazz -> {
155            actions.put(clazz, new ActionAttributes(factory.getTitle(clazz), clazz));
156            Arrays.stream(factory.getOverriddenClasses(clazz))
157                    .forEach(overridden -> overrides.put(overridden, clazz));
158        });
159    }
160
161    /**
162     * Remove the actions of a factory that is going away.
163     * <p>
164     * Several connections of the same type (for example two CBUS connections)
165     * offer the same action classes. A class is only removed if no remaining
166     * connection, and no general factory, still offers it. Otherwise removing
167     * one connection would make the startup actions of the others unknown.
168     *
169     * @param factory the factory being removed
170     * @param leaving the connection the factory belongs to, which is not
171     *                counted as still offering the actions; may be null
172     */
173    private void removeActions(StartupActionFactory factory, @CheckForNull SystemConnectionMemo leaving) {
174        Arrays.stream(factory.getActionClasses())
175                .filter(clazz -> !isOfferedElsewhere(clazz, leaving))
176                .forEach(actions::remove);
177    }
178
179    private boolean isOfferedElsewhere(Class<?> clazz, @CheckForNull SystemConnectionMemo leaving) {
180        if (serviceClasses != null && serviceClasses.contains(clazz)) {
181            return true;
182        }
183        for (SystemConnectionMemo memo : InstanceManager.getList(SystemConnectionMemo.class)) {
184            if (memo != leaving && Arrays.asList(memo.getActionFactory().getActionClasses()).contains(clazz)) {
185                return true;
186            }
187        }
188        return false;
189    }
190
191    @CheckForNull
192    public String getOverride(@CheckForNull String name) {
193        this.prepareActionsHashMap();
194        if (name != null && this.overrides.containsKey(name)) {
195            return this.overrides.get(name).getName();
196        }
197        return null;
198    }
199
200    @Override
201    public void dispose() {
202        InstanceManager.removePropertyChangeListener(InstanceManager.getListPropertyName(SystemConnectionMemo.class), memosListener);
203    }
204
205    private void memoChanged(PropertyChangeEvent evt) {
206        prepareActionsHashMap();
207        actionNames = null;
208        int size = actions.size();
209        Object src = evt.getNewValue();
210        if (src instanceof SystemConnectionMemo) {
211            SystemConnectionMemo memo = (SystemConnectionMemo) src;
212            addActions(memo.getActionFactory());
213            listenTo(memo);
214        } else {
215            src = evt.getOldValue();
216            if (src instanceof SystemConnectionMemo) {
217                SystemConnectionMemo memo = (SystemConnectionMemo) src;
218                removeActions(memo.getActionFactory(), memo);
219                memo.removePropertyChangeListener("actionFactory", actionFactoryListener);
220            }
221        }
222        firePropertyChange("length", size, actions.size());
223    }
224
225    private void actionFactoryChanged(PropertyChangeEvent evt) {
226        prepareActionsHashMap();
227        actionNames = null;
228        int size = actions.size();
229        Object value = evt.getOldValue();
230        if (value instanceof StartupActionFactory) {
231            Object source = evt.getSource();
232            removeActions((StartupActionFactory) value,
233                    source instanceof SystemConnectionMemo ? (SystemConnectionMemo) source : null);
234        }
235        value = evt.getNewValue();
236        if (value instanceof StartupActionFactory) {
237            addActions((StartupActionFactory) value);
238        }
239        firePropertyChange("length", size, actions.size());
240    }
241
242    private static class ActionAttributes {
243
244        final String name;
245        final boolean isSystemConnectionAction;
246
247        ActionAttributes(String name, Class<?> clazz) {
248            this.name = name;
249            this.isSystemConnectionAction = SystemConnectionAction.class.isAssignableFrom(clazz);
250        }
251    }
252}