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_OPERATION           = new QName(OWS_NS_URL, "Operation");
066    static final QName QN_OWS_OPERATIONS_METADATA = new QName(OWS_NS_URL, "OperationsMetadata");
067    static final QName QN_OWS_SUPPORTED_CRS       = new QName(OWS_NS_URL, "SupportedCRS");
068    static final QName QN_OWS_TITLE               = new QName(OWS_NS_URL, "Title");
069    static final QName QN_OWS_VALUE               = new QName(OWS_NS_URL, "Value");
070    // CHECKSTYLE.ON: SingleSpaceSeparator
071
072    private GetCapabilitiesParseHelper() {
073        // Hide default constructor for utilities classes
074    }
075
076    /**
077     * Returns reader with properties set for parsing WM(T)S documents
078     *
079     * @param in InputStream with pointing to GetCapabilities XML stream
080     * @return safe XMLStreamReader, that is not validating external entities, nor loads DTD's
081     * @throws XMLStreamException if any XML stream error occurs
082     */
083    public static XMLStreamReader getReader(InputStream in) throws XMLStreamException {
084        return XmlUtils.newSafeXMLInputFactory().createXMLStreamReader(in);
085    }
086
087    /**
088     * Moves the reader to the closing tag of current tag.
089     * @param reader XMLStreamReader which should be moved
090     * @throws XMLStreamException when parse exception occurs
091     */
092    public static void moveReaderToEndCurrentTag(XMLStreamReader reader) throws XMLStreamException {
093        int level = 0;
094        QName tag = reader.getName();
095        for (int event = reader.getEventType(); reader.hasNext(); event = reader.next()) {
096            if (XMLStreamReader.START_ELEMENT == event) {
097                level += 1;
098            } else if (XMLStreamReader.END_ELEMENT == event) {
099                level -= 1;
100                if (level == 0 && tag.equals(reader.getName())) {
101                    return;
102                }
103            }
104            if (level < 0) {
105                throw new IllegalStateException("WMTS Parser error - moveReaderToEndCurrentTag failed to find closing tag");
106            }
107        }
108        throw new IllegalStateException("WMTS Parser error - moveReaderToEndCurrentTag failed to find closing tag");
109    }
110
111    /**
112     * Returns whole content of the element that reader is pointing at, including other XML elements within (with their tags).
113     *
114     * @param reader XMLStreamReader that should point to start of element
115     * @return content of current tag
116     * @throws XMLStreamException if any XML stream error occurs
117     */
118    public static String getElementTextWithSubtags(XMLStreamReader reader) throws XMLStreamException {
119        StringBuilder ret = new StringBuilder();
120        int level = 0;
121        QName tag = reader.getName();
122        for (int event = reader.getEventType(); reader.hasNext(); event = reader.next()) {
123            if (XMLStreamReader.START_ELEMENT == event) {
124                if (level > 0) {
125                    ret.append('<').append(reader.getLocalName()).append('>');
126                }
127                level += 1;
128            } else if (XMLStreamReader.END_ELEMENT == event) {
129                level -= 1;
130                if (level == 0 && tag.equals(reader.getName())) {
131                    return ret.toString();
132                }
133                ret.append("</").append(reader.getLocalName()).append('>');
134            } else if (XMLStreamReader.CHARACTERS == event) {
135                ret.append(reader.getText());
136            }
137            if (level < 0) {
138                throw new IllegalStateException("WMTS Parser error - moveReaderToEndCurrentTag failed to find closing tag");
139            }
140        }
141        throw new IllegalStateException("WMTS Parser error - moveReaderToEndCurrentTag failed to find closing tag");
142    }
143
144
145    /**
146     * Moves reader to first occurrence of the structure equivalent of Xpath tags[0]/tags[1]../tags[n]. If fails to find
147     * moves the reader to the closing tag of current tag
148     *
149     * @param tags array of tags
150     * @param reader XMLStreamReader which should be moved
151     * @return true if tag was found, false otherwise
152     * @throws XMLStreamException See {@link XMLStreamReader}
153     */
154    public static boolean moveReaderToTag(XMLStreamReader reader, QName... tags) throws XMLStreamException {
155        return moveReaderToTag(reader, QName::equals, tags);
156    }
157
158    /**
159     * Moves reader to first occurrence of the structure equivalent of Xpath tags[0]/tags[1]../tags[n]. If fails to find
160     * moves the reader to the closing tag of current tag
161     *
162     * @param tags array of tags
163     * @param reader XMLStreamReader which should be moved
164     * @param equalsFunc function to check equality of the tags
165     * @return true if tag was found, false otherwise
166     * @throws XMLStreamException See {@link XMLStreamReader}
167     */
168    public static boolean moveReaderToTag(XMLStreamReader reader,
169            BiPredicate<QName, QName> equalsFunc, QName... tags) throws XMLStreamException {
170        QName stopTag = reader.getName();
171        int currentLevel = 0;
172        QName searchTag = tags[currentLevel];
173        QName parentTag = null;
174        QName skipTag = null;
175
176        for (int event = 0; //skip current element, so we will not skip it as a whole
177                reader.hasNext() && !(event == XMLStreamReader.END_ELEMENT && equalsFunc.test(stopTag, reader.getName()));
178                event = reader.next()) {
179            if (event == XMLStreamReader.END_ELEMENT && skipTag != null && equalsFunc.test(skipTag, reader.getName())) {
180                skipTag = null;
181            }
182            if (skipTag == null) {
183                if (event == XMLStreamReader.START_ELEMENT) {
184                    if (equalsFunc.test(searchTag, reader.getName())) {
185                        currentLevel += 1;
186                        if (currentLevel >= tags.length) {
187                            return true; // found!
188                        }
189                        parentTag = searchTag;
190                        searchTag = tags[currentLevel];
191                    } else {
192                        skipTag = reader.getName();
193                    }
194                }
195
196                if (event == XMLStreamReader.END_ELEMENT && parentTag != null && equalsFunc.test(parentTag, reader.getName())) {
197                    currentLevel -= 1;
198                    searchTag = parentTag;
199                    if (currentLevel >= 0) {
200                        parentTag = tags[currentLevel];
201                    } else {
202                        parentTag = null;
203                    }
204                }
205            }
206        }
207        return false;
208    }
209
210    /**
211     * Parses Operation[@name='GetTile']/DCP/HTTP/Get section. Returns when reader is on Get closing tag.
212     * @param reader StAX reader instance
213     * @return TransferMode coded in this section
214     * @throws XMLStreamException See {@link XMLStreamReader}
215     */
216    public static TransferMode getTransferMode(XMLStreamReader reader) throws XMLStreamException {
217        QName getQname = QN_OWS_GET;
218
219        Utils.ensure(getQname.equals(reader.getName()), "WMTS Parser state invalid. Expected element %s, got %s",
220                getQname, reader.getName());
221        for (int event = reader.getEventType();
222                reader.hasNext() && !(event == XMLStreamReader.END_ELEMENT && getQname.equals(reader.getName()));
223                event = reader.next()) {
224            if (event == XMLStreamReader.START_ELEMENT && QN_OWS_CONSTRAINT.equals(reader.getName())
225             && "GetEncoding".equals(reader.getAttributeValue("", "name"))) {
226                moveReaderToTag(reader, QN_OWS_ALLOWED_VALUES, QN_OWS_VALUE);
227                return TransferMode.fromString(reader.getElementText());
228            }
229        }
230        return null;
231    }
232
233    /**
234     * Normalize url
235     *
236     * @param url URL
237     * @return normalized URL
238     * @throws MalformedURLException in case of malformed URL
239     * @since 10993
240     */
241    public static String normalizeCapabilitiesUrl(String url) throws MalformedURLException {
242        URL inUrl = new URL(url);
243        URL ret = new URL(inUrl.getProtocol(), inUrl.getHost(), inUrl.getPort(), inUrl.getFile());
244        return ret.toExternalForm();
245    }
246
247    /**
248     * Convert CRS identifier to plain code
249     * @param crsIdentifier CRS identifier
250     * @return CRS Identifier as it is used within JOSM (without prefix)
251     * @see <a href="https://portal.opengeospatial.org/files/?artifact_id=24045">
252     *     Definition identifier URNs in OGC namespace, chapter 7.2: URNs for single objects</a>
253     */
254    public static String crsToCode(String crsIdentifier) {
255        if (crsIdentifier.startsWith("urn:ogc:def:crs:")) {
256            return crsIdentifier.replaceFirst("urn:ogc:def:crs:([^:]*)(?::.*)?:(.*)$", "$1:$2").toUpperCase(Locale.ENGLISH);
257        }
258        return crsIdentifier;
259    }
260}