001package jmri.jmrit.symbolicprog.tabbedframe;
002
003import java.awt.event.ActionEvent;
004import java.io.File;
005import java.io.IOException;
006import java.util.ArrayList;
007import java.util.List;
008import java.util.regex.Matcher;
009import java.util.regex.Pattern;
010
011import javax.swing.AbstractAction;
012import javax.swing.JFileChooser;
013import javax.swing.JPanel;
014
015import jmri.InstanceManager;
016import jmri.jmrit.XmlFile;
017import jmri.jmrit.symbolicprog.NameFile;
018import jmri.util.swing.JmriJOptionPane;
019
020import org.jdom2.Attribute;
021import org.jdom2.Element;
022import org.jdom2.JDOMException;
023
024/**
025 * Check the names in an XML programmer file against the names.xml definitions
026 *
027 * @author Bob Jacobsen Copyright (C) 2001, 2007
028 * @see jmri.jmrit.XmlFile
029 */
030public class ProgCheckAction extends AbstractAction {
031
032    public ProgCheckAction(String s, JPanel who) {
033        super(s);
034        _who = who;
035    }
036
037    JFileChooser fci;
038
039    JPanel _who;
040
041    @Override
042    public void actionPerformed(ActionEvent e) {
043        if (fci == null) {
044            fci = jmri.jmrit.XmlFile.userFileChooser("XML files", "xml");
045        }
046        // request the filename from an open dialog
047        fci.rescanCurrentDirectory();
048        int retVal = fci.showOpenDialog(_who);
049
050        // handle selection or cancel
051        if (retVal == JFileChooser.APPROVE_OPTION) {
052            File file = fci.getSelectedFile();
053            if (log.isDebugEnabled()) {
054                log.debug("located file {} for XML processing", file);
055            }
056
057            warnMissingNames(file);
058
059            // as ugly special case, do reverse check for Comprehensive programmer
060            if (file.getName().toLowerCase().endsWith("comprehensive.xml")) {
061                warnIncompleteComprehensive(file);
062            }
063        } else {
064            log.info("XmlFileCheckAction cancelled in open dialog");
065        }
066    }
067
068    /**
069     * Find all of the display elements descending from this element.
070     *
071     * @param el   the element to search
072     * @param list the list that will be populated with the found elements
073     */
074    static protected void expandElement(Element el, List<Element> list) {
075        // get the leaves here
076        list.addAll(el.getChildren("display"));
077
078        List<Element> children = el.getChildren();
079        for (int i = 0; i < children.size(); i++) {
080            expandElement(children.get(i), list);
081        }
082    }
083
084    /**
085     * Check for names in programer that are not in names.xml
086     * @param file A decoder definition XML file to be checked
087     */
088    void warnMissingNames(File file) {
089        String result = checkMissingNames(file);
090        if (result.equals("")) {
091            JmriJOptionPane.showMessageDialog(_who, "OK, all variables in file are known");
092        } else {
093            JmriJOptionPane.showMessageDialog(_who, result);
094        }
095    }
096
097    static String checkMissingNames(File file) {
098        try {
099            Element root = readFile(file);
100            log.debug("parsing complete");
101
102            // check to see if there's a programmer element
103            if (root.getChild("programmer") == null) {
104                log.warn("Does not appear to be a programmer file");
105                return "Does not appear to be a programmer file";
106            }
107
108            // walk the entire tree of elements, saving a reference
109            // to all of the "display" elements
110            List<Element> varList = new ArrayList<>();
111            expandElement(root.getChild("programmer"), varList);
112            log.debug("found {} display elements", varList.size());
113            NameFile nfile = InstanceManager.getDefault(NameFile.class);
114
115            StringBuilder warnings = new StringBuilder("");
116
117            for (int i = 0; i < varList.size(); i++) {
118                Element varElement = (varList.get(i));
119                // for each variable, see if can find in names file
120                Attribute nameAttr = varElement.getAttribute("item");
121                String name = null;
122                if (nameAttr != null) {
123                    name = nameAttr.getValue();
124                }
125                if (log.isDebugEnabled()) {
126                    log.debug("Variable called \"{}\"", (name != null) ? name : "<none>");
127                }
128                if (!(name == null ? false : nfile.checkName(name))) {
129                    log.warn("Variable not found in name list: name=\"{}\"", name);
130                    warnings.append("Variable not found in name list: name=\"").append(name).append("\"\n");
131                }
132            }
133
134            String result = warnings.toString();
135            return !result.isEmpty() ? "Names missing from Comprehensive.xml\n" + result : "";
136
137        } catch (IOException | JDOMException ex) {
138            return "Error parsing programmer file: " + ex;
139        }
140    }
141
142    /**
143     * Check for names in names.xml that are not in file
144     * @param file A decoder definition XML file to be checked
145     */
146    void warnIncompleteComprehensive(File file) {
147        String result = checkIncompleteComprehensive(file);
148        if (result.equals("")) {
149            JmriJOptionPane.showMessageDialog(_who, "OK, Comprehensive.xml is complete");
150        } else {
151            JmriJOptionPane.showMessageDialog(_who, result);
152        }
153    }
154
155    static String checkIncompleteComprehensive(File file) {
156        // handle the file (later should be outside this thread?)
157        try {
158            Element root = readFile(file);
159            if (log.isDebugEnabled()) {
160                log.debug("parsing complete");
161            }
162
163            // check to see if there's a programmer element
164            if (root.getChild("programmer") == null) {
165                log.warn("Does not appear to be a programmer file");
166                return "Does not appear to be a programmer file";
167            }
168
169            // walk the entire tree of elements, saving a reference
170            // to all of the "display" elements
171            List<Element> varList = new ArrayList<>();
172            expandElement(root.getChild("programmer"), varList);
173            if (log.isDebugEnabled()) {
174                log.debug("found {} display elements", varList.size());
175            }
176            NameFile nfile = InstanceManager.getDefault(NameFile.class);
177
178            StringBuilder warnings = new StringBuilder("");
179
180            // for each item in names, see if found in this file
181            nfile.names().stream().filter((s) -> !(functionMapName(s))).forEachOrdered((s) -> {
182                boolean found = false;
183                for (int i = 0; i < varList.size(); i++) {
184                    Element varElement = varList.get(i);
185                    // for each variable, see if can find in names file
186                    Attribute nameAttr = varElement.getAttribute("item");
187                    String name = null;
188                    if (nameAttr != null) {
189                        name = nameAttr.getValue();
190                    }
191                    // now check
192                    if (name != null && name.equals(s)) {
193                        found = true;
194                    }
195                }
196                if (!found) {
197                    log.warn("Variable not in Comprehensive: name=\"{}\"", s);
198                    warnings.append("Variable not in Comprehensive: name=\"").append(s).append("\"\n");
199                }
200            });
201
202            return warnings.toString();
203        } catch (IOException | JDOMException ex) {
204            return "Error parsing programmer file: " + ex;
205        }
206    }
207
208    /**
209     * Check if the name is a function name, e.g. "F5 controls output 8" or
210     * "FL(f) controls output 14"
211     * @param name Possible function name to check
212     * @return true if the input is a valid name
213     */
214    static boolean functionMapName(String name) {
215        if (numericPattern == null) {
216            numericPattern = Pattern.compile(numericRegex);
217        }
218        if (ffPattern == null) {
219            ffPattern = Pattern.compile(ffRegex);
220        }
221        if (frPattern == null) {
222            frPattern = Pattern.compile(frRegex);
223        }
224
225        Matcher matcher = numericPattern.matcher(name);
226        if (matcher.matches()) {
227            return true;
228        }
229        matcher = ffPattern.matcher(name);
230        if (matcher.matches()) {
231            return true;
232        }
233        matcher = frPattern.matcher(name);
234        return matcher.matches();
235    }
236    static final String numericRegex = "^F(\\d++) controls output (\\d++)$";
237    static volatile Pattern numericPattern;
238    static final String ffRegex = "^FL\\(f\\) controls output (\\d++)$";
239    static volatile Pattern ffPattern;
240    static final String frRegex = "^FL\\(r\\) controls output (\\d++)$";
241    static volatile Pattern frPattern;
242
243    /**
244     * Ask SAX to read and verify a file
245     * @param file XML-formatted input file
246     * @return root element if successful
247     * @throws org.jdom2.JDOMException if file can't be parsed
248     * @throws java.io.IOException  if problems reading file
249     */
250    static Element readFile(File file) throws org.jdom2.JDOMException, java.io.IOException {
251        XmlFile xf = new XmlFile() {
252        };   // odd syntax is due to XmlFile being abstract
253
254        return xf.rootFromFile(file);
255
256    }
257
258    // initialize logging
259    private final static org.slf4j.Logger log = org.slf4j.LoggerFactory.getLogger(ProgCheckAction.class);
260
261}