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}