001package jmri.configurexml;
002
003import java.util.List;
004import java.util.SortedSet;
005
006import jmri.BeanSetting;
007import jmri.Block;
008import jmri.BlockManager;
009import jmri.InstanceManager;
010import jmri.Path;
011import jmri.Reporter;
012import jmri.Turnout;
013import org.jdom2.Element;
014
015/**
016 * Persistency implementation for BlockManager persistence.
017 * <p>
018 * The Block objects are not yet read in, pending a reliable write out!
019 * <p>
020 * Every block is written twice. First, the list of blocks is written without
021 * contents, so that we're sure they're all created on read-back. Then, they're
022 * written out again with contents, including the block references in the path
023 * elements.
024 *
025 * @author Bob Jacobsen Copyright: Copyright (c) 2008
026 * @since 2.1.2
027 */
028public class BlockManagerXml extends jmri.managers.configurexml.AbstractMemoryManagerConfigXML {
029
030    public BlockManagerXml() {
031    }
032
033    /**
034     * Subclass provides implementation to create the correct top element,
035     * including the type information. Default implementation is to use the
036     * local class here.
037     *
038     * @param memories The top-level element being created
039     */
040    @Override
041    public void setStoreElementClass(Element memories) {
042        memories.setAttribute("class", "jmri.configurexml.BlockManagerXml");
043    }
044
045    /**
046     * Store the contents of a BlockManager.
047     *
048     * @param o Object to store, of type BlockManager
049     * @return Element containing the complete info
050     */
051    @Override
052    public Element store(Object o) {
053        Element blocks = new Element("blocks");
054        setStoreElementClass(blocks);
055        BlockManager bm = (BlockManager) o;
056        if (bm != null) {
057
058            SortedSet<Block> blkList = bm.getNamedBeanSet();
059            // don't return an element if there are no blocks to include
060            if (blkList.isEmpty()) {
061                return null;
062            }
063
064            blocks.addContent(new Element("defaultspeed").addContent(bm.getDefaultSpeed()));
065            //TODO: The block info saved includes paths that at load time might
066            // reference blocks that haven't yet been loaded. In the past the
067            // workaround was to write all the blocks out twice: once with just
068            // the system and user names and then again with everything so that
069            // at load time the first set of (minimum) blocks would create all 
070            // the blocks before the second pass loaded the path information.
071            // To remove the necessity of doing this (and having duplicate
072            // blocks in the saved file) we've changed the load routine to make 
073            // two passes: once only creating the blocks (with system & user 
074            // names) and then a second pass with everything (including the 
075            // paths). At some point in the future (after a major release?) we 
076            // can remove writing the first set of blocks without contents 
077            // (and this (now way overly verbose) comment).
078            if (true) {
079                // write out first set of blocks without contents
080                for (Block b : blkList) {
081                    try {
082                        String bName = b.getSystemName();
083                        Element elem = new Element("block");
084                        elem.addContent(new Element("systemName").addContent(bName));
085
086                        // As a work-around for backward compatibility, store systemName as attribute.
087                        // TODO Remove this in e.g. JMRI 4.11.1 and then update all the loadref comparison files
088                        elem.setAttribute("systemName", bName);
089
090                        // the following null check is to catch a null pointer exception that sometimes was found to happen
091                        String uName = b.getUserName();
092                        if ((uName != null) && (!uName.isEmpty())) {
093                            elem.addContent(new Element("userName").addContent(uName));
094                        }
095                        log.debug("initial store Block {}", bName);
096
097                        // and put this element out
098                        blocks.addContent(elem);
099                    } catch (Exception e) {
100                        log.error("Exception: ", e);
101                    }
102                }
103            }
104
105            // write out with contents
106            for (Block b : blkList) {
107                String bName = b.getSystemName();
108                String uName = b.getUserName();
109                if (uName == null) {
110                    uName = "";
111                }
112                Element elem = new Element("block");
113                elem.addContent(new Element("systemName").addContent(bName));
114
115                // As a work-around for backward compatibility, store systemName as attribute.
116                // TODO Remove this in e.g. JMRI 4.11.1 and then update all the loadref comparison files
117                elem.setAttribute("systemName", bName);
118                log.debug("second store Block {}: {}", bName, uName);
119
120                // add length and curvature attributes
121                elem.setAttribute("length", Float.toString(b.getLengthMm()));
122                elem.setAttribute("curve", Integer.toString(b.getCurvature()));
123
124                // store common parts
125                storeCommon(b, elem);
126
127                if ((!b.getBlockSpeed().isEmpty()) && !b.getBlockSpeed().contains("Global")) {
128                    elem.addContent(new Element("speed").addContent(b.getBlockSpeed()));
129                }
130                String perm = "no";
131                if (b.getPermissiveWorking()) {
132                    perm = "yes";
133                }
134                elem.addContent(new Element("permissive").addContent(perm));
135                // Add content. First, the sensor
136                if (b.getNamedSensor() != null) {
137                    elem.addContent(new Element("occupancysensor").addContent(b.getNamedSensor().getName()));
138                }
139
140                if (!b.getDeniedBlocks().isEmpty()) {
141                    Element denied = new Element("deniedBlocks");
142                    b.getDeniedBlocks().forEach((deniedBlock) -> {
143                        denied.addContent(new Element("block").addContent(deniedBlock));
144                    });
145                    elem.addContent(denied);
146                }
147
148                // now the Reporter
149                Reporter r = b.getReporter();
150                if (r != null) {
151                    Element re = new Element("reporter");
152                    re.setAttribute("systemName", r.getSystemName());
153                    re.setAttribute("useCurrent", b.isReportingCurrent() ? "yes" : "no");
154                    elem.addContent(re);
155                }
156
157                if (bm.isSavedPathInfo()) {
158                    // then the paths
159                    List<Path> paths = b.getPaths();
160                    
161                    // in sorted order
162                    java.util.Collections.sort(paths);
163                    
164                    for (Path p : paths) {
165                        addPath(elem, p);
166                    }
167                    // and put this element out
168                }
169                blocks.addContent(elem);
170            }
171        }
172        return blocks;
173    }
174
175    private void addPath(Element e, Path p) {
176        // for now, persist two directions and a bean setting
177        Element pe = new Element("path");
178        pe.setAttribute("todir", "" + p.getToBlockDirection());
179        pe.setAttribute("fromdir", "" + p.getFromBlockDirection());
180        if (p.getBlock() != null) {
181            pe.setAttribute("block", "" + p.getBlock().getSystemName());
182        }
183        List<BeanSetting> l = p.getSettings();
184        if (l != null) {
185            for (BeanSetting bSet : l) {
186                addBeanSetting(pe, bSet);
187            }
188        }
189        e.addContent(pe);
190    }
191
192    private void addBeanSetting(Element e, BeanSetting bs) {
193        // persist bean name, type and value
194        Element bse = new Element("beansetting");
195        // for now, assume turnout
196        bse.setAttribute("setting", "" + bs.getSetting());
197        Element be = new Element("turnout");
198        be.setAttribute("systemName", bs.getBeanName());
199        bse.addContent(be);
200        e.addContent(bse);
201    }
202
203    /**
204     * Load Blocks into the existing BlockManager.
205     * <p>
206     * The BlockManager in the InstanceManager is created automatically.
207     *
208     * @param sharedBlocks  Element containing the block elements to load
209     * @param perNodeBlocks Per-node block elements to load
210     * @return true if successful
211     * @throws jmri.configurexml.JmriConfigureXmlException if error during load
212     */
213    @Override
214    public boolean load(Element sharedBlocks, Element perNodeBlocks) throws JmriConfigureXmlException {
215        boolean result = true;
216        try {
217            if (sharedBlocks.getChild("defaultspeed") != null) {
218                String speed = sharedBlocks.getChild("defaultspeed").getText();
219                if (speed != null && !speed.isEmpty()) {
220                    InstanceManager.getDefault(jmri.BlockManager.class).setDefaultSpeed(speed);
221                }
222            }
223        } catch (IllegalArgumentException ex) {
224            log.error("Ill Argument Exception: ", ex );
225        }
226
227        List<Element> list = sharedBlocks.getChildren("block");
228        log.debug("Found {} objects", list.size());
229
230        InstanceManager.getDefault(jmri.BlockManager.class).setPropertyChangesSilenced("beans", true);
231        // first pass don't load full contents (just create all the blocks)
232        for (Element block : list) {
233            loadBlock(block, false);
234        }
235
236        // second pass load full contents
237        for (Element block : list) {
238            loadBlock(block, true);
239        }
240        InstanceManager.getDefault(jmri.BlockManager.class).setPropertyChangesSilenced("beans", false);
241        
242        return result;
243    }
244
245    /**
246     * Utility method to load the individual Block objects.
247     *
248     * @param element Element containing one block
249     * @throws jmri.configurexml.JmriConfigureXmlException if element contains
250     *                                                     malformed or
251     *                                                     schematically invalid
252     *                                                     XMl
253     */
254    // default optional contentsFlag parameter to true
255    public void loadBlock(Element element) throws JmriConfigureXmlException {
256        loadBlock(element, true);
257    }
258
259    private void loadBlock(Element element, boolean contentsFlag) throws JmriConfigureXmlException {
260        String sysName = getSystemName(element);
261        String userName = getUserName(element);
262        log.debug("defined Block: ({})({})", sysName, (userName == null ? "<null>" : userName));
263
264        Block block = InstanceManager.getDefault(jmri.BlockManager.class).getBlock(sysName);
265        if (block == null) { // create it if doesn't exist
266            InstanceManager.getDefault(jmri.BlockManager.class).createNewBlock(sysName, userName);
267            block = InstanceManager.getDefault(jmri.BlockManager.class).getBlock(sysName);
268        }
269        if (block == null) {
270            log.error("Unable to load block with system name {} and username of {}", sysName, (userName == null ? "<null>" : userName));
271            return;
272        }
273        if (userName != null) {
274            block.setUserName(userName);
275        }
276        if (!contentsFlag) {
277            return;
278        }
279        if (element.getAttribute("length") != null) {
280            // load length in millimeters
281            block.setLength(Float.parseFloat(element.getAttribute("length").getValue()));
282        }
283        if (element.getAttribute("curve") != null) {
284            // load curve attribute
285            block.setCurvature(Integer.parseInt((element.getAttribute("curve")).getValue()));
286        }
287        try {
288            block.setBlockSpeed("Global");
289            if (element.getChild("speed") != null) {
290                String speed = element.getChild("speed").getText();
291                if (speed != null && !speed.isEmpty() && !speed.contains("Global")) {
292                    block.setBlockSpeed(speed);
293                }
294            }
295        } catch (jmri.JmriException ex) {
296            log.error("Exception setting block speed, ", ex );
297        }
298        if (element.getChild("permissive") != null) {
299            boolean permissive = false;
300            if (element.getChild("permissive").getText().equals("yes")) {
301                permissive = true;
302            }
303            block.setPermissiveWorking(permissive);
304        }
305        Element deniedBlocks = element.getChild("deniedBlocks");
306        if (deniedBlocks != null) {
307            List<Element> denyBlock = deniedBlocks.getChildren("block");
308            for (Element deny : denyBlock) {
309                block.addBlockDenyList(deny.getText());
310            }
311        }
312        // load common parts
313        loadCommon(block, element);
314
315        // load sensor if present
316        List<Element> sensors = element.getChildren("sensor");
317        if (sensors.size() > 1) {
318            log.error("More than one sensor present: {}", sensors.size());
319        }
320        if (sensors.size() == 1) {
321            //Old method of saving sensors
322            if (sensors.get(0).getAttribute("systemName") != null) {
323                String name = sensors.get(0).getAttribute("systemName").getValue();
324                if (!name.isEmpty()) {
325                    block.setSensor(name);
326                }
327            }
328        }
329        if (element.getChild("occupancysensor") != null) {
330            String name = element.getChild("occupancysensor").getText();
331            if (!name.isEmpty()) {
332                block.setSensor(name);
333            }
334        }
335
336        // load Reporter if present
337        List<Element> reporters = element.getChildren("reporter");
338        if (reporters.size() > 1) {
339            log.error("More than one reporter present: {}", reporters.size());
340        }
341        if (reporters.size() == 1) {
342            // Reporter
343            String name = reporters.get(0).getAttribute("systemName").getValue();
344            try {
345                Reporter reporter = InstanceManager.getDefault(jmri.ReporterManager.class).provideReporter(name);
346                block.setReporter(reporter);
347                block.setReportingCurrent(reporters.get(0).getAttribute("useCurrent").getValue().equals("yes"));
348            } catch (IllegalArgumentException ex) {
349                log.warn("failed to create Reporter \"{}\" during Block load", name);
350            }
351        }
352
353        // load paths if present
354        List<Element> paths = element.getChildren("path");
355
356        int startSize = block.getPaths().size();
357        int loadCount = 0;
358
359        for (Element path : paths) {
360            if (loadPath(block, path)) {
361                loadCount++;
362            }
363        }
364
365        if ((startSize > 0) && (loadCount > 0)) {
366            log.warn("Added {} paths to block {} that already had {} blocks.", loadCount++, sysName, startSize);
367        }
368
369        if (startSize + loadCount != block.getPaths().size()) {
370            log.error("Started with {} paths in block {}, added {} but final count is {}; something not right.",
371                    startSize, sysName, loadCount, block.getPaths().size());
372        }
373    }
374
375    /**
376     * Load path into an existing Block from XML.
377     *
378     * @param block   Block to receive path
379     * @param element Element containing path information
380     * @return true if path added to block; false otherwise
381     * @throws jmri.configurexml.JmriConfigureXmlException if element contains
382     *                                                     malformed or
383     *                                                     schematically invalid
384     *                                                     XMl
385     */
386    public boolean loadPath(Block block, Element element) throws JmriConfigureXmlException {
387        // load individual path
388        int toDir = 0;
389        int fromDir = 0;
390        try {
391            toDir = element.getAttribute("todir").getIntValue();
392            fromDir = element.getAttribute("fromdir").getIntValue();
393        } catch (org.jdom2.DataConversionException e) {
394            log.error("Could not parse path attribute");
395        } catch (NullPointerException e) {
396            handleException("Block Path entry in file missing required attribute",
397                    null, block.getSystemName(), block.getUserName(), null);
398        }
399
400        Block toBlock = null;
401        if (element.getAttribute("block") != null) {
402            String name = element.getAttribute("block").getValue();
403            toBlock = InstanceManager.getDefault(jmri.BlockManager.class).getBlock(name);
404        }
405        Path path = new Path(toBlock, toDir, fromDir);
406
407        List<Element> settings = element.getChildren("beansetting");
408        for (Element setting : settings) {
409            loadBeanSetting(path, setting);
410        }
411
412        // check if path already in block
413        if (!block.hasPath(path)) {
414            block.addPath(path);
415            return true;
416        } else {
417            log.debug("Skipping load of duplicate path {}", path);
418            return false;
419        }
420    }
421
422    /**
423     * Load BeanSetting into an existing Path.
424     *
425     * @param path    Path to receive BeanSetting
426     * @param element Element containing beansetting information
427     */
428    public void loadBeanSetting(Path path, Element element) {
429        int setting = 0;
430        try {
431            setting = element.getAttribute("setting").getIntValue();
432        } catch (org.jdom2.DataConversionException e) {
433            log.error("Could not parse beansetting attribute");
434        }
435        List<Element> turnouts = element.getChildren("turnout");
436        if (turnouts.size() != 1) {
437            log.error("invalid number of turnout element children");
438        }
439        String name = turnouts.get(0).getAttribute("systemName").getValue();
440        try {
441            Turnout t = InstanceManager.turnoutManagerInstance().provideTurnout(name);
442            BeanSetting bs = new BeanSetting(t, name, setting);
443            path.addSetting(bs);
444        } catch (IllegalArgumentException ex) {
445            log.warn("failed to create Turnout \"{}\" during Block load", name);
446        }
447    }
448
449    @Override
450    public int loadOrder() {
451        return InstanceManager.getDefault(jmri.BlockManager.class).getXMLOrder();
452    }
453
454    private final static org.slf4j.Logger log = org.slf4j.LoggerFactory.getLogger(BlockManagerXml.class);
455
456}