001package jmri.util.startup; 002 003import java.lang.reflect.InvocationTargetException; 004import java.util.ArrayList; 005import java.util.HashMap; 006import java.util.List; 007import java.util.Locale; 008import java.util.ServiceLoader; 009import java.util.Set; 010 011import javax.annotation.Nonnull; 012 013import jmri.JmriException; 014import jmri.configurexml.ConfigXmlManager; 015import jmri.configurexml.JmriConfigureXmlException; 016import jmri.configurexml.XmlAdapter; 017import jmri.profile.Profile; 018import jmri.profile.ProfileUtils; 019import jmri.spi.PreferencesManager; 020import jmri.util.jdom.JDOMUtil; 021import jmri.util.prefs.AbstractPreferencesManager; 022import jmri.util.prefs.InitializationException; 023 024import org.jdom2.Element; 025import org.jdom2.JDOMException; 026import org.openide.util.lookup.ServiceProvider; 027import org.slf4j.Logger; 028import org.slf4j.LoggerFactory; 029 030/** 031 * Manager for Startup Actions. Reads preferences at startup and triggers 032 * actions, and is responsible for saving the preferences later. 033 * 034 * @author Randall Wood (C) 2015, 2016, 2020 035 */ 036@ServiceProvider(service = PreferencesManager.class) 037public class StartupActionsManager extends AbstractPreferencesManager { 038 039 private final List<StartupModel> actions = new ArrayList<>(); 040 private final HashMap<Class<? extends StartupModel>, StartupModelFactory> factories = new HashMap<>(); 041 private boolean isDirty = false; 042 private boolean restartRequired = false; 043 public final static String STARTUP = "startup"; // NOI18N 044 public final static String NAMESPACE = "http://jmri.org/xml/schema/auxiliary-configuration/startup-4-3-5.xsd"; // NOI18N 045 public final static String NAMESPACE_OLD = "http://jmri.org/xml/schema/auxiliary-configuration/startup-2-9-6.xsd"; // NOI18N 046 private final static Logger log = LoggerFactory.getLogger(StartupActionsManager.class); 047 048 public StartupActionsManager() { 049 super(); 050 for (StartupModelFactory factory : ServiceLoader.load(StartupModelFactory.class)) { 051 factory.initialize(); 052 this.factories.put(factory.getModelClass(), factory); 053 } 054 } 055 056 /** 057 * {@inheritDoc} 058 * <p> 059 * Loads the startup action preferences and, if all required managers have 060 * initialized without exceptions, performs those actions. Startup actions 061 * are only performed if {@link StartupModel#isValid()} is true 062 * for the action. It is assumed that the action has retained an Exception 063 * that can be used to explain why isValid() is false. 064 */ 065 @Override 066 public void initialize(Profile profile) throws InitializationException { 067 if (!this.isInitialized(profile)) { 068 boolean perform = true; 069 try { 070 this.requiresNoInitializedWithExceptions(profile, Bundle.getMessage("StartupActionsManager.RefusalToInitialize")); 071 } catch (InitializationException ex) { 072 perform = false; 073 } 074 try { 075 Element startup; 076 try { 077 startup = JDOMUtil.toJDOMElement(ProfileUtils.getAuxiliaryConfiguration(profile).getConfigurationFragment(STARTUP, NAMESPACE, true)); 078 } catch (NullPointerException ex) { 079 log.debug("Reading element from version 2.9.6 namespace..."); 080 startup = JDOMUtil.toJDOMElement(ProfileUtils.getAuxiliaryConfiguration(profile).getConfigurationFragment(STARTUP, NAMESPACE_OLD, true)); 081 } 082 for (Element action : startup.getChildren()) { 083 String adapter = action.getAttributeValue("class"); // NOI18N 084 String name = action.getAttributeValue("name"); // NOI18N 085 String override = StartupActionModelUtil.getDefault().getOverride(name); 086 if (override != null) { 087 action.setAttribute("name", override); 088 log.info("Overriding startup action class {} with {}", name, override); 089 this.addInitializationException(profile, new InitializationException(Bundle.getMessage(Locale.ENGLISH, "StartupActionsOverriddenClasses", name, override), 090 Bundle.getMessage("StartupActionsOverriddenClasses", name, override))); 091 name = override; // after logging difference and creating error message 092 } 093 String type = action.getAttributeValue("type"); // NOI18N 094 log.debug("Read {} {} adapter {}", type, name, adapter); 095 try { 096 log.debug("Creating {} {} adapter {}...", type, name, adapter); 097 ((XmlAdapter) Class.forName(adapter).getDeclaredConstructor().newInstance()).load(action, null); // no perNode preferences 098 } catch (ClassNotFoundException | InstantiationException | IllegalAccessException ex) { 099 log.error("Unable to create {} for {}", adapter, action, ex); 100 this.addInitializationException(profile, new InitializationException(Bundle.getMessage(Locale.ENGLISH, "StartupActionsCreationError", adapter, name), 101 Bundle.getMessage("StartupActionsCreationError", adapter, name))); // NOI18N 102 } catch (IllegalArgumentException | NoSuchMethodException | SecurityException | InvocationTargetException | JmriConfigureXmlException ex) { 103 log.error("Unable to load {} into {}", action, adapter, ex); 104 this.addInitializationException(profile, new InitializationException(Bundle.getMessage(Locale.ENGLISH, "StartupActionsLoadError", adapter, name), 105 Bundle.getMessage("StartupActionsLoadError", adapter, name))); // NOI18N 106 } 107 } 108 } catch (NullPointerException ex) { 109 // ignore - this indicates migration has not occurred 110 log.debug("No element to read"); 111 } 112 if (perform) { 113 this.actions.stream().filter(action -> action.isValid()).forEachOrdered(action -> { 114 try { 115 if (action.isEnabled()) { 116 action.performAction(); 117 } 118 } catch (JmriException ex) { 119 this.addInitializationException(profile, ex); 120 } 121 }); 122 ServiceLoader.load(StartupRunnable.class).forEach(Runnable::run); 123 } 124 this.isDirty = false; 125 this.restartRequired = false; 126 this.setInitialized(profile, true); 127 List<Exception> exceptions = this.getInitializationExceptions(profile); 128 if (exceptions.size() == 1) { 129 throw new InitializationException(exceptions.get(0)); 130 } else if (exceptions.size() > 1) { 131 throw new InitializationException(Bundle.getMessage(Locale.ENGLISH, "StartupActionsMultipleErrors"), 132 Bundle.getMessage("StartupActionsMultipleErrors")); // NOI18N 133 } 134 } 135 } 136 137 @Override 138 @Nonnull 139 public Set<Class<? extends PreferencesManager>> getRequires() { 140 return requireAllOther(); 141 } 142 143 @Override 144 public synchronized void savePreferences(Profile profile) { 145 Element element = new Element(STARTUP, NAMESPACE); 146 actions.stream().forEach((action) -> { 147 log.debug("model is {} ({})", action.getName(), action); 148 if (action.getName() != null) { 149 Element e = ConfigXmlManager.elementFromObject(action, true); 150 if (e != null) { 151 element.addContent(e); 152 } 153 } else { 154 // get an error with a stack trace if this occurs 155 log.error("model \"{}\" does not have a name.", action, new Exception()); 156 } 157 }); 158 try { 159 ProfileUtils.getAuxiliaryConfiguration(profile).putConfigurationFragment(JDOMUtil.toW3CElement(element), true); 160 this.isDirty = false; 161 } catch (JDOMException ex) { 162 log.error("Unable to create create XML", ex); 163 } 164 } 165 166 public StartupModel[] getActions() { 167 return this.actions.toArray(StartupModel[]::new); 168 } 169 170 @SuppressWarnings("unchecked") 171 public <T extends StartupModel> List<T> getActions(Class<T> type) { 172 ArrayList<T> result = new ArrayList<>(); 173 this.actions.stream().filter((action) -> (type.isInstance(action))).forEach((action) -> { 174 result.add((T) action); 175 }); 176 return result; 177 } 178 179 public StartupModel getActions(int index) { 180 return this.actions.get(index); 181 } 182 183 /** 184 * Insert a {@link jmri.util.startup.StartupModel} at the given position. 185 * Triggers an {@link java.beans.IndexedPropertyChangeEvent} where the old 186 * value is null and the new value is the inserted model. 187 * 188 * @param index The position where the model will be inserted 189 * @param model The model to be inserted 190 */ 191 public void setActions(int index, StartupModel model) { 192 this.setActions(index, model, true); 193 } 194 195 private void setActions(int index, StartupModel model, boolean fireChange) { 196 if (!this.actions.contains(model)) { 197 this.actions.add(index, model); 198 this.setRestartRequired(); 199 if (fireChange) { 200 this.fireIndexedPropertyChange(STARTUP, index, null, model); 201 } 202 } 203 } 204 205 /** 206 * Move a {@link jmri.util.startup.StartupModel} from position start to position 207 * end. Triggers an {@link java.beans.IndexedPropertyChangeEvent} where the 208 * index is end, the old value is start and the new value is the moved 209 * model. 210 * 211 * @param start the original position 212 * @param end the new position 213 */ 214 public void moveAction(int start, int end) { 215 StartupModel model = this.getActions(start); 216 this.removeAction(model, false); 217 this.setActions(end, model, false); 218 this.fireIndexedPropertyChange(STARTUP, end, start, model); 219 } 220 221 public void addAction(StartupModel model) { 222 this.setActions(this.actions.size(), model); 223 } 224 225 /** 226 * Remove a {@link jmri.util.startup.StartupModel}. Triggers an 227 * {@link java.beans.IndexedPropertyChangeEvent} where the index is the 228 * position of the removed model, the old value is the model, and the new 229 * value is null. 230 * 231 * @param model The startup action to remove 232 */ 233 public void removeAction(StartupModel model) { 234 this.removeAction(model, true); 235 } 236 237 private void removeAction(StartupModel model, boolean fireChange) { 238 int index = this.actions.indexOf(model); 239 this.actions.remove(model); 240 this.setRestartRequired(); 241 if (fireChange) { 242 this.fireIndexedPropertyChange(STARTUP, index, model, null); 243 } 244 } 245 246 public HashMap<Class<? extends StartupModel>, StartupModelFactory> getFactories() { 247 return new HashMap<>(this.factories); 248 } 249 250 public StartupModelFactory getFactories(Class<? extends StartupModel> model) { 251 return this.factories.get(model); 252 } 253 254 public boolean isDirty() { 255 return this.isDirty; 256 } 257 258 /** 259 * Mark that a change requires a restart. As a side effect, marks this 260 * manager dirty. 261 */ 262 public void setRestartRequired() { 263 this.restartRequired = true; 264 this.isDirty = true; 265 } 266 267 /** 268 * Indicate if a restart is required for preferences to be applied. 269 * 270 * @return true if a restart is required, false otherwise 271 */ 272 public boolean isRestartRequired() { 273 return this.isDirty || this.restartRequired; 274 } 275}