001package ca.uhn.fhir.util; 002 003import ca.uhn.fhir.model.primitive.IdDt; 004import ca.uhn.fhir.rest.api.Constants; 005import ca.uhn.fhir.rest.server.exceptions.InvalidRequestException; 006import com.google.common.escape.Escaper; 007import com.google.common.net.PercentEscaper; 008 009import java.io.UnsupportedEncodingException; 010import java.net.MalformedURLException; 011import java.net.URL; 012import java.net.URLDecoder; 013import java.util.*; 014import java.util.Map.Entry; 015 016import static org.apache.commons.lang3.StringUtils.defaultIfBlank; 017import static org.apache.commons.lang3.StringUtils.isBlank; 018 019/* 020 * #%L 021 * HAPI FHIR - Core Library 022 * %% 023 * Copyright (C) 2014 - 2018 University Health Network 024 * %% 025 * Licensed under the Apache License, Version 2.0 (the "License"); 026 * you may not use this file except in compliance with the License. 027 * You may obtain a copy of the License at 028 * 029 * http://www.apache.org/licenses/LICENSE-2.0 030 * 031 * Unless required by applicable law or agreed to in writing, software 032 * distributed under the License is distributed on an "AS IS" BASIS, 033 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 034 * See the License for the specific language governing permissions and 035 * limitations under the License. 036 * #L% 037 */ 038 039public class UrlUtil { 040 private static final org.slf4j.Logger ourLog = org.slf4j.LoggerFactory.getLogger(UrlUtil.class); 041 042 private static final String URL_FORM_PARAMETER_OTHER_SAFE_CHARS = "-_.*"; 043 private static final Escaper PARAMETER_ESCAPER = new PercentEscaper(URL_FORM_PARAMETER_OTHER_SAFE_CHARS, false); 044 045 046 /** 047 * Resolve a relative URL - THIS METHOD WILL NOT FAIL but will log a warning and return theEndpoint if the input is invalid. 048 */ 049 public static String constructAbsoluteUrl(String theBase, String theEndpoint) { 050 if (theEndpoint == null) { 051 return null; 052 } 053 if (isAbsolute(theEndpoint)) { 054 return theEndpoint; 055 } 056 if (theBase == null) { 057 return theEndpoint; 058 } 059 060 try { 061 return new URL(new URL(theBase), theEndpoint).toString(); 062 } catch (MalformedURLException e) { 063 ourLog.warn("Failed to resolve relative URL[" + theEndpoint + "] against absolute base[" + theBase + "]", e); 064 return theEndpoint; 065 } 066 } 067 068 public static String constructRelativeUrl(String theParentExtensionUrl, String theExtensionUrl) { 069 if (theParentExtensionUrl == null) { 070 return theExtensionUrl; 071 } 072 if (theExtensionUrl == null) { 073 return theExtensionUrl; 074 } 075 076 int parentLastSlashIdx = theParentExtensionUrl.lastIndexOf('/'); 077 int childLastSlashIdx = theExtensionUrl.lastIndexOf('/'); 078 079 if (parentLastSlashIdx == -1 || childLastSlashIdx == -1) { 080 return theExtensionUrl; 081 } 082 083 if (parentLastSlashIdx != childLastSlashIdx) { 084 return theExtensionUrl; 085 } 086 087 if (!theParentExtensionUrl.substring(0, parentLastSlashIdx).equals(theExtensionUrl.substring(0, parentLastSlashIdx))) { 088 return theExtensionUrl; 089 } 090 091 if (theExtensionUrl.length() > parentLastSlashIdx) { 092 return theExtensionUrl.substring(parentLastSlashIdx + 1); 093 } 094 095 return theExtensionUrl; 096 } 097 098 /** 099 * URL encode a value according to RFC 3986 100 * <p> 101 * This method is intended to be applied to an individual parameter 102 * name or value. For example, if you are creating the URL 103 * <code>http://example.com/fhir/Patient?key=føø</code> 104 * it would be appropriate to pass the string "føø" to this method, 105 * but not appropriate to pass the entire URL since characters 106 * such as "/" and "?" would also be escaped. 107 * </P> 108 */ 109 public static String escapeUrlParam(String theUnescaped) { 110 if (theUnescaped == null) { 111 return null; 112 } 113 return PARAMETER_ESCAPER.escape(theUnescaped); 114 } 115 116 117 public static boolean isAbsolute(String theValue) { 118 String value = theValue.toLowerCase(); 119 return value.startsWith("http://") || value.startsWith("https://"); 120 } 121 122 public static boolean isValid(String theUrl) { 123 if (theUrl == null || theUrl.length() < 8) { 124 return false; 125 } 126 127 String url = theUrl.toLowerCase(); 128 if (url.charAt(0) != 'h') { 129 return false; 130 } 131 if (url.charAt(1) != 't') { 132 return false; 133 } 134 if (url.charAt(2) != 't') { 135 return false; 136 } 137 if (url.charAt(3) != 'p') { 138 return false; 139 } 140 int slashOffset; 141 if (url.charAt(4) == ':') { 142 slashOffset = 5; 143 } else if (url.charAt(4) == 's') { 144 if (url.charAt(5) != ':') { 145 return false; 146 } 147 slashOffset = 6; 148 } else { 149 return false; 150 } 151 152 if (url.charAt(slashOffset) != '/') { 153 return false; 154 } 155 if (url.charAt(slashOffset + 1) != '/') { 156 return false; 157 } 158 159 return true; 160 } 161 162 public static void main(String[] args) { 163 System.out.println(escapeUrlParam("http://snomed.info/sct?fhir_vs=isa/126851005")); 164 } 165 166 public static Map<String, String[]> parseQueryString(String theQueryString) { 167 HashMap<String, List<String>> map = new HashMap<String, List<String>>(); 168 parseQueryString(theQueryString, map); 169 return toQueryStringMap(map); 170 } 171 172 private static void parseQueryString(String theQueryString, HashMap<String, List<String>> map) { 173 String query = theQueryString; 174 if (query.startsWith("?")) { 175 query = query.substring(1); 176 } 177 178 179 StringTokenizer tok = new StringTokenizer(query, "&"); 180 while (tok.hasMoreTokens()) { 181 String nextToken = tok.nextToken(); 182 if (isBlank(nextToken)) { 183 continue; 184 } 185 186 int equalsIndex = nextToken.indexOf('='); 187 String nextValue; 188 String nextKey; 189 if (equalsIndex == -1) { 190 nextKey = nextToken; 191 nextValue = ""; 192 } else { 193 nextKey = nextToken.substring(0, equalsIndex); 194 nextValue = nextToken.substring(equalsIndex + 1); 195 } 196 197 nextKey = unescape(nextKey); 198 nextValue = unescape(nextValue); 199 200 List<String> list = map.get(nextKey); 201 if (list == null) { 202 list = new ArrayList<>(); 203 map.put(nextKey, list); 204 } 205 list.add(nextValue); 206 } 207 } 208 209 public static Map<String, String[]> parseQueryStrings(String... theQueryString) { 210 HashMap<String, List<String>> map = new HashMap<String, List<String>>(); 211 for (String next : theQueryString) { 212 parseQueryString(next, map); 213 } 214 return toQueryStringMap(map); 215 } 216 217 /** 218 * Parse a URL in one of the following forms: 219 * <ul> 220 * <li>[Resource Type]?[Search Params] 221 * <li>[Resource Type]/[Resource ID] 222 * <li>[Resource Type]/[Resource ID]/_history/[Version ID] 223 * </ul> 224 */ 225 //@formatter:on 226 public static UrlParts parseUrl(String theUrl) { 227 String url = theUrl; 228 UrlParts retVal = new UrlParts(); 229 if (url.startsWith("http")) { 230 if (url.startsWith("/")) { 231 url = url.substring(1); 232 } 233 234 int qmIdx = url.indexOf('?'); 235 if (qmIdx != -1) { 236 retVal.setParams(defaultIfBlank(url.substring(qmIdx + 1), null)); 237 url = url.substring(0, qmIdx); 238 } 239 240 IdDt id = new IdDt(url); 241 retVal.setResourceType(id.getResourceType()); 242 retVal.setResourceId(id.getIdPart()); 243 retVal.setVersionId(id.getVersionIdPart()); 244 return retVal; 245 } 246 if (url.matches("\\/[a-zA-Z]+\\?.*")) { 247 url = url.substring(1); 248 } 249 int nextStart = 0; 250 boolean nextIsHistory = false; 251 252 for (int idx = 0; idx < url.length(); idx++) { 253 char nextChar = url.charAt(idx); 254 boolean atEnd = (idx + 1) == url.length(); 255 if (nextChar == '?' || nextChar == '/' || atEnd) { 256 int endIdx = (atEnd && nextChar != '?') ? idx + 1 : idx; 257 String nextSubstring = url.substring(nextStart, endIdx); 258 if (retVal.getResourceType() == null) { 259 retVal.setResourceType(nextSubstring); 260 } else if (retVal.getResourceId() == null) { 261 retVal.setResourceId(nextSubstring); 262 } else if (nextIsHistory) { 263 retVal.setVersionId(nextSubstring); 264 } else { 265 if (nextSubstring.equals(Constants.URL_TOKEN_HISTORY)) { 266 nextIsHistory = true; 267 } else { 268 throw new InvalidRequestException("Invalid FHIR resource URL: " + url); 269 } 270 } 271 if (nextChar == '?') { 272 if (url.length() > idx + 1) { 273 retVal.setParams(url.substring(idx + 1, url.length())); 274 } 275 break; 276 } 277 nextStart = idx + 1; 278 } 279 } 280 281 return retVal; 282 283 } 284 285 //@formatter:off 286 287 private static Map<String, String[]> toQueryStringMap(HashMap<String, List<String>> map) { 288 HashMap<String, String[]> retVal = new HashMap<String, String[]>(); 289 for (Entry<String, List<String>> nextEntry : map.entrySet()) { 290 retVal.put(nextEntry.getKey(), nextEntry.getValue().toArray(new String[nextEntry.getValue().size()])); 291 } 292 return retVal; 293 } 294 295 public static String unescape(String theString) { 296 if (theString == null) { 297 return null; 298 } 299 for (int i = 0; i < theString.length(); i++) { 300 char nextChar = theString.charAt(i); 301 if (nextChar == '%' || nextChar == '+') { 302 try { 303 return URLDecoder.decode(theString, "UTF-8"); 304 } catch (UnsupportedEncodingException e) { 305 throw new Error("UTF-8 not supported, this shouldn't happen", e); 306 } 307 } 308 } 309 return theString; 310 } 311 312 public static class UrlParts { 313 private String myParams; 314 private String myResourceId; 315 private String myResourceType; 316 private String myVersionId; 317 318 public String getParams() { 319 return myParams; 320 } 321 322 public void setParams(String theParams) { 323 myParams = theParams; 324 } 325 326 public String getResourceId() { 327 return myResourceId; 328 } 329 330 public void setResourceId(String theResourceId) { 331 myResourceId = theResourceId; 332 } 333 334 public String getResourceType() { 335 return myResourceType; 336 } 337 338 public void setResourceType(String theResourceType) { 339 myResourceType = theResourceType; 340 } 341 342 public String getVersionId() { 343 return myVersionId; 344 } 345 346 public void setVersionId(String theVersionId) { 347 myVersionId = theVersionId; 348 } 349 } 350 351}