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.BiFunction; 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 BiFunction<QName, QName, Boolean> 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.apply(stopTag, reader.getName())); 178 event = reader.next()) { 179 if (event == XMLStreamReader.END_ELEMENT && skipTag != null && equalsFunc.apply(skipTag, reader.getName())) { 180 skipTag = null; 181 } 182 if (skipTag == null) { 183 if (event == XMLStreamReader.START_ELEMENT) { 184 if (equalsFunc.apply(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.apply(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}