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}