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 == null ? "" : "/"+id));
115  }
116
117  public <T extends Resource> URI resolveValidateUri(String resourceType, String id) {
118    return baseServiceUri.resolve(resourceType +"/$validate"+(id == null ? "" : "/"+id));
119  }
120  
121        public <T extends Resource> URI resolveGetUriFromResourceClass(Class<T> resourceClass) {
122                return baseServiceUri.resolve(nameForClass(resourceClass));
123        }
124        
125        public <T extends Resource> URI resolveGetUriFromResourceClassAndId(Class<T> resourceClass, String id) {
126                return baseServiceUri.resolve(nameForClass(resourceClass) +"/"+id);
127        }
128        
129        public <T extends Resource> URI resolveGetUriFromResourceClassAndIdAndVersion(Class<T> resourceClass, String id, String version) {
130                return baseServiceUri.resolve(nameForClass(resourceClass) +"/"+id+"/_history/"+version);
131        }
132        
133  public <T extends Resource> URI resolveGetUriFromResourceClassAndCanonical(Class<T> resourceClass, String canonicalUrl) {
134    if (canonicalUrl.contains("|"))
135      return baseServiceUri.resolve(nameForClass(resourceClass)+"?url="+canonicalUrl.substring(0, canonicalUrl.indexOf("|"))+"&version="+canonicalUrl.substring(canonicalUrl.indexOf("|")+1));      
136    else
137      return baseServiceUri.resolve(nameForClass(resourceClass)+"?url="+canonicalUrl);
138  }
139  
140        public URI resolveGetHistoryForAllResources(int count) {
141                if(count > 0) {
142                        return appendHttpParameter(baseServiceUri.resolve("_history"), "_count", ""+count);
143                } else {
144                        return baseServiceUri.resolve("_history");
145                }
146}
147        
148        public <T extends Resource> URI resolveGetHistoryForResourceId(Class<T> resourceClass, String id, int count) {
149                return resolveGetHistoryUriForResourceId(resourceClass, id, null, count);
150        }
151        
152        protected <T extends Resource> URI resolveGetHistoryUriForResourceId(Class<T> resourceClass, String id, Object since, int count) {
153                Map<String,String>  parameters = getHistoryParameters(since, count);
154                return appendHttpParameters(baseServiceUri.resolve(nameForClass(resourceClass) + "/" + id + "/_history"), parameters);
155        }
156        
157        public <T extends Resource> URI resolveGetHistoryForResourceType(Class<T> resourceClass, int count) {
158                Map<String,String>  parameters = getHistoryParameters(null, count);
159                return appendHttpParameters(baseServiceUri.resolve(nameForClass(resourceClass) + "/_history"), parameters);
160        }
161        
162        public <T extends Resource> URI resolveGetHistoryForResourceType(Class<T> resourceClass, Object since, int count) {
163                Map<String,String>  parameters = getHistoryParameters(since, count);
164                return appendHttpParameters(baseServiceUri.resolve(nameForClass(resourceClass) + "/_history"), parameters);
165        }
166        
167        public URI resolveGetHistoryForAllResources(Calendar since, int count) {
168                Map<String,String>  parameters = getHistoryParameters(since, count);
169                return appendHttpParameters(baseServiceUri.resolve("_history"), parameters);
170        }
171        
172        public URI resolveGetHistoryForAllResources(Date since, int count) {
173                Map<String,String>  parameters = getHistoryParameters(since, count);
174                return appendHttpParameters(baseServiceUri.resolve("_history"), parameters);
175        }
176        
177        public Map<String,String> getHistoryParameters(Object since, int count) {
178                Map<String,String>  parameters = new HashMap<String,String>();
179                if (since != null) {
180                        parameters.put("_since", since.toString());
181                }
182                if(count > 0) {
183                        parameters.put("_count", ""+count);
184                }
185                return parameters;
186        }
187        
188        public <T extends Resource> URI resolveGetHistoryForResourceId(Class<T> resourceClass, String id, Calendar since, int count) {
189                return resolveGetHistoryUriForResourceId(resourceClass, id, since, count);
190        }
191        
192        public <T extends Resource> URI resolveGetHistoryForResourceId(Class<T> resourceClass, String id, Date since, int count) {
193                return resolveGetHistoryUriForResourceId(resourceClass, id, since, count);
194        }
195        
196        public <T extends Resource> URI resolveGetHistoryForResourceType(Class<T> resourceClass, Calendar since, int count) {
197                return resolveGetHistoryForResourceType(resourceClass, getCalendarDateInIsoTimeFormat(since), count);
198        }
199        
200        public <T extends Resource> URI resolveGetHistoryForResourceType(Class<T> resourceClass, Date since, int count) {
201                return resolveGetHistoryForResourceType(resourceClass, since.toString(), count);
202        }
203        
204        public <T extends Resource> URI resolveGetAllTags() {
205                return baseServiceUri.resolve("_tags");
206        }
207        
208        public <T extends Resource> URI resolveGetAllTagsForResourceType(Class<T> resourceClass) {
209                return baseServiceUri.resolve(nameForClass(resourceClass) + "/_tags");
210        }
211        
212        public <T extends Resource> URI resolveGetTagsForReference(Class<T> resourceClass, String id) {
213                return baseServiceUri.resolve(nameForClass(resourceClass) + "/" + id + "/_tags");
214        }
215        
216        public <T extends Resource> URI resolveGetTagsForResourceVersion(Class<T> resourceClass, String id, String version) {
217                return baseServiceUri.resolve(nameForClass(resourceClass) +"/"+id+"/_history/"+version + "/_tags");
218        }
219        
220        public <T extends Resource> URI resolveDeleteTagsForResourceVersion(Class<T> resourceClass, String id, String version) {
221                return baseServiceUri.resolve(nameForClass(resourceClass) +"/"+id+"/_history/"+version + "/_tags/_delete");
222        }
223        
224
225        public <T extends Resource> String nameForClass(Class<T> resourceClass) {
226          if (resourceClass == null)
227            return null;
228                String res = resourceClass.getSimpleName();
229                if (res.equals("List_"))
230                        return "List";
231                else
232                        return res;
233        }
234        
235  public URI resolveMetadataUri(boolean quick) {
236    return baseServiceUri.resolve(quick ? "metadata?_summary=true" : "metadata");
237  }
238  
239  public URI resolveMetadataTxCaps() {
240    return baseServiceUri.resolve("metadata?mode=terminology");
241  }
242  
243        /**
244         * For now, assume this type of location header structure.
245         * Generalize later: http://hl7connect.healthintersections.com.au/svc/fhir/318/_history/1
246         */
247        public static ResourceAddress.ResourceVersionedIdentifier parseCreateLocation(String locationResponseHeader) {
248                Pattern pattern = Pattern.compile(REGEX_ID_WITH_HISTORY);
249                Matcher matcher = pattern.matcher(locationResponseHeader);
250                ResourceVersionedIdentifier parsedHeader = null;
251                if(matcher.matches()){
252                        String serviceRoot = matcher.group(1);
253                        String resourceType = matcher.group(3);
254                        String id = matcher.group(5);
255                        String version = matcher.group(7);
256                        parsedHeader = new ResourceVersionedIdentifier(serviceRoot, resourceType, id, version);
257                }
258                return parsedHeader;
259        }
260        
261        public static URI buildAbsoluteURI(String absoluteURI) {
262                
263                if(StringUtils.isBlank(absoluteURI)) {
264                        throw new EFhirClientException(0, "Invalid URI", new URISyntaxException(absoluteURI, "URI/URL cannot be blank"));
265                } 
266                
267                String endpoint = appendForwardSlashToPath(absoluteURI);
268
269                return buildEndpointUriFromString(endpoint);
270        }
271        
272        public static String appendForwardSlashToPath(String path) {
273                if(path.lastIndexOf('/') != path.length() - 1) {
274                        path += "/";
275                }
276                return path;
277        }
278        
279        public static URI buildEndpointUriFromString(String endpointPath) {
280                URI uri = null; 
281                try {
282                        URIBuilder uriBuilder = new URIBuilder(endpointPath);
283                        uri = uriBuilder.build();
284                        String scheme = uri.getScheme();
285                        String host = uri.getHost();
286                        if(!scheme.equalsIgnoreCase("http") && !scheme.equalsIgnoreCase("https")) {
287                                throw new EFhirClientException(0, "Scheme must be 'http' or 'https': " + uri);
288                        }
289                        if(StringUtils.isBlank(host)) {
290                                throw new EFhirClientException(0, "host cannot be blank: " + uri);
291                        }
292                } catch(URISyntaxException e) {
293                        throw new EFhirClientException(0, "Invalid URI", e);
294                }
295                return uri;
296        }
297        
298        public static URI appendQueryStringToUri(URI uri, String parameterName, String parameterValue) {
299                URI modifiedUri = null;
300                try {
301                        URIBuilder uriBuilder = new URIBuilder(uri);
302                        uriBuilder.setQuery(parameterName + "=" + parameterValue);
303                        modifiedUri = uriBuilder.build();
304                } catch(Exception e) {
305                        throw new EFhirClientException(0, "Unable to append query parameter '" + parameterName + "=" + parameterValue + " to URI " + uri, e);
306                }
307                return modifiedUri;
308        }
309        
310        public static String buildRelativePathFromResourceType(ResourceType resourceType) {
311                //return resourceType.toString().toLowerCase()+"/";
312                return resourceType.toString() + "/";
313        }
314        
315        public static String buildRelativePathFromResourceType(ResourceType resourceType, String id) {
316                return buildRelativePathFromResourceType(resourceType)+ "@" + id;
317        }
318        
319        public static String buildRelativePathFromReference(Resource resource) {
320                return buildRelativePathFromResourceType(resource.getResourceType());
321        }
322        
323        public static String buildRelativePathFromReference(Resource resource, String id) {
324                return buildRelativePathFromResourceType(resource.getResourceType(), id);
325        }
326        
327        public static class ResourceVersionedIdentifier {
328                
329                private String serviceRoot;
330                private String resourceType;
331                private String id;
332                private String version;
333                private URI resourceLocation;
334                
335                public ResourceVersionedIdentifier(String serviceRoot, String resourceType, String id, String version, URI resourceLocation) {
336                        this.serviceRoot = serviceRoot;
337                        this.resourceType = resourceType;
338                        this.id = id;
339                        this.version = version;
340                        this.resourceLocation = resourceLocation;
341                }
342                
343                public ResourceVersionedIdentifier(String resourceType, String id, String version, URI resourceLocation) {
344                        this(null, resourceType, id, version, resourceLocation);
345                }
346                
347                public ResourceVersionedIdentifier(String serviceRoot, String resourceType, String id, String version) {
348                        this(serviceRoot, resourceType, id, version, null);
349                }
350                
351                public ResourceVersionedIdentifier(String resourceType, String id, String version) {
352                        this(null, resourceType, id, version, null);
353                }
354                
355                public ResourceVersionedIdentifier(String resourceType, String id) {
356                        this.id = id;
357                }
358                
359                public String getId() {
360                        return this.id;
361                }
362                
363                protected void setId(String id) {
364                        this.id = id;
365                }
366                
367                public String getVersionId() {
368                        return this.version;
369                }
370                
371                protected void setVersionId(String version) {
372                        this.version = version;
373                }
374                
375                public String getResourceType() {
376                        return resourceType;
377                }
378
379                public void setResourceType(String resourceType) {
380                        this.resourceType = resourceType;
381                }
382                
383                public String getServiceRoot() {
384                        return serviceRoot;
385                }
386
387                public void setServiceRoot(String serviceRoot) {
388                        this.serviceRoot = serviceRoot;
389                }
390                
391                public String getResourcePath() {
392                        return this.serviceRoot + "/" + this.resourceType + "/" + this.id;
393                }
394
395                public String getVersion() {
396                        return version;
397                }
398
399                public void setVersion(String version) {
400                        this.version = version;
401                }
402
403                public URI getResourceLocation() {
404                        return this.resourceLocation;
405                }
406                
407                public void setResourceLocation(URI resourceLocation) {
408                        this.resourceLocation = resourceLocation;
409                }
410        }
411        
412        public static String getCalendarDateInIsoTimeFormat(Calendar calendar) {
413                SimpleDateFormat format = new SimpleDateFormat("yyyy-MM-dd'T'hh:mm:ss", new Locale("en", "US"));//TODO Move out
414                format.setTimeZone(TimeZone.getTimeZone("GMT"));
415            return format.format(calendar.getTime());
416        }
417        
418        public static URI appendHttpParameter(URI basePath, String httpParameterName, String httpParameterValue) {
419                Map<String, String> parameters = new HashMap<String, String>();
420                parameters.put(httpParameterName, httpParameterValue);
421                return appendHttpParameters(basePath, parameters);
422        }
423        
424        public static URI appendHttpParameters(URI basePath, Map<String,String> parameters) {
425        try {
426          List<NameValuePair> existingParams = URLEncodedUtils.parse(basePath.getQuery(), StandardCharsets.UTF_8);
427
428          URIBuilder uriBuilder = new URIBuilder()
429            .setScheme(basePath.getScheme())
430            .setHost(basePath.getHost())
431            .setPort(basePath.getPort())
432            .setUserInfo(basePath.getUserInfo())
433            .setFragment(basePath.getFragment());
434          for (NameValuePair pair : existingParams) {
435            uriBuilder.addParameter(pair.getName(), pair.getValue());
436          }
437          for (Map.Entry<String, String> entry : parameters.entrySet()) {
438            uriBuilder.addParameter(entry.getKey(), entry.getValue());
439          }
440          uriBuilder.setPath(basePath.getPath());
441
442          return uriBuilder.build();
443        } catch(Exception e) {
444                throw new EFhirClientException(0, "Error appending http parameter", e);
445        }
446    }
447        
448}