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}