001package org.hl7.fhir.r5.utils.client;
002
003
004
005
006
007/*
008  Copyright (c) 2011+, HL7, Inc.
009  All rights reserved.
010  
011  Redistribution and use in source and binary forms, with or without modification, 
012  are permitted provided that the following conditions are met:
013  
014   * Redistributions of source code must retain the above copyright notice, this 
015     list of conditions and the following disclaimer.
016   * Redistributions in binary form must reproduce the above copyright notice, 
017     this list of conditions and the following disclaimer in the documentation 
018     and/or other materials provided with the distribution.
019   * Neither the name of HL7 nor the names of its contributors may be used to 
020     endorse or promote products derived from this software without specific 
021     prior written permission.
022  
023  THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND 
024  ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED 
025  WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. 
026  IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, 
027  INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT 
028  NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR 
029  PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, 
030  WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) 
031  ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE 
032  POSSIBILITY OF SUCH DAMAGE.
033  
034*/
035
036
037import java.net.URI;
038import java.net.URISyntaxException;
039import java.nio.charset.StandardCharsets;
040import java.text.SimpleDateFormat;
041import java.util.*;
042import java.util.regex.Matcher;
043import java.util.regex.Pattern;
044
045import org.apache.commons.lang3.StringUtils;
046import org.apache.http.NameValuePair;
047import org.apache.http.client.utils.URIBuilder;
048import org.apache.http.client.utils.URLEncodedUtils;
049import org.hl7.fhir.r5.model.Resource;
050import org.hl7.fhir.r5.model.ResourceType;
051import org.hl7.fhir.utilities.Utilities;
052
053//Make resources address subclass of URI
054/**
055 * Helper class to manage FHIR Resource URIs
056 * 
057 * @author Claude Nanjo
058 *
059 */
060public class ResourceAddress {
061        
062        public static final String REGEX_ID_WITH_HISTORY = "(.*)(/)([a-zA-Z0-9]*)(/)([a-z0-9\\-\\.]{1,64})(/_history/)([a-z0-9\\-\\.]{1,64})$";
063        
064        private URI baseServiceUri;
065        
066        public ResourceAddress(String endpointPath) throws URISyntaxException {//TODO Revisit this exception
067                this.baseServiceUri = ResourceAddress.buildAbsoluteURI(endpointPath);
068        }
069        
070        public ResourceAddress(URI baseServiceUri) {
071                this.baseServiceUri = baseServiceUri;
072        }
073        
074        public URI getBaseServiceUri() {
075                return this.baseServiceUri;
076        }
077        
078
079  public URI resolveGetUriFromResourceClassAndId(String resourceClass, String id) {
080    return baseServiceUri.resolve(resourceClass + "/" + id);
081  }
082
083  public <T extends Resource> URI resolveGetResource(Class<T> resourceClass, String id) {
084    return baseServiceUri.resolve(nameForClassWithSlash(resourceClass) + "/" + id);
085  }
086  
087        public <T extends Resource> URI resolveOperationURLFromClass(Class<T> resourceClass, String name, String parameters) {
088                return baseServiceUri.resolve(nameForClassWithSlash(resourceClass) +"$"+name+"?"+ parameters);
089        }
090        
091        public <T extends Resource> URI resolveSearchUri(Class<T> resourceClass, Map<String,String> parameters) {
092                return appendHttpParameters(baseServiceUri.resolve(nameForClassWithSlash(resourceClass) +"_search"), parameters);
093        }
094        
095  private <T extends Resource> String nameForClassWithSlash(Class<T> resourceClass) {
096    String n = nameForClass(resourceClass);
097    return n == null ? "" : n +"/";
098        }
099        
100  public <T extends Resource> URI resolveOperationUri(Class<T> resourceClass, String opName) {
101    return baseServiceUri.resolve(nameForClassWithSlash(resourceClass) +"$"+opName);
102  }
103
104  public <T extends Resource> URI resolveOperationUri(Class<T> resourceClass, String opName, Map<String,String> parameters) {
105    return appendHttpParameters(baseServiceUri.resolve(nameForClassWithSlash(resourceClass) +"$"+opName), parameters);
106  }
107
108  @Deprecated
109  public <T extends Resource> URI resolveOperationUriNoEscape(Class<T> resourceClass, String opName, Map<String,String> parameters) {
110    return appendHttpParameters(baseServiceUri.resolve(nameForClassWithSlash(resourceClass) +"$"+opName), parameters);
111  }
112  
113        public <T extends Resource> URI resolveValidateUri(Class<T> resourceClass, String id) {
114                return baseServiceUri.resolve(nameForClassWithSlash(resourceClass) +"$validate/"+id);
115        }
116        
117        public <T extends Resource> URI resolveGetUriFromResourceClass(Class<T> resourceClass) {
118                return baseServiceUri.resolve(nameForClass(resourceClass));
119        }
120        
121        public <T extends Resource> URI resolveGetUriFromResourceClassAndId(Class<T> resourceClass, String id) {
122                return baseServiceUri.resolve(nameForClass(resourceClass) +"/"+id);
123        }
124        
125        public <T extends Resource> URI resolveGetUriFromResourceClassAndIdAndVersion(Class<T> resourceClass, String id, String version) {
126                return baseServiceUri.resolve(nameForClass(resourceClass) +"/"+id+"/_history/"+version);
127        }
128        
129  public <T extends Resource> URI resolveGetUriFromResourceClassAndCanonical(Class<T> resourceClass, String canonicalUrl) {
130    if (canonicalUrl.contains("|"))
131      return baseServiceUri.resolve(nameForClass(resourceClass)+"?url="+canonicalUrl.substring(0, canonicalUrl.indexOf("|"))+"&version="+canonicalUrl.substring(canonicalUrl.indexOf("|")+1));      
132    else
133      return baseServiceUri.resolve(nameForClass(resourceClass)+"?url="+canonicalUrl);
134  }
135  
136        public URI resolveGetHistoryForAllResources(int count) {
137                if(count > 0) {
138                        return appendHttpParameter(baseServiceUri.resolve("_history"), "_count", ""+count);
139                } else {
140                        return baseServiceUri.resolve("_history");
141                }
142}
143        
144        public <T extends Resource> URI resolveGetHistoryForResourceId(Class<T> resourceClass, String id, int count) {
145                return resolveGetHistoryUriForResourceId(resourceClass, id, null, count);
146        }
147        
148        protected <T extends Resource> URI resolveGetHistoryUriForResourceId(Class<T> resourceClass, String id, Object since, int count) {
149                Map<String,String>  parameters = getHistoryParameters(since, count);
150                return appendHttpParameters(baseServiceUri.resolve(nameForClass(resourceClass) + "/" + id + "/_history"), parameters);
151        }
152        
153        public <T extends Resource> URI resolveGetHistoryForResourceType(Class<T> resourceClass, int count) {
154                Map<String,String>  parameters = getHistoryParameters(null, count);
155                return appendHttpParameters(baseServiceUri.resolve(nameForClass(resourceClass) + "/_history"), parameters);
156        }
157        
158        public <T extends Resource> URI resolveGetHistoryForResourceType(Class<T> resourceClass, Object since, int count) {
159                Map<String,String>  parameters = getHistoryParameters(since, count);
160                return appendHttpParameters(baseServiceUri.resolve(nameForClass(resourceClass) + "/_history"), parameters);
161        }
162        
163        public URI resolveGetHistoryForAllResources(Calendar since, int count) {
164                Map<String,String>  parameters = getHistoryParameters(since, count);
165                return appendHttpParameters(baseServiceUri.resolve("_history"), parameters);
166        }
167        
168        public URI resolveGetHistoryForAllResources(Date since, int count) {
169                Map<String,String>  parameters = getHistoryParameters(since, count);
170                return appendHttpParameters(baseServiceUri.resolve("_history"), parameters);
171        }
172        
173        public Map<String,String> getHistoryParameters(Object since, int count) {
174                Map<String,String>  parameters = new HashMap<String,String>();
175                if (since != null) {
176                        parameters.put("_since", since.toString());
177                }
178                if(count > 0) {
179                        parameters.put("_count", ""+count);
180                }
181                return parameters;
182        }
183        
184        public <T extends Resource> URI resolveGetHistoryForResourceId(Class<T> resourceClass, String id, Calendar since, int count) {
185                return resolveGetHistoryUriForResourceId(resourceClass, id, since, count);
186        }
187        
188        public <T extends Resource> URI resolveGetHistoryForResourceId(Class<T> resourceClass, String id, Date since, int count) {
189                return resolveGetHistoryUriForResourceId(resourceClass, id, since, count);
190        }
191        
192        public <T extends Resource> URI resolveGetHistoryForResourceType(Class<T> resourceClass, Calendar since, int count) {
193                return resolveGetHistoryForResourceType(resourceClass, getCalendarDateInIsoTimeFormat(since), count);
194        }
195        
196        public <T extends Resource> URI resolveGetHistoryForResourceType(Class<T> resourceClass, Date since, int count) {
197                return resolveGetHistoryForResourceType(resourceClass, since.toString(), count);
198        }
199        
200        public <T extends Resource> URI resolveGetAllTags() {
201                return baseServiceUri.resolve("_tags");
202        }
203        
204        public <T extends Resource> URI resolveGetAllTagsForResourceType(Class<T> resourceClass) {
205                return baseServiceUri.resolve(nameForClass(resourceClass) + "/_tags");
206        }
207        
208        public <T extends Resource> URI resolveGetTagsForReference(Class<T> resourceClass, String id) {
209                return baseServiceUri.resolve(nameForClass(resourceClass) + "/" + id + "/_tags");
210        }
211        
212        public <T extends Resource> URI resolveGetTagsForResourceVersion(Class<T> resourceClass, String id, String version) {
213                return baseServiceUri.resolve(nameForClass(resourceClass) +"/"+id+"/_history/"+version + "/_tags");
214        }
215        
216        public <T extends Resource> URI resolveDeleteTagsForResourceVersion(Class<T> resourceClass, String id, String version) {
217                return baseServiceUri.resolve(nameForClass(resourceClass) +"/"+id+"/_history/"+version + "/_tags/_delete");
218        }
219        
220
221        public <T extends Resource> String nameForClass(Class<T> resourceClass) {
222          if (resourceClass == null)
223            return null;
224                String res = resourceClass.getSimpleName();
225                if (res.equals("List_"))
226                        return "List";
227                else
228                        return res;
229        }
230        
231  public URI resolveMetadataUri(boolean quick) {
232    return baseServiceUri.resolve(quick ? "metadata?_summary=true" : "metadata");
233  }
234  
235  public URI resolveMetadataTxCaps() {
236    return baseServiceUri.resolve("metadata?mode=terminology");
237  }
238  
239        /**
240         * For now, assume this type of location header structure.
241         * Generalize later: http://hl7connect.healthintersections.com.au/svc/fhir/318/_history/1
242         */
243        public static ResourceAddress.ResourceVersionedIdentifier parseCreateLocation(String locationResponseHeader) {
244                Pattern pattern = Pattern.compile(REGEX_ID_WITH_HISTORY);
245                Matcher matcher = pattern.matcher(locationResponseHeader);
246                ResourceVersionedIdentifier parsedHeader = null;
247                if(matcher.matches()){
248                        String serviceRoot = matcher.group(1);
249                        String resourceType = matcher.group(3);
250                        String id = matcher.group(5);
251                        String version = matcher.group(7);
252                        parsedHeader = new ResourceVersionedIdentifier(serviceRoot, resourceType, id, version);
253                }
254                return parsedHeader;
255        }
256        
257        public static URI buildAbsoluteURI(String absoluteURI) {
258                
259                if(StringUtils.isBlank(absoluteURI)) {
260                        throw new EFhirClientException(0, "Invalid URI", new URISyntaxException(absoluteURI, "URI/URL cannot be blank"));
261                } 
262                
263                String endpoint = appendForwardSlashToPath(absoluteURI);
264
265                return buildEndpointUriFromString(endpoint);
266        }
267        
268        public static String appendForwardSlashToPath(String path) {
269                if(path.lastIndexOf('/') != path.length() - 1) {
270                        path += "/";
271                }
272                return path;
273        }
274        
275        public static URI buildEndpointUriFromString(String endpointPath) {
276                URI uri = null; 
277                try {
278                        URIBuilder uriBuilder = new URIBuilder(endpointPath);
279                        uri = uriBuilder.build();
280                        String scheme = uri.getScheme();
281                        String host = uri.getHost();
282                        if(!scheme.equalsIgnoreCase("http") && !scheme.equalsIgnoreCase("https")) {
283                                throw new EFhirClientException(0, "Scheme must be 'http' or 'https': " + uri);
284                        }
285                        if(StringUtils.isBlank(host)) {
286                                throw new EFhirClientException(0, "host cannot be blank: " + uri);
287                        }
288                } catch(URISyntaxException e) {
289                        throw new EFhirClientException(0, "Invalid URI", e);
290                }
291                return uri;
292        }
293        
294        public static URI appendQueryStringToUri(URI uri, String parameterName, String parameterValue) {
295                URI modifiedUri = null;
296                try {
297                        URIBuilder uriBuilder = new URIBuilder(uri);
298                        uriBuilder.setQuery(parameterName + "=" + parameterValue);
299                        modifiedUri = uriBuilder.build();
300                } catch(Exception e) {
301                        throw new EFhirClientException(0, "Unable to append query parameter '" + parameterName + "=" + parameterValue + " to URI " + uri, e);
302                }
303                return modifiedUri;
304        }
305        
306        public static String buildRelativePathFromResourceType(ResourceType resourceType) {
307                //return resourceType.toString().toLowerCase()+"/";
308                return resourceType.toString() + "/";
309        }
310        
311        public static String buildRelativePathFromResourceType(ResourceType resourceType, String id) {
312                return buildRelativePathFromResourceType(resourceType)+ "@" + id;
313        }
314        
315        public static String buildRelativePathFromReference(Resource resource) {
316                return buildRelativePathFromResourceType(resource.getResourceType());
317        }
318        
319        public static String buildRelativePathFromReference(Resource resource, String id) {
320                return buildRelativePathFromResourceType(resource.getResourceType(), id);
321        }
322        
323        public static class ResourceVersionedIdentifier {
324                
325                private String serviceRoot;
326                private String resourceType;
327                private String id;
328                private String version;
329                private URI resourceLocation;
330                
331                public ResourceVersionedIdentifier(String serviceRoot, String resourceType, String id, String version, URI resourceLocation) {
332                        this.serviceRoot = serviceRoot;
333                        this.resourceType = resourceType;
334                        this.id = id;
335                        this.version = version;
336                        this.resourceLocation = resourceLocation;
337                }
338                
339                public ResourceVersionedIdentifier(String resourceType, String id, String version, URI resourceLocation) {
340                        this(null, resourceType, id, version, resourceLocation);
341                }
342                
343                public ResourceVersionedIdentifier(String serviceRoot, String resourceType, String id, String version) {
344                        this(serviceRoot, resourceType, id, version, null);
345                }
346                
347                public ResourceVersionedIdentifier(String resourceType, String id, String version) {
348                        this(null, resourceType, id, version, null);
349                }
350                
351                public ResourceVersionedIdentifier(String resourceType, String id) {
352                        this.id = id;
353                }
354                
355                public String getId() {
356                        return this.id;
357                }
358                
359                protected void setId(String id) {
360                        this.id = id;
361                }
362                
363                public String getVersionId() {
364                        return this.version;
365                }
366                
367                protected void setVersionId(String version) {
368                        this.version = version;
369                }
370                
371                public String getResourceType() {
372                        return resourceType;
373                }
374
375                public void setResourceType(String resourceType) {
376                        this.resourceType = resourceType;
377                }
378                
379                public String getServiceRoot() {
380                        return serviceRoot;
381                }
382
383                public void setServiceRoot(String serviceRoot) {
384                        this.serviceRoot = serviceRoot;
385                }
386                
387                public String getResourcePath() {
388                        return this.serviceRoot + "/" + this.resourceType + "/" + this.id;
389                }
390
391                public String getVersion() {
392                        return version;
393                }
394
395                public void setVersion(String version) {
396                        this.version = version;
397                }
398
399                public URI getResourceLocation() {
400                        return this.resourceLocation;
401                }
402                
403                public void setResourceLocation(URI resourceLocation) {
404                        this.resourceLocation = resourceLocation;
405                }
406        }
407        
408        public static String getCalendarDateInIsoTimeFormat(Calendar calendar) {
409                SimpleDateFormat format = new SimpleDateFormat("yyyy-MM-dd'T'hh:mm:ss", new Locale("en", "US"));//TODO Move out
410                format.setTimeZone(TimeZone.getTimeZone("GMT"));
411            return format.format(calendar.getTime());
412        }
413        
414        public static URI appendHttpParameter(URI basePath, String httpParameterName, String httpParameterValue) {
415                Map<String, String> parameters = new HashMap<String, String>();
416                parameters.put(httpParameterName, httpParameterValue);
417                return appendHttpParameters(basePath, parameters);
418        }
419        
420        public static URI appendHttpParameters(URI basePath, Map<String,String> parameters) {
421        try {
422          List<NameValuePair> existingParams = URLEncodedUtils.parse(basePath.getQuery(), StandardCharsets.UTF_8);
423
424          URIBuilder uriBuilder = new URIBuilder()
425            .setScheme(basePath.getScheme())
426            .setHost(basePath.getHost())
427            .setPort(basePath.getPort())
428            .setUserInfo(basePath.getUserInfo())
429            .setFragment(basePath.getFragment());
430          for (NameValuePair pair : existingParams) {
431            uriBuilder.addParameter(pair.getName(), pair.getValue());
432          }
433          for (Map.Entry<String, String> entry : parameters.entrySet()) {
434            uriBuilder.addParameter(entry.getKey(), entry.getValue());
435          }
436          uriBuilder.setPath(basePath.getPath());
437
438          return uriBuilder.build();
439        } catch(Exception e) {
440                throw new EFhirClientException(0, "Error appending http parameter", e);
441        }
442    }
443        
444}