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}