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.isBlank;
18  
19  /*
20   * #%L
21   * HAPI FHIR - Core Library
22   * %%
23   * Copyright (C) 2014 - 2018 University Health Network
24   * %%
25   * Licensed under the Apache License, Version 2.0 (the "License");
26   * you may not use this file except in compliance with the License.
27   * You may obtain a copy of the License at
28   * 
29   *      http://www.apache.org/licenses/LICENSE-2.0
30   * 
31   * Unless required by applicable law or agreed to in writing, software
32   * distributed under the License is distributed on an "AS IS" BASIS,
33   * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
34   * See the License for the specific language governing permissions and
35   * limitations under the License.
36   * #L%
37   */
38  
39  public class UrlUtil {
40  	private static final org.slf4j.Logger ourLog = org.slf4j.LoggerFactory.getLogger(UrlUtil.class);
41  
42  	private static final String URL_FORM_PARAMETER_OTHER_SAFE_CHARS = "-_.*";
43  	private static final Escaper PARAMETER_ESCAPER = new PercentEscaper(URL_FORM_PARAMETER_OTHER_SAFE_CHARS, false);
44  
45  
46  	/**
47  	 * Resolve a relative URL - THIS METHOD WILL NOT FAIL but will log a warning and return theEndpoint if the input is invalid.
48  	 */
49  	public static String constructAbsoluteUrl(String theBase, String theEndpoint) {
50  		if (theEndpoint == null) {
51  			return null;
52  		}
53  		if (isAbsolute(theEndpoint)) {
54  			return theEndpoint;
55  		}
56  		if (theBase == null) {
57  			return theEndpoint;
58  		}
59  
60  		try {
61  			return new URL(new URL(theBase), theEndpoint).toString();
62  		} catch (MalformedURLException e) {
63  			ourLog.warn("Failed to resolve relative URL[" + theEndpoint + "] against absolute base[" + theBase + "]", e);
64  			return theEndpoint;
65  		}
66  	}
67  
68  	public static String constructRelativeUrl(String theParentExtensionUrl, String theExtensionUrl) {
69  		if (theParentExtensionUrl == null) {
70  			return theExtensionUrl;
71  		}
72  		if (theExtensionUrl == null) {
73  			return theExtensionUrl;
74  		}
75  
76  		int parentLastSlashIdx = theParentExtensionUrl.lastIndexOf('/');
77  		int childLastSlashIdx = theExtensionUrl.lastIndexOf('/');
78  
79  		if (parentLastSlashIdx == -1 || childLastSlashIdx == -1) {
80  			return theExtensionUrl;
81  		}
82  
83  		if (parentLastSlashIdx != childLastSlashIdx) {
84  			return theExtensionUrl;
85  		}
86  
87  		if (!theParentExtensionUrl.substring(0, parentLastSlashIdx).equals(theExtensionUrl.substring(0, parentLastSlashIdx))) {
88  			return theExtensionUrl;
89  		}
90  
91  		if (theExtensionUrl.length() > parentLastSlashIdx) {
92  			return theExtensionUrl.substring(parentLastSlashIdx + 1);
93  		}
94  
95  		return theExtensionUrl;
96  	}
97  
98  	/**
99  	 * 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 }