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}