001package jmri.util.startup; 002 003import java.util.ArrayList; 004import java.util.List; 005import java.util.Objects; 006 007import javax.annotation.Nonnull; 008import javax.annotation.CheckForNull; 009import javax.swing.Action; 010 011import jmri.JmriException; 012import jmri.SystemConnectionMemo; 013import jmri.jmrix.SystemConnectionMemoManager; 014import jmri.jmrix.swing.SystemConnectionAction; 015import jmri.util.ConnectionNameFromSystemName; 016 017import org.slf4j.Logger; 018import org.slf4j.LoggerFactory; 019 020/** 021 * Provide services for invoking actions during configuration and startup. 022 * <p> 023 * The action classes and corresponding human-readable names are provided by 024 * {@link jmri.util.startup.StartupActionFactory} instances. 025 * 026 * @author Bob Jacobsen Copyright 2003, 2007, 2014 027 * @see AbstractActionModelFactory 028 */ 029public abstract class AbstractActionModel implements StartupModel { 030 031 private String systemPrefix = ""; // NOI18N 032 private String className = ""; // NOI18N 033 private boolean enabled = true; 034 private final List<Exception> exceptions = new ArrayList<>(); 035 private static final Logger log = LoggerFactory.getLogger(AbstractActionModel.class); 036 037 public String getClassName() { 038 return this.className; 039 } 040 041 @Override 042 public String getName() { 043 if (className != null) { 044 return StartupActionModelUtil.getDefault().getActionName(className); 045 } 046 return null; 047 } 048 049 @Override 050 public void setName(String n) { 051 log.debug("setName(\"{}\")", n); 052 // can set className to null if no class found for n 053 this.className = StartupActionModelUtil.getDefault().getClassName(n); 054 } 055 056 public void setClassName(@Nonnull String n) { 057 log.debug("setClassName(\"{}\")", n); 058 Objects.requireNonNull(n, "Class name cannot be null"); 059 this.className = n; 060 } 061 062 @Nonnull 063 public String getSystemPrefix() { 064 return this.systemPrefix; 065 } 066 067 public void setSystemPrefix(@CheckForNull String prefix) { 068 if (prefix == null) { 069 this.systemPrefix = ""; // NOI18N 070 } else { 071 this.systemPrefix = prefix; 072 } 073 } 074 075 /** 076 * Check if the action needs a system connection. 077 * <p> 078 * This is decided from the action class itself, not from the list of 079 * actions offered by the running connections, so an action belonging to a 080 * connection that is disabled, failed to start or has been removed is 081 * still recognised as a connection action. 082 * 083 * @return true if the action class implements {@link SystemConnectionAction} 084 */ 085 public boolean isSystemConnectionAction() { 086 String name = this.getClassName(); 087 if (name != null && !name.isEmpty()) { 088 try { 089 return SystemConnectionAction.class.isAssignableFrom(Class.forName(name)); 090 } catch (ClassNotFoundException | LinkageError ex) { 091 return false; 092 } 093 } 094 return false; 095 } 096 097 /** 098 * Check if the connection this action needs is available. 099 * 100 * @return true if the action does not need a connection, or its 101 * connection is currently registered 102 */ 103 public boolean isConnectionAvailable() { 104 return !isSystemConnectionAction() 105 || SystemConnectionMemoManager.getDefault().getSystemConnectionMemoForSystemPrefix(systemPrefix) != null; 106 } 107 108 @Override 109 public boolean isValid() { 110 if (this.className != null && !this.className.isEmpty()) { 111 try { 112 // don't need return value, just want to know if exception is triggered 113 Class.forName(className); 114 return isConnectionAvailable(); 115 } catch (ClassNotFoundException | LinkageError ex) { 116 return false; 117 } 118 } 119 return false; 120 } 121 122 /** {@inheritDoc} */ 123 @Override 124 public void setEnabled(boolean value) { 125 enabled = value; 126 } 127 128 /** {@inheritDoc} */ 129 @Override 130 public boolean isEnabled() { 131 return enabled; 132 } 133 134 @Override 135 public String toString() { 136 String name = this.getName(); 137 if (this.className != null && !this.className.isEmpty() && !this.isConnectionAvailable()) { 138 // action kept for a connection that is not running (disabled, removed, or failed to start). 139 // The name is known if another connection of the same type offers the action. 140 return Bundle.getMessage("AbstractActionModel.ConnectionNotAvailable", 141 name != null ? name : this.className, this.systemPrefix); // NOI18N 142 } 143 if (name != null) { 144 if (!this.systemPrefix.isEmpty()) { 145 return Bundle.getMessage("AbstractActionModel.ToolTip", name, ConnectionNameFromSystemName.getConnectionName(this.systemPrefix)); // NOI18N 146 } 147 return name; 148 } 149 if (this.className != null && this.isValid()) { 150 return Bundle.getMessage("AbstractActionModel.UnknownClass", this.className); 151 } else if (this.className != null && !this.className.isEmpty()) { 152 return Bundle.getMessage("AbstractActionModel.InvalidClass", this.className); 153 } 154 return Bundle.getMessage("AbstractActionModel.InvalidAction", super.toString()); 155 } 156 157 @SuppressWarnings("unchecked") // parametized cast of action cannot be checked 158 @Override 159 public void performAction() throws JmriException { 160 log.debug("Invoke Action from {}", className); 161 if (!isConnectionAvailable()) { 162 // Do not create the action: connection actions often look up a default 163 // connection in their constructor, which would either fail or pick 164 // an unrelated connection. 165 log.warn("Connection \"{}\" does not exist and cannot be assigned to action {}\nThis warning can be silenced by configuring the connection associated with the startup action.", this.getSystemPrefix(), className); 166 return; 167 } 168 try { 169 Action action = (Action) Class.forName(className).getDeclaredConstructor().newInstance(); 170 if (SystemConnectionAction.class.isAssignableFrom(action.getClass())) { 171 SystemConnectionMemo memo = ConnectionNameFromSystemName.getSystemConnectionMemoFromSystemPrefix(this.getSystemPrefix()); 172 if (memo != null) { 173 ((SystemConnectionAction<SystemConnectionMemo>) action).setSystemConnectionMemo(memo); 174 } else { 175 log.warn("Connection \"{}\" does not exist and cannot be assigned to action {}\nThis warning can be silenced by configuring the connection associated with the startup action.", this.getSystemPrefix(), className); 176 } 177 } 178 jmri.util.ThreadingUtil.runOnLayout(() -> { 179 try { 180 this.performAction(action); 181 } catch (JmriException ex) { 182 log.error("Error while performing startup action for class: {}", className, ex); 183 } 184 }); 185 } catch (ClassNotFoundException ex) { 186 log.error("Could not find specified class: {}", className); 187 } catch (IllegalAccessException ex) { 188 log.error("Unexpected access exception for class: {}", className, ex); 189 throw new JmriException(ex); 190 } catch (InstantiationException ex) { 191 log.error("Could not instantiate specified class: {}", className, ex); 192 throw new JmriException(ex); 193 } catch (java.lang.reflect.InvocationTargetException ex) { 194 log.error("Error while invoking startup action for class: {}", className, ex); 195 throw new JmriException(ex); 196 } catch (NoSuchMethodException ex) { 197 log.error("Could not locate specified method: {}", className, ex); 198 throw new JmriException(ex); 199 } catch (Exception ex) { 200 log.error("Error while performing startup action for class: {}", className, ex); 201 throw new JmriException(ex); 202 } 203 } 204 205 @Override 206 public List<Exception> getExceptions() { 207 return new ArrayList<>(this.exceptions); 208 } 209 210 @Override 211 public void addException(Exception exception) { 212 this.exceptions.add(exception); 213 } 214 215 protected abstract void performAction(Action action) throws JmriException; 216}