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