View Javadoc
1   package ca.uhn.fhir.util;
2   
3   import ca.uhn.fhir.model.primitive.IdDt;
4   import ca.uhn.fhir.rest.api.Constants;
5   import ca.uhn.fhir.rest.server.exceptions.InvalidRequestException;
6   import com.google.common.escape.Escaper;
7   import com.google.common.net.PercentEscaper;
8   
9   import java.io.UnsupportedEncodingException;
10  import java.net.MalformedURLException;
11  import java.net.URL;
12  import java.net.URLDecoder;
13  import java.util.*;
14  import java.util.Map.Entry;
15  
16  import static org.apache.commons.lang3.StringUtils.defaultIfBlank;
17  import static org.apache.commons.lang3.StringUtils.defaultString;
18  import static org.apache.commons.lang3.StringUtils.isBlank;
19  
20  /*
21   * #%L
22   * HAPI FHIR - Core Library
23   * %%
24   * Copyright (C) 2014 - 2018 University Health Network
25   * %%
26   * Licensed under the Apache License, Version 2.0 (the "License");
27   * you may not use this file except in compliance with the License.
28   * You may obtain a copy of the License at
29   * 
30   *      http://www.apache.org/licenses/LICENSE-2.0
31   * 
32   * Unless required by applicable law or agreed to in writing, software
33   * distributed under the License is distributed on an "AS IS" BASIS,
34   * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
35   * See the License for the specific language governing permissions and
36   * limitations under the License.
37   * #L%
38   */
39  
40  public class UrlUtil {
41  	private static final org.slf4j.Logger ourLog = org.slf4j.LoggerFactory.getLogger(UrlUtil.class);
42  
43  	private static final String URL_FORM_PARAMETER_OTHER_SAFE_CHARS = "-_.*";
44  	private static final Escaper PARAMETER_ESCAPER = new PercentEscaper(URL_FORM_PARAMETER_OTHER_SAFE_CHARS, false);
45  
46  
47  	/**
48  	 * Resolve a relative URL - THIS METHOD WILL NOT FAIL but will log a warning and return theEndpoint if the input is invalid.
49  	 */
50  	public static String constructAbsoluteUrl(String theBase, String theEndpoint) {
51  		if (theEndpoint == null) {
52  			return null;
53  		}
54  		if (isAbsolute(theEndpoint)) {
55  			return theEndpoint;
56  		}
57  		if (theBase == null) {
58  			return theEndpoint;
59  		}
60  
61  		try {
62  			return new URL(new URL(theBase), theEndpoint).toString();
63  		} catch (MalformedURLException e) {
64  			ourLog.warn("Failed to resolve relative URL[" + theEndpoint + "] against absolute base[" + theBase + "]", e);
65  			return theEndpoint;
66  		}
67  	}
68  
69  	public static String constructRelativeUrl(String theParentExtensionUrl, String theExtensionUrl) {
70  		if (theParentExtensionUrl == null) {
71  			return theExtensionUrl;
72  		}
73  		if (theExtensionUrl == null) {
74  			return null;
75  		}
76  
77  		int parentLastSlashIdx = theParentExtensionUrl.lastIndexOf('/');
78  		int childLastSlashIdx = theExtensionUrl.lastIndexOf('/');
79  
80  		if (parentLastSlashIdx == -1 || childLastSlashIdx == -1) {
81  			return theExtensionUrl;
82  		}
83  
84  		if (parentLastSlashIdx != childLastSlashIdx) {
85  			return theExtensionUrl;
86  		}
87  
88  		if (!theParentExtensionUrl.substring(0, parentLastSlashIdx).equals(theExtensionUrl.substring(0, parentLastSlashIdx))) {
89  			return theExtensionUrl;
90  		}
91  
92  		if (theExtensionUrl.length() > parentLastSlashIdx) {
93  			return theExtensionUrl.substring(parentLastSlashIdx + 1);
94  		}
95  
96  		return theExtensionUrl;
97  	}
98  
99  	/**
100 	 * URL encode a value according to RFC 3986
101 	 * <p>
102 	 * This method is intended to be applied to an individual parameter
103 	 * name or value. For example, if you are creating the URL
104 	 * <code>http://example.com/fhir/Patient?key=føø</code>;
105 	 * it would be appropriate to pass the string "føø" to this method,
106 	 * but not appropriate to pass the entire URL since characters
107 	 * such as "/" and "?" would also be escaped.
108 	 * </P>
109 	 */
110 	public static String escapeUrlParam(String theUnescaped) {
111 		if (theUnescaped == null) {
112 			return null;
113 		}
114 		return PARAMETER_ESCAPER.escape(theUnescaped);
115 	}
116 
117 
118 	public static boolean isAbsolute(String theValue) {
119 		String value = theValue.toLowerCase();
120 		return value.startsWith("http://") || value.startsWith("https://");
121 	}
122 
123 	public static boolean isNeedsSanitization(String theString) {
124 		if (theString != null) {
125 			for (int i = 0; i < theString.length(); i++) {
126 				char nextChar = theString.charAt(i);
127 				if (nextChar == '<' || nextChar == '"') {
128 					return true;
129 				}
130 			}
131 		}
132 		return false;
133 	}
134 
135 	public static boolean isValid(String theUrl) {
136 		if (theUrl == null || theUrl.length() < 8) {
137 			return false;
138 		}
139 
140 		String url = theUrl.toLowerCase();
141 		if (url.charAt(0) != 'h') {
142 			return false;
143 		}
144 		if (url.charAt(1) != 't') {
145 			return false;
146 		}
147 		if (url.charAt(2) != 't') {
148 			return false;
149 		}
150 		if (url.charAt(3) != 'p') {
151 			return false;
152 		}
153 		int slashOffset;
154 		if (url.charAt(4) == ':') {
155 			slashOffset = 5;
156 		} else if (url.charAt(4) == 's') {
157 			if (url.charAt(5) != ':') {
158 				return false;
159 			}
160 			slashOffset = 6;
161 		} else {
162 			return false;
163 		}
164 
165 		if (url.charAt(slashOffset) != '/') {
166 			return false;
167 		}
168 		if (url.charAt(slashOffset + 1) != '/') {
169 			return false;
170 		}
171 
172 		return true;
173 	}
174 
175 	public static void main(String[] args) {
176 		System.out.println(escapeUrlParam("http://snomed.info/sct?fhir_vs=isa/126851005"));
177 	}
178 
179 	public static Map<String, String[]> parseQueryString(String theQueryString) {
180 		HashMap<String, List<String>> map = new HashMap<>();
181 		parseQueryString(theQueryString, map);
182 		return toQueryStringMap(map);
183 	}
184 
185 	private static void parseQueryString(String theQueryString, HashMap<String, List<String>> map) {
186 		String query = defaultString(theQueryString);
187 		if (query.startsWith("?")) {
188 			query = query.substring(1);
189 		}
190 
191 
192 		StringTokenizer tok = new StringTokenizer(query, "&");
193 		while (tok.hasMoreTokens()) {
194 			String nextToken = tok.nextToken();
195 			if (isBlank(nextToken)) {
196 				continue;
197 			}
198 
199 			int equalsIndex = nextToken.indexOf('=');
200 			String nextValue;
201 			String nextKey;
202 			if (equalsIndex == -1) {
203 				nextKey = nextToken;
204 				nextValue = "";
205 			} else {
206 				nextKey = nextToken.substring(0, equalsIndex);
207 				nextValue = nextToken.substring(equalsIndex + 1);
208 			}
209 
210 			nextKey = unescape(nextKey);
211 			nextValue = unescape(nextValue);
212 
213 			List<String> list = map.computeIfAbsent(nextKey, k -> new ArrayList<>());
214 			list.add(nextValue);
215 		}
216 	}
217 
218 	public static Map<String, String[]> parseQueryStrings(String... theQueryString) {
219 		HashMap<String, List<String>> map = new HashMap<>();
220 		for (String next : theQueryString) {
221 			parseQueryString(next, map);
222 		}
223 		return toQueryStringMap(map);
224 	}
225 
226 	/**
227 	 * Parse a URL in one of the following forms:
228 	 * <ul>
229 	 * <li>[Resource Type]?[Search Params]
230 	 * <li>[Resource Type]/[Resource ID]
231 	 * <li>[Resource Type]/[Resource ID]/_history/[Version ID]
232 	 * </ul>
233 	 */
234 	public static UrlParts parseUrl(String theUrl) {
235 		String url = theUrl;
236 		UrlParts retVal = new UrlParts();
237 		if (url.startsWith("http")) {
238 			if (url.startsWith("/")) {
239 				url = url.substring(1);
240 			}
241 
242 			int qmIdx = url.indexOf('?');
243 			if (qmIdx != -1) {
244 				retVal.setParams(defaultIfBlank(url.substring(qmIdx + 1), null));
245 				url = url.substring(0, qmIdx);
246 			}
247 
248 			IdDt id = new IdDt(url);
249 			retVal.setResourceType(id.getResourceType());
250 			retVal.setResourceId(id.getIdPart());
251 			retVal.setVersionId(id.getVersionIdPart());
252 			return retVal;
253 		}
254 		if (url.matches("/[a-zA-Z]+\\?.*")) {
255 			url = url.substring(1);
256 		}
257 		int nextStart = 0;
258 		boolean nextIsHistory = false;
259 
260 		for (int idx = 0; idx < url.length(); idx++) {
261 			char nextChar = url.charAt(idx);
262 			boolean atEnd = (idx + 1) == url.length();
263 			if (nextChar == '?' || nextChar == '/' || atEnd) {
264 				int endIdx = (atEnd && nextChar != '?') ? idx + 1 : idx;
265 				String nextSubstring = url.substring(nextStart, endIdx);
266 				if (retVal.getResourceType() == null) {
267 					retVal.setResourceType(nextSubstring);
268 				} else if (retVal.getResourceId() == null) {
269 					retVal.setResourceId(nextSubstring);
270 				} else if (nextIsHistory) {
271 					retVal.setVersionId(nextSubstring);
272 				} else {
273 					if (nextSubstring.equals(Constants.URL_TOKEN_HISTORY)) {
274 						nextIsHistory = true;
275 					} else {
276 						throw new InvalidRequestException("Invalid FHIR resource URL: " + url);
277 					}
278 				}
279 				if (nextChar == '?') {
280 					if (url.length() > idx + 1) {
281 						retVal.setParams(url.substring(idx + 1, url.length()));
282 					}
283 					break;
284 				}
285 				nextStart = idx + 1;
286 			}
287 		}
288 
289 		return retVal;
290 
291 	}
292 
293 	/**
294 	 * This method specifically HTML-encodes the &quot; and
295 	 * &lt; characters in order to prevent injection attacks
296 	 */
297 	public static String sanitizeUrlPart(String theString) {
298 		if (theString == null) {
299 			return null;
300 		}
301 
302 		boolean needsSanitization = isNeedsSanitization(theString);
303 
304 		if (needsSanitization) {
305 			// Ok, we're sanitizing
306 			StringBuilder buffer = new StringBuilder(theString.length() + 10);
307 			for (int j = 0; j < theString.length(); j++) {
308 
309 				char nextChar = theString.charAt(j);
310 				switch (nextChar) {
311 					case '"':
312 						buffer.append("&quot;");
313 						break;
314 					case '<':
315 						buffer.append("&lt;");
316 						break;
317 					default:
318 						buffer.append(nextChar);
319 						break;
320 				}
321 
322 			} // for build escaped string
323 
324 			return buffer.toString();
325 		}
326 
327 		return theString;
328 	}
329 
330 	private static Map<String, String[]> toQueryStringMap(HashMap<String, List<String>> map) {
331 		HashMap<String, String[]> retVal = new HashMap<>();
332 		for (Entry<String, List<String>> nextEntry : map.entrySet()) {
333 			retVal.put(nextEntry.getKey(), nextEntry.getValue().toArray(new String[0]));
334 		}
335 		return retVal;
336 	}
337 
338 	public static String unescape(String theString) {
339 		if (theString == null) {
340 			return null;
341 		}
342 		for (int i = 0; i < theString.length(); i++) {
343 			char nextChar = theString.charAt(i);
344 			if (nextChar == '%' || nextChar == '+') {
345 				try {
346 					return URLDecoder.decode(theString, "UTF-8");
347 				} catch (UnsupportedEncodingException e) {
348 					throw new Error("UTF-8 not supported, this shouldn't happen", e);
349 				}
350 			}
351 		}
352 		return theString;
353 	}
354 
355 	public static class UrlParts {
356 		private String myParams;
357 		private String myResourceId;
358 		private String myResourceType;
359 		private String myVersionId;
360 
361 		public String getParams() {
362 			return myParams;
363 		}
364 
365 		public void setParams(String theParams) {
366 			myParams = theParams;
367 		}
368 
369 		public String getResourceId() {
370 			return myResourceId;
371 		}
372 
373 		public void setResourceId(String theResourceId) {
374 			myResourceId = theResourceId;
375 		}
376 
377 		public String getResourceType() {
378 			return myResourceType;
379 		}
380 
381 		public void setResourceType(String theResourceType) {
382 			myResourceType = theResourceType;
383 		}
384 
385 		public String getVersionId() {
386 			return myVersionId;
387 		}
388 
389 		public void setVersionId(String theVersionId) {
390 			myVersionId = theVersionId;
391 		}
392 	}
393 
394 }