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.Arrays;
008import java.util.Locale;
009import java.util.function.BiPredicate;
010
011import javax.xml.namespace.QName;
012import javax.xml.stream.XMLStreamException;
013import javax.xml.stream.XMLStreamReader;
014
015import org.openstreetmap.josm.tools.Utils;
016import org.openstreetmap.josm.tools.XmlUtils;
017
018/**
019 * Helper class for handling OGC GetCapabilities documents
020 * @since 10993
021 */
022public final class GetCapabilitiesParseHelper {
023    enum TransferMode {
024        KVP("KVP"),
025        REST("RESTful");
026
027        private final String typeString;
028
029        TransferMode(String urlString) {
030            this.typeString = urlString;
031        }
032
033        private String getTypeString() {
034            return typeString;
035        }
036
037        static TransferMode fromString(String s) {
038            return Arrays.stream(TransferMode.values())
039                    .filter(type -> type.getTypeString().equals(s))
040                    .findFirst().orElse(null);
041        }
042    }
043
044    /**
045     * OWS namespace address
046     */
047    public static final String OWS_NS_URL = "http://www.opengis.net/ows/1.1";
048    /**
049     * XML xlink namespace address
050     */
051    public static final String XLINK_NS_URL = "http://www.w3.org/1999/xlink";
052
053    /**
054     * QNames in OWS namespace
055     */
056    // CHECKSTYLE.OFF: SingleSpaceSeparator
057    static final QName QN_OWS_ALLOWED_VALUES      = new QName(OWS_NS_URL, "AllowedValues");
058    static final QName QN_OWS_CONSTRAINT          = new QName(OWS_NS_URL, "Constraint");
059    static final QName QN_OWS_DCP                 = new QName(OWS_NS_URL, "DCP");
060    static final QName QN_OWS_GET                 = new QName(OWS_NS_URL, "Get");
061    static final QName QN_OWS_HTTP                = new QName(OWS_NS_URL, "HTTP");
062    static final QName QN_OWS_IDENTIFIER          = new QName(OWS_NS_URL, "Identifier");
063    static final QName QN_OWS_LOWER_CORNER        = new QName(OWS_NS_URL, "LowerCorner");
064    static final QName QN_OWS_OPERATION           = new QName(OWS_NS_URL, "Operation");
065    static final QName QN_OWS_OPERATIONS_METADATA = new QName(OWS_NS_URL, "OperationsMetadata");
066    static final QName QN_OWS_SUPPORTED_CRS       = new QName(OWS_NS_URL, "SupportedCRS");
067    static final QName QN_OWS_TITLE               = new QName(OWS_NS_URL, "Title");
068    static final QName QN_OWS_UPPER_CORNER        = new QName(OWS_NS_URL, "UpperCorner");
069    static final QName QN_OWS_VALUE               = new QName(OWS_NS_URL, "Value");
070    static final QName QN_OWS_WGS84_BOUNDING_BOX  = new QName(OWS_NS_URL, "WGS84BoundingBox");
071    // CHECKSTYLE.ON: SingleSpaceSeparator
072
073    private GetCapabilitiesParseHelper() {
074        // Hide default constructor for utilities classes
075    }
076
077    /**
078     * Returns reader with properties set for parsing WM(T)S documents
079     *
080     * @param in InputStream with pointing to GetCapabilities XML stream
081     * @return safe XMLStreamReader, that is not validating external entities, nor loads DTD's
082     * @throws XMLStreamException if any XML stream error occurs
083     */
084    public static XMLStreamReader getReader(InputStream in) throws XMLStreamException {
085        return XmlUtils.newSafeXMLInputFactory().createXMLStreamReader(in);
086    }
087
088    /**
089     * Moves the reader to the closing tag of current tag.
090     * @param reader XMLStreamReader which should be moved
091     * @throws XMLStreamException when parse exception occurs
092     */
093    public static void moveReaderToEndCurrentTag(XMLStreamReader reader) throws XMLStreamException {
094        int level = 0;
095        QName tag = reader.getName();
096        for (int event = reader.getEventType(); reader.hasNext(); event = reader.next()) {
097            if (XMLStreamReader.START_ELEMENT == event) {
098                level += 1;
099            } else if (XMLStreamReader.END_ELEMENT == event) {
100                level -= 1;
101                if (level == 0 && tag.equals(reader.getName())) {
102                    return;
103                }
104            }
105            if (level < 0) {
106                throw new IllegalStateException("WMTS Parser error - moveReaderToEndCurrentTag failed to find closing tag");
107            }
108        }
109        throw new IllegalStateException("WMTS Parser error - moveReaderToEndCurrentTag failed to find closing tag");
110    }
111
112    /**
113     * Returns whole content of the element that reader is pointing at, including other XML elements within (with their tags).
114     *
115     * @param reader XMLStreamReader that should point to start of element
116     * @return content of current tag
117     * @throws XMLStreamException if any XML stream error occurs
118     */
119    public static String getElementTextWithSubtags(XMLStreamReader reader) throws XMLStreamException {
120        StringBuilder ret = new StringBuilder();
121        int level = 0;
122        QName tag = reader.getName();
123        for (int event = reader.getEventType(); reader.hasNext(); event = reader.next()) {
124            if (XMLStreamReader.START_ELEMENT == event) {
125                if (level > 0) {
126                    ret.append('<').append(reader.getLocalName()).append('>');
127                }
128                level += 1;
129            } else if (XMLStreamReader.END_ELEMENT == event) {
130                level -= 1;
131                if (level == 0 && tag.equals(reader.getName())) {
132                    return ret.toString();
133                }
134                ret.append("</").append(reader.getLocalName()).append('>');
135            } else if (XMLStreamReader.CHARACTERS == event) {
136                ret.append(reader.getText());
137            }
138            if (level < 0) {
139                throw new IllegalStateException("WMTS Parser error - moveReaderToEndCurrentTag failed to find closing tag");
140            }
141        }
142        throw new IllegalStateException("WMTS Parser error - moveReaderToEndCurrentTag failed to find closing tag");
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}