001/*
002 * This file is part of McIDAS-V
003 *
004 * Copyright 2007-2024
005 * Space Science and Engineering Center (SSEC)
006 * University of Wisconsin - Madison
007 * 1225 W. Dayton Street, Madison, WI 53706, USA
008 * https://www.ssec.wisc.edu/mcidas/
009 * 
010 * All Rights Reserved
011 * 
012 * McIDAS-V is built on Unidata's IDV and SSEC's VisAD libraries, and
013 * some McIDAS-V source code is based on IDV and VisAD source code.  
014 * 
015 * McIDAS-V is free software; you can redistribute it and/or modify
016 * it under the terms of the GNU Lesser Public License as published by
017 * the Free Software Foundation; either version 3 of the License, or
018 * (at your option) any later version.
019 * 
020 * McIDAS-V is distributed in the hope that it will be useful,
021 * but WITHOUT ANY WARRANTY; without even the implied warranty of
022 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
023 * GNU Lesser Public License for more details.
024 * 
025 * You should have received a copy of the GNU Lesser Public License
026 * along with this program.  If not, see https://www.gnu.org/licenses/.
027 */
028package edu.wisc.ssec.mcidasv.util;
029
030import static java.util.Objects.requireNonNull;
031import static javax.xml.xpath.XPathConstants.NODESET;
032
033import java.io.File;
034import java.io.IOException;
035import java.io.InputStream;
036import java.util.ArrayList;
037import java.util.Iterator;
038import java.util.List;
039import java.util.Map;
040import java.util.NoSuchElementException;
041import java.util.concurrent.ConcurrentHashMap;
042
043import javax.xml.parsers.DocumentBuilder;
044import javax.xml.parsers.DocumentBuilderFactory;
045import javax.xml.xpath.XPathExpression;
046import javax.xml.xpath.XPathExpressionException;
047import javax.xml.xpath.XPathFactory;
048
049import org.slf4j.Logger;
050import org.slf4j.LoggerFactory;
051import org.w3c.dom.Document;
052import org.w3c.dom.Element;
053import org.w3c.dom.Node;
054import org.w3c.dom.NodeList;
055
056import ucar.unidata.idv.IntegratedDataViewer;
057import ucar.unidata.idv.IdvResourceManager.IdvResource;
058import ucar.unidata.util.ResourceCollection.Resource;
059import ucar.unidata.xml.XmlResourceCollection;
060
061/**
062 * Documentation is still forthcoming, but remember that <b>no methods accept 
063 * {@code null} parameters!</b>
064 */
065public final class XPathUtils {
066
067    /** Logging object. */
068    private static final Logger logger =
069        LoggerFactory.getLogger(XPathUtils.class);
070    
071    /**
072     * Maps (and caches) the XPath {@link String} to its compiled
073     * {@link XPathExpression}.
074     */
075    private static final Map<String, XPathExpression> pathMap =
076        new ConcurrentHashMap<>();
077
078    /**
079     * Thou shalt not create an instantiation of this class!
080     */
081    private XPathUtils() {}
082
083    public static XPathExpression expr(String xPath) {
084        requireNonNull(xPath, "Cannot compile a null string");
085
086        XPathExpression expr = pathMap.get(xPath);
087        if (expr == null) {
088            try {
089                expr = XPathFactory.newInstance().newXPath().compile(xPath);
090                pathMap.put(xPath, expr);
091            } catch (XPathExpressionException e) {
092                throw new RuntimeException("Error compiling xpath", e);
093            }
094        }
095        return expr;
096    }
097
098    public static List<Node> eval(final XmlResourceCollection collection,
099                                  final String xPath) 
100    {
101        requireNonNull(collection, "Cannot search a null resource collection");
102        requireNonNull(xPath, "Cannot search using a null XPath query");
103
104        try {
105            List<Node> nodeList = new ArrayList<>();
106            XPathExpression expression = expr(xPath);
107
108            // Resources are the only things added to the list returned by 
109            // getResources().
110            @SuppressWarnings("unchecked")
111            List<Resource> files = collection.getResources();
112
113            for (int i = 0; i < files.size(); i++) {
114                if (!collection.isValid(i)) {
115                    continue;
116                }
117                String f = files.get(i).toString();
118                try (InputStream in = XPathUtils.class.getResourceAsStream(f)) {
119                    NodeList tmpList = (NodeList)expression.evaluate(loadXml(in), NODESET);
120                    for (int j = 0; j < tmpList.getLength(); j++) {
121                        nodeList.add(tmpList.item(j));
122                    }
123                } catch (IOException e) {
124                    logger.warn("Problem reading from file", e);
125                }
126
127
128            }
129            return nodeList;
130        } catch (XPathExpressionException e) {
131            throw new RuntimeException("Error evaluating xpath", e);
132        }
133    }
134
135    public static NodeList eval(final String xmlFile, final String xPath) {
136        requireNonNull(xmlFile, "Null path to a XML file");
137        requireNonNull(xPath, "Cannot search using a null XPath query");
138
139        try {
140            return (NodeList)expr(xPath).evaluate(loadXml(xmlFile), NODESET);
141        } catch (XPathExpressionException e) {
142            throw new RuntimeException("Error evaluation xpath", e);
143        }
144    }
145
146    public static NodeList eval(final Node root, final String xPath) {
147        requireNonNull(root, "Cannot search a null root node");
148        requireNonNull(xPath, "Cannot search using a null XPath query");
149
150        try {
151            return (NodeList)expr(xPath).evaluate(root, NODESET);
152        } catch (XPathExpressionException e) {
153            throw new RuntimeException("Error evaluation xpath", e);
154        }
155    }
156
157    public static List<Node> nodes(final IntegratedDataViewer idv,
158                                   final IdvResource collectionId,
159                                   final String xPath)
160    {
161        requireNonNull(idv);
162        requireNonNull(collectionId);
163        requireNonNull(xPath);
164
165        XmlResourceCollection collection =
166            idv.getResourceManager().getXmlResources(collectionId);
167        return nodes(collection, xPath);
168    }
169
170    public static List<Node> nodes(final XmlResourceCollection collection,
171                                   final String xPath)
172    {
173        requireNonNull(collection);
174        requireNonNull(xPath);
175        return eval(collection, xPath);
176    }
177
178    public static NodeListIterator nodes(final String xmlFile,
179                                         final String xPath)
180    {
181        requireNonNull(xmlFile);
182        requireNonNull(xPath);
183        return new NodeListIterator(eval(xmlFile, xPath));
184    }
185
186    public static NodeListIterator nodes(final Node root, final String xPath) {
187        requireNonNull(root);
188        requireNonNull(xPath);
189        return new NodeListIterator(eval(root, xPath));
190    }
191
192    public static NodeListIterator nodes(final Node root) {
193        requireNonNull(root);
194        return nodes(root, "//*");
195    }
196
197    public static List<Element> elements(final IntegratedDataViewer idv,
198                                         final IdvResource collectionId,
199                                         final String xPath)
200    {
201        requireNonNull(idv);
202        requireNonNull(collectionId);
203        requireNonNull(xPath);
204
205        XmlResourceCollection collection =
206            idv.getResourceManager().getXmlResources(collectionId);
207        return elements(collection, xPath);
208    }
209
210    public static List<Element> elements(final XmlResourceCollection collection,
211                                         final String xPath)
212    {
213        requireNonNull(collection);
214        requireNonNull(xPath);
215        List<Element> elements = new ArrayList<>();
216        for (Node n : eval(collection, xPath)) {
217            elements.add((Element)n);
218        }
219        return elements;
220    }
221
222    public static ElementListIterator elements(final String xmlFile,
223                                               final String xPath)
224    {
225        requireNonNull(xmlFile);
226        requireNonNull(xPath);
227        return new ElementListIterator(eval(xmlFile, xPath));
228    }
229
230    public static ElementListIterator elements(final Node root) {
231        requireNonNull(root);
232        return elements(root, "//*");
233    }
234
235    public static ElementListIterator elements(final Node root,
236                                               final String xPath)
237    {
238        requireNonNull(root);
239        requireNonNull(xPath);
240        return new ElementListIterator(eval(root, xPath));
241    }
242
243    public static Document loadXml(final String xmlFile) {
244        requireNonNull(xmlFile);
245
246        DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance();
247        factory.setNamespaceAware(false);
248        try {
249            DocumentBuilder builder = factory.newDocumentBuilder();
250            return builder.parse(xmlFile);
251        } catch (Exception e) {
252            throw new RuntimeException("Error loading XML file: "+e.getMessage(), e);
253        }
254    }
255
256    public static Document loadXml(final File xmlFile) {
257        requireNonNull(xmlFile);
258
259        DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance();
260        factory.setNamespaceAware(false);
261        try {
262            DocumentBuilder builder = factory.newDocumentBuilder();
263            return builder.parse(xmlFile);
264        } catch (Exception e) {
265            throw new RuntimeException("Error loading XML file: "+e.getMessage(), e);
266        }
267    }
268
269    public static Document loadXml(final InputStream in) {
270        requireNonNull(in);
271        
272        DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance();
273        factory.setNamespaceAware(false);
274        try {
275            DocumentBuilder builder = factory.newDocumentBuilder();
276            return builder.parse(in);
277        } catch (Exception e) {
278            throw new RuntimeException("Error loading XML from input stream: "+e.getMessage(), e);
279        }
280    }
281
282    public static class NodeListIterator implements Iterable<Node>,
283                                                    Iterator<Node>
284    {
285        private final NodeList nodeList;
286        private int index = 0;
287
288        public NodeListIterator(final NodeList nodeList) {
289            requireNonNull(nodeList);
290            this.nodeList = nodeList;
291        }
292
293        public Iterator<Node> iterator() {
294            return this;
295        }
296
297        @Override public boolean hasNext() {
298            return index < nodeList.getLength();
299        }
300
301        @Override public Node next() {
302            Node result = nodeList.item(index++);
303            if (result != null) {
304                return result;
305            } else {
306                throw new NoSuchElementException("No more nodes to iterate through!");
307            }
308        }
309
310        @Override public void remove() {
311            throw new UnsupportedOperationException("not implemented");
312        }
313    }
314
315    public static class ElementListIterator implements Iterable<Element>,
316                                                       Iterator<Element>
317    {
318        private final NodeList nodeList;
319        private int index = 0;
320
321        public ElementListIterator(final NodeList nodeList) {
322            requireNonNull(nodeList);
323            this.nodeList = nodeList;
324        }
325
326        public Iterator<Element> iterator() {
327            return this;
328        }
329
330        @Override public boolean hasNext() {
331            return index < nodeList.getLength();
332        }
333
334        @Override public Element next() {
335            Element result = (Element)nodeList.item(index++);
336            if (result != null) {
337                return result;
338            } else {
339                throw new NoSuchElementException("No more elements to iterate through!");
340            }
341        }
342
343        @Override public void remove() {
344            throw new UnsupportedOperationException("not implemented");
345        }
346    }
347}