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 * Moves reader to first occurrence of the structure equivalent of Xpath tags[0]/tags[1]../tags[n]. If fails to find 149 * moves the reader to the closing tag of current tag 150 * 151 * @param tags array of tags 152 * @param reader XMLStreamReader which should be moved 153 * @return true if tag was found, false otherwise 154 * @throws XMLStreamException See {@link XMLStreamReader} 155 */ 156 public static boolean moveReaderToTag(XMLStreamReader reader, QName... tags) throws XMLStreamException { 157 return moveReaderToTag(reader, QName::equals, tags); 158 } 159 160 /** 161 * Moves reader to first occurrence of the structure equivalent of Xpath tags[0]/tags[1]../tags[n]. If fails to find 162 * moves the reader to the closing tag of current tag 163 * 164 * @param tags array of tags 165 * @param reader XMLStreamReader which should be moved 166 * @param equalsFunc function to check equality of the tags 167 * @return true if tag was found, false otherwise 168 * @throws XMLStreamException See {@link XMLStreamReader} 169 */ 170 public static boolean moveReaderToTag(XMLStreamReader reader, 171 BiPredicate<QName, QName> equalsFunc, QName... tags) throws XMLStreamException { 172 QName stopTag = reader.getName(); 173 int currentLevel = 0; 174 QName searchTag = tags[currentLevel]; 175 QName parentTag = null; 176 QName skipTag = null; 177 178 for (int event = 0; //skip current element, so we will not skip it as a whole 179 reader.hasNext() && !(event == XMLStreamReader.END_ELEMENT && equalsFunc.test(stopTag, reader.getName())); 180 event = reader.next()) { 181 if (event == XMLStreamReader.END_ELEMENT && skipTag != null && equalsFunc.test(skipTag, reader.getName())) { 182 skipTag = null; 183 } 184 if (skipTag == null) { 185 if (event == XMLStreamReader.START_ELEMENT) { 186 if (equalsFunc.test(searchTag, reader.getName())) { 187 currentLevel += 1; 188 if (currentLevel >= tags.length) { 189 return true; // found! 190 } 191 parentTag = searchTag; 192 searchTag = tags[currentLevel]; 193 } else { 194 skipTag = reader.getName(); 195 } 196 } 197 198 if (event == XMLStreamReader.END_ELEMENT && parentTag != null && equalsFunc.test(parentTag, reader.getName())) { 199 currentLevel -= 1; 200 searchTag = parentTag; 201 if (currentLevel >= 0) { 202 parentTag = tags[currentLevel]; 203 } else { 204 parentTag = null; 205 } 206 } 207 } 208 } 209 return false; 210 } 211 212 /** 213 * Parses Operation[@name='GetTile']/DCP/HTTP/Get section. Returns when reader is on Get closing tag. 214 * @param reader StAX reader instance 215 * @return TransferMode coded in this section 216 * @throws XMLStreamException See {@link XMLStreamReader} 217 */ 218 public static TransferMode getTransferMode(XMLStreamReader reader) throws XMLStreamException { 219 QName getQname = QN_OWS_GET; 220 221 Utils.ensure(getQname.equals(reader.getName()), "WMTS Parser state invalid. Expected element %s, got %s", 222 getQname, reader.getName()); 223 for (int event = reader.getEventType(); 224 reader.hasNext() && !(event == XMLStreamReader.END_ELEMENT && getQname.equals(reader.getName())); 225 event = reader.next()) { 226 if (event == XMLStreamReader.START_ELEMENT && QN_OWS_CONSTRAINT.equals(reader.getName()) 227 && "GetEncoding".equals(reader.getAttributeValue("", "name"))) { 228 moveReaderToTag(reader, QN_OWS_ALLOWED_VALUES, QN_OWS_VALUE); 229 return TransferMode.fromString(reader.getElementText()); 230 } 231 } 232 return null; 233 } 234 235 /** 236 * Normalize url 237 * 238 * @param url URL 239 * @return normalized URL 240 * @throws MalformedURLException in case of malformed URL 241 * @since 10993 242 */ 243 public static String normalizeCapabilitiesUrl(String url) throws MalformedURLException { 244 URL inUrl = new URL(url); 245 URL ret = new URL(inUrl.getProtocol(), inUrl.getHost(), inUrl.getPort(), inUrl.getFile()); 246 return ret.toExternalForm(); 247 } 248 249 /** 250 * Convert CRS identifier to plain code 251 * @param crsIdentifier CRS identifier 252 * @return CRS Identifier as it is used within JOSM (without prefix) 253 * @see <a href="https://portal.opengeospatial.org/files/?artifact_id=24045"> 254 * Definition identifier URNs in OGC namespace, chapter 7.2: URNs for single objects</a> 255 */ 256 public static String crsToCode(String crsIdentifier) { 257 if (crsIdentifier.startsWith("urn:ogc:def:crs:")) { 258 return crsIdentifier.replaceFirst("urn:ogc:def:crs:([^:]*)(?::.*)?:(.*)$", "$1:$2").toUpperCase(Locale.ENGLISH); 259 } 260 return crsIdentifier; 261 } 262}