001package jmri.managers;
002
003import edu.umd.cs.findbugs.annotations.SuppressFBWarnings;
004
005import java.io.File;
006import java.io.IOException;
007import java.lang.reflect.Constructor;
008import java.net.URISyntaxException;
009import java.net.URL;
010import java.util.ArrayList;
011import java.util.List;
012
013import javax.annotation.CheckForNull;
014import javax.annotation.Nonnull;
015
016import jmri.NamedBean;
017import jmri.SignalSystem;
018import jmri.SignalSystemManager;
019import jmri.implementation.DefaultSignalSystem;
020import jmri.jmrit.XmlFile;
021import jmri.jmrix.internal.InternalSystemConnectionMemo;
022import jmri.util.FileUtil;
023
024import org.jdom2.Element;
025import org.jdom2.JDOMException;
026import org.slf4j.Logger;
027import org.slf4j.LoggerFactory;
028
029/**
030 * Default implementation of a SignalSystemManager.
031 * <p>
032 * This loads automatically the first time used.
033 *
034 * @author Bob Jacobsen Copyright (C) 2009
035 */
036public class DefaultSignalSystemManager extends AbstractManager<SignalSystem>
037        implements SignalSystemManager {
038
039    public DefaultSignalSystemManager(InternalSystemConnectionMemo memo) {
040        super(memo);
041
042        // load when created, which will generally
043        // be the first time referenced
044        load();
045    }
046
047    @Override
048    public int getXMLOrder() {
049        return 65400;
050    }
051
052    /**
053     * Don't want to store this information
054     */
055    @Override
056    @SuppressFBWarnings(value = "OVERRIDING_METHODS_MUST_INVOKE_SUPER",
057            justification = "This method intentionally doesn't do anything")
058    protected void registerSelf() {
059    }
060
061    @Override
062    public char typeLetter() {
063        return 'F';
064    }
065
066    /**
067     * {@inheritDoc}
068     * @param name to search, by UserName then SystemName.
069     */
070    @CheckForNull
071    @Override
072    public SignalSystem getSystem(String name) {
073        SignalSystem t = getByUserName(name);
074        return ( t!=null ? t : getBySystemName(name));
075    }
076
077    final void load() {
078        List<String> list = getListOfNames();
079        for (int i = 0; i < list.size(); i++) {
080            try {
081                SignalSystem s = makeBean(list.get(i));
082                register(s);
083            }
084            catch (IllegalArgumentException ex){} // error already logged
085        }
086    }
087
088    @Nonnull
089    protected List<String> getListOfNames() {
090        List<String> retval = new ArrayList<>();
091        // first locate the signal system directory
092        // and get names of systems
093        File signalDir = null;
094        // First get the default pre-configured signalling systems
095        try {
096            signalDir = new File(FileUtil.findURL("xml/signals", FileUtil.Location.INSTALLED).toURI());
097        } catch (URISyntaxException | NullPointerException ex) {
098            log.error("Unable to get installed signals.", ex);
099        }
100        if (signalDir != null) {
101            File[] files = signalDir.listFiles();
102            if (files != null) { // null if not a directory
103                for (File file : files) {
104                    if (file.isDirectory()) {
105                        // check that there's an aspects.xml file
106                        File aspects = new File(file.getPath() + File.separator + "aspects.xml");
107                        if (aspects.exists()) {
108                            log.debug("found system: {}", file.getName());
109                            retval.add(file.getName());
110                        }
111                    }
112                }
113            }
114        }
115        // Now get the user defined systems.
116        try {
117            URL dir = FileUtil.findURL("signals", FileUtil.Location.USER, "resources", "xml");
118            if (dir == null) {
119                try {
120                    if (!(new File(FileUtil.getUserFilesPath(), "xml/signals")).mkdirs()) {
121                        log.error("Error while creating xml/signals directory");
122                    }
123                } catch (Exception ex) {
124                    log.error("Unable to create user's signals directory.", ex);
125                }
126                dir = FileUtil.findURL("xml/signals", FileUtil.Location.USER);
127            }
128            signalDir = new File(dir.toURI());
129        } catch (URISyntaxException ex) {
130            log.error("Unable to get installed signals.", ex);
131        }
132        if (signalDir != null) {
133            File[] files = signalDir.listFiles();
134            if (files != null) { // null if not a directory
135                for (File file : files) {
136                    if (file.isDirectory()) {
137                        // check that there's an aspects.xml file
138                        File aspects = new File(file.getPath() + File.separator + "aspects.xml");
139                        log.trace("checking for {}", aspects);
140                        if ((aspects.exists()) && (!retval.contains(file.getName()))) {
141                            log.debug("found user system: {}", file.getName());
142                            retval.add(file.getName());
143                        }
144                    }
145                }
146            }
147        }
148        return retval;
149    }
150
151    @Nonnull
152    protected SignalSystem makeBean(String name) throws IllegalArgumentException {
153
154        URL path;
155        XmlFile xf;
156
157        // First check to see if the bean is in the user directory resources/signals/, then xml/signals
158        path = FileUtil.findURL("signals/" + name + "/aspects.xml", FileUtil.Location.USER, "resources", "xml");
159        log.debug("load from {}", path);
160        if (path != null) {
161            xf = new AspectFile();
162            try {
163                log.debug(" successful");
164                Element root = xf.rootFromURL(path);
165                DefaultSignalSystem s = new DefaultSignalSystem(name);
166                loadBean(s, root);
167                return s;
168            } catch (IOException | JDOMException e) {
169                log.error("Could not parse aspect file \"{}\" due to", path, e);
170            }
171        }
172
173        throw new IllegalArgumentException("Unable to parse aspect file "+path);
174    }
175
176    void loadBean(DefaultSignalSystem s, Element root) {
177        List<Element> l = root.getChild("aspects").getChildren("aspect");
178
179        // set user name from system name element
180        s.setUserName(root.getChild("name").getText());
181
182        // find all aspects, include them by name,
183        // add all other sub-elements as key/value pairs
184        for (int i = 0; i < l.size(); i++) {
185            String name = l.get(i).getChild("name").getText();
186            log.debug("aspect name {}", name);
187
188            List<Element> c = l.get(i).getChildren();
189
190            for (int j = 0; j < c.size(); j++) {
191                // note: includes setting name; redundant, but needed
192                s.setProperty(name, c.get(j).getName(), c.get(j).getText());
193            }
194        }
195
196        if (root.getChild("imagetypes") != null) {
197            List<Element> t = root.getChild("imagetypes").getChildren("imagetype");
198            for (int i = 0; i < t.size(); i++) {
199                String type = t.get(i).getAttribute("type").getValue();
200                s.setImageType(type);
201            }
202        }
203        //loadProperties(s, root);
204        if (root.getChild("properties") != null) {
205            for (Object next : root.getChild("properties").getChildren("property")) {
206                Element e = (Element) next;
207
208                try {
209                    Class<?> cl;
210                    Constructor<?> ctor;
211
212                    // create key string
213                    String key = e.getChild("key").getText();
214
215                    // check for non-String key.  Warn&proceed if found.
216                    // Pre-JMRI 4.3, keys in NamedBean parameters could be Objects
217                    // constructed from Strings, similar to the value code below.
218                    if (! (
219                        e.getChild("key").getAttributeValue("class") == null
220                        || e.getChild("key").getAttributeValue("class").isEmpty()
221                        || e.getChild("key").getAttributeValue("class").equals("java.lang.String")
222                        )) {
223
224                        log.warn("SignalSystem {} property key of invalid non-String type {} not supported",
225                            s.getSystemName(), e.getChild("key").getAttributeValue("class"));
226                    }
227
228                    // create value object
229                    Object value = null;
230                    if (e.getChild("value") != null) {
231                        cl = Class.forName(e.getChild("value").getAttributeValue("class"));
232                        ctor = cl.getConstructor(new Class<?>[]{String.class});
233                        value = ctor.newInstance(new Object[]{e.getChild("value").getText()});
234                    }
235
236                    // store
237                    s.setProperty(key, value);
238                } catch (ClassNotFoundException
239                            | NoSuchMethodException | InstantiationException
240                            | IllegalAccessException | java.lang.reflect.InvocationTargetException ex) {
241                    log.error("Error loading properties", ex);
242                }
243            }
244        }
245    }
246
247    void loadProperties(NamedBean t, Element elem) {
248        // do nothing
249    }
250
251    /**
252     * XmlFile is abstract, so this extends for local use
253     */
254    static class AspectFile extends XmlFile {
255    }
256
257    @Override
258    public String getBeanTypeHandled(boolean plural) {
259        return Bundle.getMessage(plural ? "BeanNameSignalSystems" : "BeanNameSignalSystem");
260    }
261
262    /**
263     * {@inheritDoc}
264     */
265    @Override
266    public Class<SignalSystem> getNamedBeanClass() {
267        return SignalSystem.class;
268    }
269
270    private final static Logger log = LoggerFactory.getLogger(DefaultSignalSystemManager.class);
271}