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;
008
009import javax.xml.namespace.QName;
010import javax.xml.stream.XMLInputFactory;
011import javax.xml.stream.XMLStreamException;
012import javax.xml.stream.XMLStreamReader;
013
014import org.openstreetmap.josm.tools.Utils;
015
016/**
017 * Helper class for handling OGC GetCapabilities documents
018 * @since 10993
019 */
020public final class GetCapabilitiesParseHelper {
021    enum TransferMode {
022        KVP("KVP"),
023        REST("RESTful");
024
025        private final String typeString;
026
027        TransferMode(String urlString) {
028            this.typeString = urlString;
029        }
030
031        private String getTypeString() {
032            return typeString;
033        }
034
035        static TransferMode fromString(String s) {
036            for (TransferMode type : TransferMode.values()) {
037                if (type.getTypeString().equals(s)) {
038                    return type;
039                }
040            }
041            return null;
042        }
043    }
044
045    /**
046     * OWS namespace address
047     */
048    public static final String OWS_NS_URL = "http://www.opengis.net/ows/1.1";
049    /**
050     * XML xlink namespace address
051     */
052    public static final String XLINK_NS_URL = "http://www.w3.org/1999/xlink";
053
054    /**
055     * QNames in OWS namespace
056     */
057    // CHECKSTYLE.OFF: SingleSpaceSeparator
058    static final QName QN_OWS_ALLOWED_VALUES      = new QName(OWS_NS_URL, "AllowedValues");
059    static final QName QN_OWS_CONSTRAINT          = new QName(OWS_NS_URL, "Constraint");
060    static final QName QN_OWS_DCP                 = new QName(OWS_NS_URL, "DCP");
061    static final QName QN_OWS_GET                 = new QName(OWS_NS_URL, "Get");
062    static final QName QN_OWS_HTTP                = new QName(OWS_NS_URL, "HTTP");
063    static final QName QN_OWS_IDENTIFIER          = new QName(OWS_NS_URL, "Identifier");
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_VALUE               = new QName(OWS_NS_URL, "Value");
069    // CHECKSTYLE.ON: SingleSpaceSeparator
070
071    private GetCapabilitiesParseHelper() {
072        // Hide default constructor for utilities classes
073    }
074
075    /**
076     * @param in InputStream with pointing to GetCapabilities XML stream
077     * @return safe XMLStreamReader, that is not validating external entities, nor loads DTD's
078     * @throws XMLStreamException if any XML stream error occurs
079     */
080    public static XMLStreamReader getReader(InputStream in) throws XMLStreamException {
081        XMLInputFactory factory = XMLInputFactory.newInstance();
082        // do not try to load external entities, nor validate the XML
083        factory.setProperty(XMLInputFactory.IS_SUPPORTING_EXTERNAL_ENTITIES, Boolean.FALSE);
084        factory.setProperty(XMLInputFactory.IS_VALIDATING, Boolean.FALSE);
085        factory.setProperty(XMLInputFactory.SUPPORT_DTD, Boolean.FALSE);
086        return factory.createXMLStreamReader(in);
087    }
088
089    /**
090     * Moves the reader to the closing tag of current tag.
091     * @param reader XMLStreamReader which should be moved
092     * @throws XMLStreamException when parse exception occurs
093     */
094    public static void moveReaderToEndCurrentTag(XMLStreamReader reader) throws XMLStreamException {
095        int level = 0;
096        QName tag = reader.getName();
097        for (int event = reader.getEventType(); reader.hasNext(); event = reader.next()) {
098            if (XMLStreamReader.START_ELEMENT == event) {
099                level += 1;
100            } else if (XMLStreamReader.END_ELEMENT == event) {
101                level -= 1;
102                if (level == 0 && tag.equals(reader.getName())) {
103                    return;
104                }
105            }
106            if (level < 0) {
107                throw new IllegalStateException("WMTS Parser error - moveReaderToEndCurrentTag failed to find closing tag");
108            }
109        }
110        throw new IllegalStateException("WMTS Parser error - moveReaderToEndCurrentTag failed to find closing tag");
111    }
112
113    /**
114     * Moves reader to first occurrence of the structure equivalent of Xpath tags[0]/tags[1]../tags[n]. If fails to find
115     * moves the reader to the closing tag of current tag
116     *
117     * @param tags array of tags
118     * @param reader XMLStreamReader which should be moved
119     * @return true if tag was found, false otherwise
120     * @throws XMLStreamException See {@link XMLStreamReader}
121     */
122    public static boolean moveReaderToTag(XMLStreamReader reader, QName... tags) throws XMLStreamException {
123        QName stopTag = reader.getName();
124        int currentLevel = 0;
125        QName searchTag = tags[currentLevel];
126        QName parentTag = null;
127        QName skipTag = null;
128
129        for (int event = 0; //skip current element, so we will not skip it as a whole
130                reader.hasNext() && !(event == XMLStreamReader.END_ELEMENT && stopTag.equals(reader.getName()));
131                event = reader.next()) {
132            if (event == XMLStreamReader.END_ELEMENT && skipTag != null && skipTag.equals(reader.getName())) {
133                skipTag = null;
134            }
135            if (skipTag == null) {
136                if (event == XMLStreamReader.START_ELEMENT) {
137                    if (searchTag.equals(reader.getName())) {
138                        currentLevel += 1;
139                        if (currentLevel >= tags.length) {
140                            return true; // found!
141                        }
142                        parentTag = searchTag;
143                        searchTag = tags[currentLevel];
144                    } else {
145                        skipTag = reader.getName();
146                    }
147                }
148
149                if (event == XMLStreamReader.END_ELEMENT && parentTag != null && parentTag.equals(reader.getName())) {
150                    currentLevel -= 1;
151                    searchTag = parentTag;
152                    if (currentLevel >= 0) {
153                        parentTag = tags[currentLevel];
154                    } else {
155                        parentTag = null;
156                    }
157                }
158            }
159        }
160        return false;
161    }
162
163    /**
164     * Parses Operation[@name='GetTile']/DCP/HTTP/Get section. Returns when reader is on Get closing tag.
165     * @param reader StAX reader instance
166     * @return TransferMode coded in this section
167     * @throws XMLStreamException See {@link XMLStreamReader}
168     */
169    public static TransferMode getTransferMode(XMLStreamReader reader) throws XMLStreamException {
170        QName getQname = QN_OWS_GET;
171
172        Utils.ensure(getQname.equals(reader.getName()), "WMTS Parser state invalid. Expected element %s, got %s",
173                getQname, reader.getName());
174        for (int event = reader.getEventType();
175                reader.hasNext() && !(event == XMLStreamReader.END_ELEMENT && getQname.equals(reader.getName()));
176                event = reader.next()) {
177            if (event == XMLStreamReader.START_ELEMENT && QN_OWS_CONSTRAINT.equals(reader.getName())
178             && "GetEncoding".equals(reader.getAttributeValue("", "name"))) {
179                moveReaderToTag(reader, QN_OWS_ALLOWED_VALUES, QN_OWS_VALUE);
180                return TransferMode.fromString(reader.getElementText());
181            }
182        }
183        return null;
184    }
185
186    /**
187     * @param url URL
188     * @return normalized URL
189     * @throws MalformedURLException in case of malformed URL
190     * @since 10993
191     */
192    public static String normalizeCapabilitiesUrl(String url) throws MalformedURLException {
193        URL inUrl = new URL(url);
194        URL ret = new URL(inUrl.getProtocol(), inUrl.getHost(), inUrl.getPort(), inUrl.getFile());
195        return ret.toExternalForm();
196    }
197
198    /**
199     * Convert CRS identifier to plain code
200     * @param crsIdentifier CRS identifier
201     * @return CRS Identifier as it is used within JOSM (without prefix)
202     * @see <a href="https://portal.opengeospatial.org/files/?artifact_id=24045">
203     *     Definition identifier URNs in OGC namespace, chapter 7.2: URNs for single objects</a>
204     */
205    public static String crsToCode(String crsIdentifier) {
206        if (crsIdentifier.startsWith("urn:ogc:def:crs:")) {
207            return crsIdentifier.replaceFirst("urn:ogc:def:crs:([^:]*)(?::.*)?:(.*)$", "$1:$2").toUpperCase(Locale.ENGLISH);
208        }
209        return crsIdentifier;
210    }
211
212}