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                if (b.getIsGhost()) {
136                    elem.addContent(new Element("ghost").addContent("yes"));
137                }
138                // Add content. First, the sensor
139                var sensor = b.getNamedSensor();
140                if ( sensor != null) {
141                    elem.addContent(new Element("occupancysensor").addContent(sensor.getName()));
142                }
143
144                if (!b.getDeniedBlocks().isEmpty()) {
145                    Element denied = new Element("deniedBlocks");
146                    b.getDeniedBlocks().forEach( deniedBlock ->
147                        denied.addContent(new Element("block").addContent(deniedBlock)));
148                    elem.addContent(denied);
149                }
150
151                // now the Reporter
152                Reporter r = b.getReporter();
153                if (r != null) {
154                    Element re = new Element("reporter");
155                    re.setAttribute("systemName", r.getSystemName());
156                    re.setAttribute("useCurrent", b.isReportingCurrent() ? "yes" : "no");
157                    elem.addContent(re);
158                }
159
160                if (bm.isSavedPathInfo()) {
161                    // then the paths
162                    List<Path> paths = b.getPaths();
163                    
164                    // in sorted order
165                    java.util.Collections.sort(paths);
166                    
167                    for (Path p : paths) {
168                        addPath(elem, p);
169                    }
170                    // and put this element out
171                }
172                blocks.addContent(elem);
173            }
174        }
175        return blocks;
176    }
177
178    private void addPath(Element e, Path p) {
179        // for now, persist two directions and a bean setting
180        Element pe = new Element("path");
181        pe.setAttribute("todir", "" + p.getToBlockDirection());
182        pe.setAttribute("fromdir", "" + p.getFromBlockDirection());
183        if (p.getBlock() != null) {
184            pe.setAttribute("block", "" + p.getBlock().getSystemName());
185        }
186        List<BeanSetting> l = p.getSettings();
187        if (l != null) {
188            for (BeanSetting bSet : l) {
189                addBeanSetting(pe, bSet);
190            }
191        }
192        e.addContent(pe);
193    }
194
195    private void addBeanSetting(Element e, BeanSetting bs) {
196        // persist bean name, type and value
197        Element bse = new Element("beansetting");
198        // for now, assume turnout
199        bse.setAttribute("setting", "" + bs.getSetting());
200        Element be = new Element("turnout");
201        be.setAttribute("systemName", bs.getBeanName());
202        bse.addContent(be);
203        e.addContent(bse);
204    }
205
206    /**
207     * Load Blocks into the existing BlockManager.
208     * <p>
209     * The BlockManager in the InstanceManager is created automatically.
210     *
211     * @param sharedBlocks  Element containing the block elements to load
212     * @param perNodeBlocks Per-node block elements to load
213     * @return true if successful
214     * @throws jmri.configurexml.JmriConfigureXmlException if error during load
215     */
216    @Override
217    public boolean load(Element sharedBlocks, Element perNodeBlocks) throws JmriConfigureXmlException {
218        boolean result = true;
219        try {
220            if (sharedBlocks.getChild("defaultspeed") != null) {
221                String speed = sharedBlocks.getChild("defaultspeed").getText();
222                if (speed != null && !speed.isEmpty()) {
223                    InstanceManager.getDefault(BlockManager.class).setDefaultSpeed(speed);
224                }
225            }
226        } catch (IllegalArgumentException ex) {
227            log.error("Ill Argument Exception: ", ex );
228        }
229
230        List<Element> list = sharedBlocks.getChildren("block");
231        log.debug("Found {} objects", list.size());
232
233        InstanceManager.getDefault(BlockManager.class).setPropertyChangesSilenced("beans", true);
234        // first pass don't load full contents (just create all the blocks)
235        for (Element block : list) {
236            loadBlock(block, false);
237        }
238
239        // second pass load full contents
240        for (Element block : list) {
241            loadBlock(block, true);
242        }
243        InstanceManager.getDefault(BlockManager.class).setPropertyChangesSilenced("beans", false);
244        
245        return result;
246    }
247
248    /**
249     * Utility method to load the individual Block objects.
250     *
251     * @param element Element containing one block
252     * @throws jmri.configurexml.JmriConfigureXmlException if element contains
253     *                                                     malformed or
254     *                                                     schematically invalid
255     *                                                     XMl
256     */
257    // default optional contentsFlag parameter to true
258    public void loadBlock(Element element) throws JmriConfigureXmlException {
259        loadBlock(element, true);
260    }
261
262    private void loadBlock(Element element, boolean contentsFlag) throws JmriConfigureXmlException {
263        String sysName = getSystemName(element);
264        String userName = getUserName(element);
265        log.debug("defined Block: ({})({})", sysName, (userName == null ? "<null>" : userName));
266
267        Block block = InstanceManager.getDefault(BlockManager.class).getBlock(sysName);
268        if (block == null) { // create it if doesn't exist
269            InstanceManager.getDefault(BlockManager.class).createNewBlock(sysName, userName);
270            block = InstanceManager.getDefault(BlockManager.class).getBlock(sysName);
271        }
272        if (block == null) {
273            log.error("Unable to load block with system name {} and username of {}",
274                sysName, (userName == null ? "<null>" : userName));
275            return;
276        }
277        if (userName != null) {
278            block.setUserName(userName);
279        }
280        if (!contentsFlag) {
281            return;
282        }
283        if (element.getAttribute("length") != null) {
284            // load length in millimeters
285            block.setLength(Float.parseFloat(element.getAttribute("length").getValue()));
286        }
287        if (element.getAttribute("curve") != null) {
288            // load curve attribute
289            block.setCurvature(Integer.parseInt((element.getAttribute("curve")).getValue()));
290        }
291        try {
292            block.setBlockSpeed("Global");
293            if (element.getChild("speed") != null) {
294                String speed = element.getChild("speed").getText();
295                if (speed != null && !speed.isEmpty() && !speed.contains("Global")) {
296                    block.setBlockSpeed(speed);
297                }
298            }
299        } catch (jmri.JmriException ex) {
300            log.error("Exception setting block speed, ", ex );
301        }
302        if (element.getChild("permissive") != null) {
303            boolean permissive = false;
304            if ("yes".equals(element.getChild("permissive").getText())) {
305                permissive = true;
306            }
307            block.setPermissiveWorking(permissive);
308        }
309        if (element.getChild("ghost") != null) {
310            block.setIsGhost("yes".equals(element.getChild("ghost").getText()));
311        }
312        Element deniedBlocks = element.getChild("deniedBlocks");
313        if (deniedBlocks != null) {
314            List<Element> denyBlock = deniedBlocks.getChildren("block");
315            for (Element deny : denyBlock) {
316                block.addBlockDenyList(deny.getText());
317            }
318        }
319        // load common parts
320        loadCommon(block, element);
321
322        // load sensor if present
323        List<Element> sensors = element.getChildren("sensor");
324        if (sensors.size() > 1) {
325            log.error("More than one sensor present: {}", sensors.size());
326        }
327        if (sensors.size() == 1 && sensors.get(0).getAttribute("systemName") != null) {
328            //Old method of saving sensors
329            String name = sensors.get(0).getAttribute("systemName").getValue();
330            if (!name.isEmpty()) {
331                block.setSensor(name);
332            }
333        }
334        if (element.getChild("occupancysensor") != null) {
335            String name = element.getChild("occupancysensor").getText();
336            if (!name.isEmpty()) {
337                block.setSensor(name);
338            }
339        }
340
341        // load Reporter if present
342        List<Element> reporters = element.getChildren("reporter");
343        if (reporters.size() > 1) {
344            log.error("More than one reporter present: {}", reporters.size());
345        }
346        if (reporters.size() == 1) {
347            // Reporter
348            String name = reporters.get(0).getAttribute("systemName").getValue();
349            try {
350                Reporter reporter = InstanceManager.getDefault(jmri.ReporterManager.class).provideReporter(name);
351                block.setReporter(reporter);
352                block.setReportingCurrent("yes".equals(reporters.get(0).getAttribute("useCurrent").getValue()));
353            } catch (IllegalArgumentException ex) {
354                log.warn("failed to create Reporter \"{}\" during Block load", name);
355            }
356        }
357
358        // load paths if present
359        List<Element> paths = element.getChildren("path");
360
361        int startSize = block.getPaths().size();
362        int loadCount = 0;
363
364        for (Element path : paths) {
365            if (loadPath(block, path)) {
366                loadCount++;
367            }
368        }
369
370        if (startSize > 0 && loadCount > 0) {
371            log.warn("Added {} paths to block {} that already had {} blocks.", loadCount, sysName, startSize);
372            loadCount++;
373        }
374
375        if (startSize + loadCount != block.getPaths().size()) {
376            log.error("Started with {} paths in block {}, added {} but final count is {}; something not right.",
377                    startSize, sysName, loadCount, block.getPaths().size());
378        }
379    }
380
381    /**
382     * Load path into an existing Block from XML.
383     *
384     * @param block   Block to receive path
385     * @param element Element containing path information
386     * @return true if path added to block; false otherwise
387     * @throws jmri.configurexml.JmriConfigureXmlException if element contains
388     *                                                     malformed or
389     *                                                     schematically invalid
390     *                                                     XMl
391     */
392    public boolean loadPath(Block block, Element element) throws JmriConfigureXmlException {
393        // load individual path
394        int toDir = 0;
395        int fromDir = 0;
396
397        var toElem = element.getAttribute("todir");
398        var fromElem = element.getAttribute("fromdir");
399        if ( toElem == null || fromElem == null ) {
400            handleException("Block Path entry in file missing required attribute",
401                    null, block.getSystemName(), block.getUserName(), null);
402        } else {
403            try {
404                toDir = toElem.getIntValue();
405                fromDir = fromElem.getIntValue();
406            } catch (org.jdom2.DataConversionException e) {
407                log.error("Could not parse Block {} path attribute {}", block.getDisplayName(), e.getMessage());
408            }
409        }
410
411        Block toBlock = null;
412        if (element.getAttribute("block") != null) {
413            String name = element.getAttribute("block").getValue();
414            toBlock = InstanceManager.getDefault(BlockManager.class).getBlock(name);
415        }
416        Path path = new Path(toBlock, toDir, fromDir);
417
418        List<Element> settings = element.getChildren("beansetting");
419        for (Element setting : settings) {
420            loadBeanSetting(path, setting);
421        }
422
423        // check if path already in block
424        if (!block.hasPath(path)) {
425            block.addPath(path);
426            return true;
427        } else {
428            log.debug("Skipping load of duplicate path {}", path);
429            return false;
430        }
431    }
432
433    /**
434     * Load BeanSetting into an existing Path.
435     *
436     * @param path    Path to receive BeanSetting
437     * @param element Element containing beansetting information
438     */
439    public void loadBeanSetting(Path path, Element element) {
440        int setting = 0;
441        try {
442            setting = element.getAttribute("setting").getIntValue();
443        } catch (org.jdom2.DataConversionException e) {
444            log.error("Could not parse beansetting attribute");
445        }
446        List<Element> turnouts = element.getChildren("turnout");
447        if (turnouts.size() != 1) {
448            log.error("invalid number of turnout element children");
449        }
450        String name = turnouts.get(0).getAttribute("systemName").getValue();
451        try {
452            Turnout t = InstanceManager.turnoutManagerInstance().provideTurnout(name);
453            BeanSetting bs = new BeanSetting(t, name, setting);
454            path.addSetting(bs);
455        } catch (IllegalArgumentException ex) {
456            log.warn("failed to create Turnout \"{}\" during Block load", name);
457        }
458    }
459
460    @Override
461    public int loadOrder() {
462        return InstanceManager.getDefault(BlockManager.class).getXMLOrder();
463    }
464
465    private static final org.slf4j.Logger log = org.slf4j.LoggerFactory.getLogger(BlockManagerXml.class);
466
467}