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