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