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.Map.Entry;
009import java.util.ServiceLoader;
010import javax.annotation.Nonnull;
011import javax.annotation.CheckForNull;
012import jmri.Disposable;
013import jmri.InstanceManager;
014import jmri.beans.Bean;
015import jmri.SystemConnectionMemo;
016import jmri.jmrix.swing.SystemConnectionAction;
017import org.slf4j.Logger;
018import org.slf4j.LoggerFactory;
019
020/**
021 * Maintain a list of actions that can be used by
022 * {@link jmri.util.startup.AbstractActionModel} and its descendants. This list is
023 * populated by {@link StartupActionFactory} instances
024 * registered with a {@link java.util.ServiceLoader}.
025 *
026 * @author Randall Wood Copyright 2016, 2020
027 */
028public class StartupActionModelUtil extends Bean implements Disposable {
029
030    private HashMap<Class<?>, ActionAttributes> actions = null;
031    private HashMap<String, Class<?>> overrides = null;
032    private ArrayList<String> actionNames = null; // built on demand, invalidated in changes to actions
033    private final PropertyChangeListener memosListener = this::memoChanged;
034    private final PropertyChangeListener actionFactoryListener = this::actionFactoryChanged;
035    private final static Logger log = LoggerFactory.getLogger(StartupActionModelUtil.class);
036
037    /**
038     * Get the default StartupActionModelUtil instance, creating it if
039     * necessary.
040     *
041     * @return the default instance
042     */
043    @Nonnull
044    static public StartupActionModelUtil getDefault() {
045        return InstanceManager.getOptionalDefault(StartupActionModelUtil.class).orElseGet(() -> {
046            return InstanceManager.setDefault(StartupActionModelUtil.class, new StartupActionModelUtil());
047        });
048    }
049
050    public StartupActionModelUtil() {
051        InstanceManager.addPropertyChangeListener(InstanceManager.getListPropertyName(SystemConnectionMemo.class), memosListener);
052    }
053
054    @CheckForNull
055    public String getActionName(@Nonnull Class<?> clazz) {
056        this.prepareActionsHashMap();
057        ActionAttributes attrs = this.actions.get(clazz);
058        return attrs != null ? attrs.name : null;
059    }
060
061    @CheckForNull
062    public String getActionName(@Nonnull String className) {
063        if (!className.isEmpty()) {
064            try {
065                return this.getActionName(Class.forName(className));
066            } catch (ClassNotFoundException ex) {
067                log.error("Did not find class \"{}\"", className);
068            }
069        }
070        return null;
071    }
072
073    public boolean isSystemConnectionAction(@Nonnull Class<?> clazz) {
074        this.prepareActionsHashMap();
075        if (this.actions.containsKey(clazz)) {
076            return this.actions.get(clazz).isSystemConnectionAction;
077        }
078        return false;
079    }
080
081    public boolean isSystemConnectionAction(@Nonnull String className) {
082        if (!className.isEmpty()) {
083            try {
084                return this.isSystemConnectionAction(Class.forName(className));
085            } catch (ClassNotFoundException ex) {
086                log.error("Did not find class \"{}\"", className);
087            }
088        }
089        return false;
090    }
091
092    @CheckForNull
093    public String getClassName(@CheckForNull String name) {
094        if (name != null && !name.isEmpty()) {
095            this.prepareActionsHashMap();
096            for (Entry<Class<?>, ActionAttributes> entry : this.actions.entrySet()) {
097                if (entry.getValue().name.equals(name)) {
098                    return entry.getKey().getName();
099                }
100            }
101        }
102        return null;
103    }
104
105    @CheckForNull
106    public String[] getNames() {
107        this.prepareActionsHashMap();
108        if (this.actionNames == null) {
109            this.actionNames = new ArrayList<>();
110            this.actions.values().stream().forEach((attrs) -> {
111                this.actionNames.add(attrs.name);
112            });
113            this.actionNames.sort(null);
114        }
115        return this.actionNames.toArray(new String[this.actionNames.size()]);
116    }
117
118    @Nonnull
119    public Class<?>[] getClasses() {
120        this.prepareActionsHashMap();
121        return actions.keySet().toArray(new Class<?>[actions.size()]);
122    }
123
124    private void prepareActionsHashMap() {
125        if (this.actions == null) {
126            this.actions = new HashMap<>();
127            this.overrides = new HashMap<>();
128
129            ServiceLoader<StartupActionFactory> jusLoader = ServiceLoader.load(StartupActionFactory.class);
130            jusLoader.forEach(factory -> addActions(factory));
131            jusLoader.reload(); // allow factories to be garbage collected
132
133            InstanceManager.getList(SystemConnectionMemo.class).forEach(memo -> addActions(memo.getActionFactory()));
134            InstanceManager.getList(SystemConnectionMemo.class).forEach(memo -> memo.addPropertyChangeListener("actionFactory", actionFactoryListener));
135
136            firePropertyChange("length", 0, actions.size());
137        }
138    }
139
140    private void addActions(StartupActionFactory factory) {
141        Arrays.stream(factory.getActionClasses()).forEach(clazz -> {
142            actions.put(clazz, new ActionAttributes(factory.getTitle(clazz), clazz));
143            Arrays.stream(factory.getOverriddenClasses(clazz))
144                    .forEach(overridden -> overrides.put(overridden, clazz));
145        });
146    }
147
148    private void removeActions(StartupActionFactory factory) {
149        Arrays.stream(factory.getActionClasses()).forEach(actions::remove);
150    }
151
152    @CheckForNull
153    public String getOverride(@CheckForNull String name) {
154        this.prepareActionsHashMap();
155        if (name != null && this.overrides.containsKey(name)) {
156            return this.overrides.get(name).getName();
157        }
158        return null;
159    }
160
161    @Override
162    public void dispose() {
163        InstanceManager.removePropertyChangeListener(InstanceManager.getListPropertyName(SystemConnectionMemo.class), memosListener);
164    }
165
166    private void memoChanged(PropertyChangeEvent evt) {
167        prepareActionsHashMap();
168        actionNames = null;
169        int size = actions.size();
170        Object src = evt.getNewValue();
171        if (src instanceof SystemConnectionMemo) {
172            SystemConnectionMemo memo = (SystemConnectionMemo) src;
173            addActions(memo.getActionFactory());
174            memo.addPropertyChangeListener("actionFactory", actionFactoryListener);
175        } else {
176            src = evt.getOldValue();
177            if (src instanceof SystemConnectionMemo) {
178                SystemConnectionMemo memo = (SystemConnectionMemo) src;
179                removeActions(memo.getActionFactory());
180                memo.removePropertyChangeListener("actionFactory", actionFactoryListener);
181            }
182        }
183        firePropertyChange("length", size, actions.size());
184    }
185
186    private void actionFactoryChanged(PropertyChangeEvent evt) {
187        prepareActionsHashMap();
188        actionNames = null;
189        int size = actions.size();
190        Object value = evt.getOldValue();
191        if (value instanceof StartupActionFactory) {
192            removeActions((StartupActionFactory) value);
193        }
194        value = evt.getNewValue();
195        if (value instanceof StartupActionFactory) {
196            addActions((StartupActionFactory) value);
197        }
198        firePropertyChange("length", size, actions.size());
199    }
200
201    private static class ActionAttributes {
202
203        final String name;
204        final boolean isSystemConnectionAction;
205
206        ActionAttributes(String name, Class<?> clazz) {
207            this.name = name;
208            this.isSystemConnectionAction = SystemConnectionAction.class.isAssignableFrom(clazz);
209        }
210    }
211}