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