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        /**
222         * For now, assume this type of location header structure.
223         * Generalize later: http://hl7connect.healthintersections.com.au/svc/fhir/318/_history/1
224         * 
225         * @param serviceBase
226         * @param locationHeader
227         */
228        public static ResourceAddress.ResourceVersionedIdentifier parseCreateLocation(String locationResponseHeader) {
229                Pattern pattern = Pattern.compile(REGEX_ID_WITH_HISTORY);
230                Matcher matcher = pattern.matcher(locationResponseHeader);
231                ResourceVersionedIdentifier parsedHeader = null;
232                if(matcher.matches()){
233                        String serviceRoot = matcher.group(1);
234                        String resourceType = matcher.group(3);
235                        String id = matcher.group(5);
236                        String version = matcher.group(7);
237                        parsedHeader = new ResourceVersionedIdentifier(serviceRoot, resourceType, id, version);
238                }
239                return parsedHeader;
240        }
241        
242        public static URI buildAbsoluteURI(String absoluteURI) {
243                
244                if(StringUtils.isBlank(absoluteURI)) {
245                        throw new EFhirClientException("Invalid URI", new URISyntaxException(absoluteURI, "URI/URL cannot be blank"));
246                } 
247                
248                String endpoint = appendForwardSlashToPath(absoluteURI);
249
250                return buildEndpointUriFromString(endpoint);
251        }
252        
253        public static String appendForwardSlashToPath(String path) {
254                if(path.lastIndexOf('/') != path.length() - 1) {
255                        path += "/";
256                }
257                return path;
258        }
259        
260        public static URI buildEndpointUriFromString(String endpointPath) {
261                URI uri = null; 
262                try {
263                        URIBuilder uriBuilder = new URIBuilder(endpointPath);
264                        uri = uriBuilder.build();
265                        String scheme = uri.getScheme();
266                        String host = uri.getHost();
267                        if(!scheme.equalsIgnoreCase("http") && !scheme.equalsIgnoreCase("https")) {
268                                throw new EFhirClientException("Scheme must be 'http' or 'https': " + uri);
269                        }
270                        if(StringUtils.isBlank(host)) {
271                                throw new EFhirClientException("host cannot be blank: " + uri);
272                        }
273                } catch(URISyntaxException e) {
274                        throw new EFhirClientException("Invalid URI", e);
275                }
276                return uri;
277        }
278        
279        public static URI appendQueryStringToUri(URI uri, String parameterName, String parameterValue) {
280                URI modifiedUri = null;
281                try {
282                        URIBuilder uriBuilder = new URIBuilder(uri);
283                        uriBuilder.setQuery(parameterName + "=" + parameterValue);
284                        modifiedUri = uriBuilder.build();
285                } catch(Exception e) {
286                        throw new EFhirClientException("Unable to append query parameter '" + parameterName + "=" + parameterValue + " to URI " + uri, e);
287                }
288                return modifiedUri;
289        }
290        
291        public static String buildRelativePathFromResourceType(ResourceType resourceType) {
292                //return resourceType.toString().toLowerCase()+"/";
293                return resourceType.toString() + "/";
294        }
295        
296        public static String buildRelativePathFromResourceType(ResourceType resourceType, String id) {
297                return buildRelativePathFromResourceType(resourceType)+ "@" + id;
298        }
299        
300        public static String buildRelativePathFromReference(Resource resource) {
301                return buildRelativePathFromResourceType(resource.getResourceType());
302        }
303        
304        public static String buildRelativePathFromReference(Resource resource, String id) {
305                return buildRelativePathFromResourceType(resource.getResourceType(), id);
306        }
307        
308        public static class ResourceVersionedIdentifier {
309                
310                private String serviceRoot;
311                private String resourceType;
312                private String id;
313                private String version;
314                private URI resourceLocation;
315                
316                public ResourceVersionedIdentifier(String serviceRoot, String resourceType, String id, String version, URI resourceLocation) {
317                        this.serviceRoot = serviceRoot;
318                        this.resourceType = resourceType;
319                        this.id = id;
320                        this.version = version;
321                        this.resourceLocation = resourceLocation;
322                }
323                
324                public ResourceVersionedIdentifier(String resourceType, String id, String version, URI resourceLocation) {
325                        this(null, resourceType, id, version, resourceLocation);
326                }
327                
328                public ResourceVersionedIdentifier(String serviceRoot, String resourceType, String id, String version) {
329                        this(serviceRoot, resourceType, id, version, null);
330                }
331                
332                public ResourceVersionedIdentifier(String resourceType, String id, String version) {
333                        this(null, resourceType, id, version, null);
334                }
335                
336                public ResourceVersionedIdentifier(String resourceType, String id) {
337                        this.id = id;
338                }
339                
340                public String getId() {
341                        return this.id;
342                }
343                
344                protected void setId(String id) {
345                        this.id = id;
346                }
347                
348                public String getVersionId() {
349                        return this.version;
350                }
351                
352                protected void setVersionId(String version) {
353                        this.version = version;
354                }
355                
356                public String getResourceType() {
357                        return resourceType;
358                }
359
360                public void setResourceType(String resourceType) {
361                        this.resourceType = resourceType;
362                }
363                
364                public String getServiceRoot() {
365                        return serviceRoot;
366                }
367
368                public void setServiceRoot(String serviceRoot) {
369                        this.serviceRoot = serviceRoot;
370                }
371                
372                public String getResourcePath() {
373                        return this.serviceRoot + "/" + this.resourceType + "/" + this.id;
374                }
375
376                public String getVersion() {
377                        return version;
378                }
379
380                public void setVersion(String version) {
381                        this.version = version;
382                }
383
384                public URI getResourceLocation() {
385                        return this.resourceLocation;
386                }
387                
388                public void setResourceLocation(URI resourceLocation) {
389                        this.resourceLocation = resourceLocation;
390                }
391        }
392        
393        public static String getCalendarDateInIsoTimeFormat(Calendar calendar) {
394                SimpleDateFormat format = new SimpleDateFormat("yyyy-MM-dd'T'hh:mm:ss");//TODO Move out
395                format.setTimeZone(TimeZone.getTimeZone("GMT"));
396            return format.format(calendar.getTime());
397        }
398        
399        public static URI appendHttpParameter(URI basePath, String httpParameterName, String httpParameterValue) {
400                Map<String, String> parameters = new HashMap<String, String>();
401                parameters.put(httpParameterName, httpParameterValue);
402                return appendHttpParameters(basePath, parameters);
403        }
404        
405        public static URI appendHttpParameters(URI basePath, Map<String,String> parameters) {
406        try {
407                Set<String> httpParameterNames = parameters.keySet();
408                String query = basePath.getQuery();
409                
410                for(String httpParameterName : httpParameterNames) {
411                        if(query != null) {
412                                query += "&";
413                        } else {
414                                query = "";
415                        }
416                        query += httpParameterName + "=" + Utilities.encodeUri(parameters.get(httpParameterName));
417                }
418        
419                return new URI(basePath.getScheme(), basePath.getUserInfo(), basePath.getHost(),basePath.getPort(), basePath.getPath(), query, basePath.getFragment());
420        } catch(Exception e) {
421                throw new EFhirClientException("Error appending http parameter", e);
422        }
423    }
424        
425}