001// License: GPL. For details, see LICENSE file.
002package org.openstreetmap.josm.data.imagery;
003
004import java.io.InputStream;
005import java.net.MalformedURLException;
006import java.net.URL;
007import java.util.Locale;
008import java.util.function.BiPredicate;
009
010import javax.xml.namespace.QName;
011import javax.xml.stream.XMLStreamException;
012import javax.xml.stream.XMLStreamReader;
013
014import org.openstreetmap.josm.tools.Utils;
015import org.openstreetmap.josm.tools.XmlUtils;
016
017/**
018 * Helper class for handling OGC GetCapabilities documents
019 * @since 10993
020 */
021public final class GetCapabilitiesParseHelper {
022    enum TransferMode {
023        KVP("KVP"),
024        REST("RESTful");
025
026        private final String typeString;
027
028        TransferMode(String urlString) {
029            this.typeString = urlString;
030        }
031
032        private String getTypeString() {
033            return typeString;
034        }
035
036        static TransferMode fromString(String s) {
037            for (TransferMode type : TransferMode.values()) {
038                if (type.getTypeString().equals(s)) {
039                    return type;
040                }
041            }
042            return null;
043        }
044    }
045
046    /**
047     * OWS namespace address
048     */
049    public static final String OWS_NS_URL = "http://www.opengis.net/ows/1.1";
050    /**
051     * XML xlink namespace address
052     */
053    public static final String XLINK_NS_URL = "http://www.w3.org/1999/xlink";
054
055    /**
056     * QNames in OWS namespace
057     */
058    // CHECKSTYLE.OFF: SingleSpaceSeparator
059    static final QName QN_OWS_ALLOWED_VALUES      = new QName(OWS_NS_URL, "AllowedValues");
060    static final QName QN_OWS_CONSTRAINT          = new QName(OWS_NS_URL, "Constraint");
061    static final QName QN_OWS_DCP                 = new QName(OWS_NS_URL, "DCP");
062    static final QName QN_OWS_GET                 = new QName(OWS_NS_URL, "Get");
063    static final QName QN_OWS_HTTP                = new QName(OWS_NS_URL, "HTTP");
064    static final QName QN_OWS_IDENTIFIER          = new QName(OWS_NS_URL, "Identifier");
065    static final QName QN_OWS_LOWER_CORNER        = new QName(OWS_NS_URL, "LowerCorner");
066    static final QName QN_OWS_OPERATION           = new QName(OWS_NS_URL, "Operation");
067    static final QName QN_OWS_OPERATIONS_METADATA = new QName(OWS_NS_URL, "OperationsMetadata");
068    static final QName QN_OWS_SUPPORTED_CRS       = new QName(OWS_NS_URL, "SupportedCRS");
069    static final QName QN_OWS_TITLE               = new QName(OWS_NS_URL, "Title");
070    static final QName QN_OWS_UPPER_CORNER        = new QName(OWS_NS_URL, "UpperCorner");
071    static final QName QN_OWS_VALUE               = new QName(OWS_NS_URL, "Value");
072    static final QName QN_OWS_WGS84_BOUNDING_BOX  = new QName(OWS_NS_URL, "WGS84BoundingBox");
073    // CHECKSTYLE.ON: SingleSpaceSeparator
074
075    private GetCapabilitiesParseHelper() {
076        // Hide default constructor for utilities classes
077    }
078
079    /**
080     * Returns reader with properties set for parsing WM(T)S documents
081     *
082     * @param in InputStream with pointing to GetCapabilities XML stream
083     * @return safe XMLStreamReader, that is not validating external entities, nor loads DTD's
084     * @throws XMLStreamException if any XML stream error occurs
085     */
086    public static XMLStreamReader getReader(InputStream in) throws XMLStreamException {
087        return XmlUtils.newSafeXMLInputFactory().createXMLStreamReader(in);
088    }
089
090    /**
091     * Moves the reader to the closing tag of current tag.
092     * @param reader XMLStreamReader which should be moved
093     * @throws XMLStreamException when parse exception occurs
094     */
095    public static void moveReaderToEndCurrentTag(XMLStreamReader reader) throws XMLStreamException {
096        int level = 0;
097        QName tag = reader.getName();
098        for (int event = reader.getEventType(); reader.hasNext(); event = reader.next()) {
099            if (XMLStreamReader.START_ELEMENT == event) {
100                level += 1;
101            } else if (XMLStreamReader.END_ELEMENT == event) {
102                level -= 1;
103                if (level == 0 && tag.equals(reader.getName())) {
104                    return;
105                }
106            }
107            if (level < 0) {
108                throw new IllegalStateException("WMTS Parser error - moveReaderToEndCurrentTag failed to find closing tag");
109            }
110        }
111        throw new IllegalStateException("WMTS Parser error - moveReaderToEndCurrentTag failed to find closing tag");
112    }
113
114    /**
115     * Returns whole content of the element that reader is pointing at, including other XML elements within (with their tags).
116     *
117     * @param reader XMLStreamReader that should point to start of element
118     * @return content of current tag
119     * @throws XMLStreamException if any XML stream error occurs
120     */
121    public static String getElementTextWithSubtags(XMLStreamReader reader) throws XMLStreamException {
122        StringBuilder ret = new StringBuilder();
123        int level = 0;
124        QName tag = reader.getName();
125        for (int event = reader.getEventType(); reader.hasNext(); event = reader.next()) {
126            if (XMLStreamReader.START_ELEMENT == event) {
127                if (level > 0) {
128                    ret.append('<').append(reader.getLocalName()).append('>');
129                }
130                level += 1;
131            } else if (XMLStreamReader.END_ELEMENT == event) {
132                level -= 1;
133                if (level == 0 && tag.equals(reader.getName())) {
134                    return ret.toString();
135                }
136                ret.append("</").append(reader.getLocalName()).append('>');
137            } else if (XMLStreamReader.CHARACTERS == event) {
138                ret.append(reader.getText());
139            }
140            if (level < 0) {
141                throw new IllegalStateException("WMTS Parser error - moveReaderToEndCurrentTag failed to find closing tag");
142            }
143        }
144        throw new IllegalStateException("WMTS Parser error - moveReaderToEndCurrentTag failed to find closing tag");
145    }
146
147
148    /**
149     * Moves reader to first occurrence of the structure equivalent of Xpath tags[0]/tags[1]../tags[n]. If fails to find
150     * moves the reader to the closing tag of current tag
151     *
152     * @param tags array of tags
153     * @param reader XMLStreamReader which should be moved
154     * @return true if tag was found, false otherwise
155     * @throws XMLStreamException See {@link XMLStreamReader}
156     */
157    public static boolean moveReaderToTag(XMLStreamReader reader, QName... tags) throws XMLStreamException {
158        return moveReaderToTag(reader, QName::equals, tags);
159    }
160
161    /**
162     * Moves reader to first occurrence of the structure equivalent of Xpath tags[0]/tags[1]../tags[n]. If fails to find
163     * moves the reader to the closing tag of current tag
164     *
165     * @param tags array of tags
166     * @param reader XMLStreamReader which should be moved
167     * @param equalsFunc function to check equality of the tags
168     * @return true if tag was found, false otherwise
169     * @throws XMLStreamException See {@link XMLStreamReader}
170     */
171    public static boolean moveReaderToTag(XMLStreamReader reader,
172            BiPredicate<QName, QName> equalsFunc, QName... tags) throws XMLStreamException {
173        QName stopTag = reader.getName();
174        int currentLevel = 0;
175        QName searchTag = tags[currentLevel];
176        QName parentTag = null;
177        QName skipTag = null;
178
179        for (int event = 0; //skip current element, so we will not skip it as a whole
180                reader.hasNext() && !(event == XMLStreamReader.END_ELEMENT && equalsFunc.test(stopTag, reader.getName()));
181                event = reader.next()) {
182            if (event == XMLStreamReader.END_ELEMENT && skipTag != null && equalsFunc.test(skipTag, reader.getName())) {
183                skipTag = null;
184            }
185            if (skipTag == null) {
186                if (event == XMLStreamReader.START_ELEMENT) {
187                    if (equalsFunc.test(searchTag, reader.getName())) {
188                        currentLevel += 1;
189                        if (currentLevel >= tags.length) {
190                            return true; // found!
191                        }
192                        parentTag = searchTag;
193                        searchTag = tags[currentLevel];
194                    } else {
195                        skipTag = reader.getName();
196                    }
197                }
198
199                if (event == XMLStreamReader.END_ELEMENT && parentTag != null && equalsFunc.test(parentTag, reader.getName())) {
200                    currentLevel -= 1;
201                    searchTag = parentTag;
202                    if (currentLevel >= 0) {
203                        parentTag = tags[currentLevel];
204                    } else {
205                        parentTag = null;
206                    }
207                }
208            }
209        }
210        return false;
211    }
212
213    /**
214     * Parses Operation[@name='GetTile']/DCP/HTTP/Get section. Returns when reader is on Get closing tag.
215     * @param reader StAX reader instance
216     * @return TransferMode coded in this section
217     * @throws XMLStreamException See {@link XMLStreamReader}
218     */
219    public static TransferMode getTransferMode(XMLStreamReader reader) throws XMLStreamException {
220        QName getQname = QN_OWS_GET;
221
222        Utils.ensure(getQname.equals(reader.getName()), "WMTS Parser state invalid. Expected element %s, got %s",
223                getQname, reader.getName());
224        for (int event = reader.getEventType();
225                reader.hasNext() && !(event == XMLStreamReader.END_ELEMENT && getQname.equals(reader.getName()));
226                event = reader.next()) {
227            if (event == XMLStreamReader.START_ELEMENT && QN_OWS_CONSTRAINT.equals(reader.getName())
228             && "GetEncoding".equals(reader.getAttributeValue("", "name"))) {
229                moveReaderToTag(reader, QN_OWS_ALLOWED_VALUES, QN_OWS_VALUE);
230                return TransferMode.fromString(reader.getElementText());
231            }
232        }
233        return null;
234    }
235
236    /**
237     * Normalize url
238     *
239     * @param url URL
240     * @return normalized URL
241     * @throws MalformedURLException in case of malformed URL
242     * @since 10993
243     */
244    public static String normalizeCapabilitiesUrl(String url) throws MalformedURLException {
245        URL inUrl = new URL(url);
246        URL ret = new URL(inUrl.getProtocol(), inUrl.getHost(), inUrl.getPort(), inUrl.getFile());
247        return ret.toExternalForm();
248    }
249
250    /**
251     * Convert CRS identifier to plain code
252     * @param crsIdentifier CRS identifier
253     * @return CRS Identifier as it is used within JOSM (without prefix)
254     * @see <a href="https://portal.opengeospatial.org/files/?artifact_id=24045">
255     *     Definition identifier URNs in OGC namespace, chapter 7.2: URNs for single objects</a>
256     */
257    public static String crsToCode(String crsIdentifier) {
258        if (crsIdentifier.startsWith("urn:ogc:def:crs:")) {
259            return crsIdentifier.replaceFirst("urn:ogc:def:crs:([^:]*)(?::.*)?:(.*)$", "$1:$2").toUpperCase(Locale.ENGLISH);
260        }
261        return crsIdentifier;
262    }
263}