001package org.hl7.fhir.r4.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.text.SimpleDateFormat;
040import java.util.Calendar;
041import java.util.Date;
042import java.util.HashMap;
043import java.util.Locale;
044import java.util.Map;
045import java.util.Set;
046import java.util.TimeZone;
047import java.util.regex.Matcher;
048import java.util.regex.Pattern;
049
050import org.apache.commons.lang3.StringUtils;
051import org.apache.http.client.utils.URIBuilder;
052import org.hl7.fhir.r4.model.Resource;
053import org.hl7.fhir.r4.model.ResourceType;
054import org.hl7.fhir.utilities.Utilities;
055
056//Make resources address subclass of URI
057/**
058 * Helper class to manage FHIR Resource URIs
059 * 
060 * @author Claude Nanjo
061 *
062 */
063public class ResourceAddress {
064        
065        public static final String REGEX_ID_WITH_HISTORY = "(.*)(/)([a-zA-Z0-9]*)(/)([a-z0-9\\-\\.]{1,64})(/_history/)([a-z0-9\\-\\.]{1,64})$";
066        
067        private URI baseServiceUri;
068        
069        public ResourceAddress(String endpointPath) throws URISyntaxException {//TODO Revisit this exception
070                this.baseServiceUri = ResourceAddress.buildAbsoluteURI(endpointPath);
071        }
072        
073        public ResourceAddress(URI baseServiceUri) {
074                this.baseServiceUri = baseServiceUri;
075        }
076        
077        public URI getBaseServiceUri() {
078                return this.baseServiceUri;
079        }
080        
081        public <T extends Resource> URI resolveOperationURLFromClass(Class<T> resourceClass, String name, String parameters) {
082                return baseServiceUri.resolve(nameForClassWithSlash(resourceClass) +"$"+name+"?"+ parameters);
083        }
084        
085        public <T extends Resource> URI resolveSearchUri(Class<T> resourceClass, Map<String,String> parameters) {
086                return appendHttpParameters(baseServiceUri.resolve(nameForClassWithSlash(resourceClass) +"_search"), parameters);
087        }
088        
089  private <T extends Resource> String nameForClassWithSlash(Class<T> resourceClass) {
090    String n = nameForClass(resourceClass);
091    return n == null ? "" : n +"/";
092        }
093        
094  public <T extends Resource> URI resolveOperationUri(Class<T> resourceClass, String opName) {
095    return baseServiceUri.resolve(nameForClassWithSlash(resourceClass) +"/"+opName);
096  }
097  
098  public <T extends Resource> URI resolveOperationUri(Class<T> resourceClass, String opName, Map<String,String> parameters) {
099    return appendHttpParameters(baseServiceUri.resolve(nameForClassWithSlash(resourceClass) +"$"+opName), parameters);
100  }
101  
102        public <T extends Resource> URI resolveValidateUri(Class<T> resourceClass, String id) {
103                return baseServiceUri.resolve(nameForClassWithSlash(resourceClass) +"$validate/"+id);
104        }
105        
106        public <T extends Resource> URI resolveGetUriFromResourceClass(Class<T> resourceClass) {
107                return baseServiceUri.resolve(nameForClass(resourceClass));
108        }
109        
110        public <T extends Resource> URI resolveGetUriFromResourceClassAndId(Class<T> resourceClass, String id) {
111                return baseServiceUri.resolve(nameForClass(resourceClass) +"/"+id);
112        }
113        
114        public <T extends Resource> URI resolveGetUriFromResourceClassAndIdAndVersion(Class<T> resourceClass, String id, String version) {
115                return baseServiceUri.resolve(nameForClass(resourceClass) +"/"+id+"/_history/"+version);
116        }
117        
118  public <T extends Resource> URI resolveGetUriFromResourceClassAndCanonical(Class<T> resourceClass, String canonicalUrl) {
119    if (canonicalUrl.contains("|"))
120      return baseServiceUri.resolve(nameForClass(resourceClass)+"?url="+canonicalUrl.substring(0, canonicalUrl.indexOf("|"))+"&version="+canonicalUrl.substring(canonicalUrl.indexOf("|")+1));      
121    else
122      return baseServiceUri.resolve(nameForClass(resourceClass)+"?url="+canonicalUrl);
123  }
124  
125        public URI resolveGetHistoryForAllResources(int count) {
126                if(count > 0) {
127                        return appendHttpParameter(baseServiceUri.resolve("_history"), "_count", ""+count);
128                } else {
129                        return baseServiceUri.resolve("_history");
130                }
131}
132        
133        public <T extends Resource> URI resolveGetHistoryForResourceId(Class<T> resourceClass, String id, int count) {
134                return resolveGetHistoryUriForResourceId(resourceClass, id, null, count);
135        }
136        
137        protected <T extends Resource> URI resolveGetHistoryUriForResourceId(Class<T> resourceClass, String id, Object since, int count) {
138                Map<String,String>  parameters = getHistoryParameters(since, count);
139                return appendHttpParameters(baseServiceUri.resolve(nameForClass(resourceClass) + "/" + id + "/_history"), parameters);
140        }
141        
142        public <T extends Resource> URI resolveGetHistoryForResourceType(Class<T> resourceClass, int count) {
143                Map<String,String>  parameters = getHistoryParameters(null, count);
144                return appendHttpParameters(baseServiceUri.resolve(nameForClass(resourceClass) + "/_history"), parameters);
145        }
146        
147        public <T extends Resource> URI resolveGetHistoryForResourceType(Class<T> resourceClass, Object since, int count) {
148                Map<String,String>  parameters = getHistoryParameters(since, count);
149                return appendHttpParameters(baseServiceUri.resolve(nameForClass(resourceClass) + "/_history"), parameters);
150        }
151        
152        public URI resolveGetHistoryForAllResources(Calendar since, int count) {
153                Map<String,String>  parameters = getHistoryParameters(since, count);
154                return appendHttpParameters(baseServiceUri.resolve("_history"), parameters);
155        }
156        
157        public URI resolveGetHistoryForAllResources(Date since, int count) {
158                Map<String,String>  parameters = getHistoryParameters(since, count);
159                return appendHttpParameters(baseServiceUri.resolve("_history"), parameters);
160        }
161        
162        public Map<String,String> getHistoryParameters(Object since, int count) {
163                Map<String,String>  parameters = new HashMap<String,String>();
164                if (since != null) {
165                        parameters.put("_since", since.toString());
166                }
167                if(count > 0) {
168                        parameters.put("_count", ""+count);
169                }
170                return parameters;
171        }
172        
173        public <T extends Resource> URI resolveGetHistoryForResourceId(Class<T> resourceClass, String id, Calendar since, int count) {
174                return resolveGetHistoryUriForResourceId(resourceClass, id, since, count);
175        }
176        
177        public <T extends Resource> URI resolveGetHistoryForResourceId(Class<T> resourceClass, String id, Date since, int count) {
178                return resolveGetHistoryUriForResourceId(resourceClass, id, since, count);
179        }
180        
181        public <T extends Resource> URI resolveGetHistoryForResourceType(Class<T> resourceClass, Calendar since, int count) {
182                return resolveGetHistoryForResourceType(resourceClass, getCalendarDateInIsoTimeFormat(since), count);
183        }
184        
185        public <T extends Resource> URI resolveGetHistoryForResourceType(Class<T> resourceClass, Date since, int count) {
186                return resolveGetHistoryForResourceType(resourceClass, since.toString(), count);
187        }
188        
189        public <T extends Resource> URI resolveGetAllTags() {
190                return baseServiceUri.resolve("_tags");
191        }
192        
193        public <T extends Resource> URI resolveGetAllTagsForResourceType(Class<T> resourceClass) {
194                return baseServiceUri.resolve(nameForClass(resourceClass) + "/_tags");
195        }
196        
197        public <T extends Resource> URI resolveGetTagsForReference(Class<T> resourceClass, String id) {
198                return baseServiceUri.resolve(nameForClass(resourceClass) + "/" + id + "/_tags");
199        }
200        
201        public <T extends Resource> URI resolveGetTagsForResourceVersion(Class<T> resourceClass, String id, String version) {
202                return baseServiceUri.resolve(nameForClass(resourceClass) +"/"+id+"/_history/"+version + "/_tags");
203        }
204        
205        public <T extends Resource> URI resolveDeleteTagsForResourceVersion(Class<T> resourceClass, String id, String version) {
206                return baseServiceUri.resolve(nameForClass(resourceClass) +"/"+id+"/_history/"+version + "/_tags/_delete");
207        }
208        
209
210        public <T extends Resource> String nameForClass(Class<T> resourceClass) {
211          if (resourceClass == null)
212            return null;
213                String res = resourceClass.getSimpleName();
214                if (res.equals("List_"))
215                        return "List";
216                else
217                        return res;
218        }
219        
220  public URI resolveMetadataUri(boolean quick) {
221    return baseServiceUri.resolve(quick ? "metadata?_summary=true" : "metadata");
222  }
223  
224  public URI resolveMetadataTxCaps() {
225    return baseServiceUri.resolve("metadata?mode=terminology");
226  }
227  
228        /**
229         * For now, assume this type of location header structure.
230         * Generalize later: http://hl7connect.healthintersections.com.au/svc/fhir/318/_history/1
231         * 
232         * @param serviceBase
233         * @param locationHeader
234         */
235        public static ResourceAddress.ResourceVersionedIdentifier parseCreateLocation(String locationResponseHeader) {
236                Pattern pattern = Pattern.compile(REGEX_ID_WITH_HISTORY);
237                Matcher matcher = pattern.matcher(locationResponseHeader);
238                ResourceVersionedIdentifier parsedHeader = null;
239                if(matcher.matches()){
240                        String serviceRoot = matcher.group(1);
241                        String resourceType = matcher.group(3);
242                        String id = matcher.group(5);
243                        String version = matcher.group(7);
244                        parsedHeader = new ResourceVersionedIdentifier(serviceRoot, resourceType, id, version);
245                }
246                return parsedHeader;
247        }
248        
249        public static URI buildAbsoluteURI(String absoluteURI) {
250                
251                if(StringUtils.isBlank(absoluteURI)) {
252                        throw new EFhirClientException("Invalid URI", new URISyntaxException(absoluteURI, "URI/URL cannot be blank"));
253                } 
254                
255                String endpoint = appendForwardSlashToPath(absoluteURI);
256
257                return buildEndpointUriFromString(endpoint);
258        }
259        
260        public static String appendForwardSlashToPath(String path) {
261                if(path.lastIndexOf('/') != path.length() - 1) {
262                        path += "/";
263                }
264                return path;
265        }
266        
267        public static URI buildEndpointUriFromString(String endpointPath) {
268                URI uri = null; 
269                try {
270                        URIBuilder uriBuilder = new URIBuilder(endpointPath);
271                        uri = uriBuilder.build();
272                        String scheme = uri.getScheme();
273                        String host = uri.getHost();
274                        if(!scheme.equalsIgnoreCase("http") && !scheme.equalsIgnoreCase("https")) {
275                                throw new EFhirClientException("Scheme must be 'http' or 'https': " + uri);
276                        }
277                        if(StringUtils.isBlank(host)) {
278                                throw new EFhirClientException("host cannot be blank: " + uri);
279                        }
280                } catch(URISyntaxException e) {
281                        throw new EFhirClientException("Invalid URI", e);
282                }
283                return uri;
284        }
285        
286        public static URI appendQueryStringToUri(URI uri, String parameterName, String parameterValue) {
287                URI modifiedUri = null;
288                try {
289                        URIBuilder uriBuilder = new URIBuilder(uri);
290                        uriBuilder.setQuery(parameterName + "=" + parameterValue);
291                        modifiedUri = uriBuilder.build();
292                } catch(Exception e) {
293                        throw new EFhirClientException("Unable to append query parameter '" + parameterName + "=" + parameterValue + " to URI " + uri, e);
294                }
295                return modifiedUri;
296        }
297        
298        public static String buildRelativePathFromResourceType(ResourceType resourceType) {
299                //return resourceType.toString().toLowerCase()+"/";
300                return resourceType.toString() + "/";
301        }
302        
303        public static String buildRelativePathFromResourceType(ResourceType resourceType, String id) {
304                return buildRelativePathFromResourceType(resourceType)+ "@" + id;
305        }
306        
307        public static String buildRelativePathFromReference(Resource resource) {
308                return buildRelativePathFromResourceType(resource.getResourceType());
309        }
310        
311        public static String buildRelativePathFromReference(Resource resource, String id) {
312                return buildRelativePathFromResourceType(resource.getResourceType(), id);
313        }
314        
315        public static class ResourceVersionedIdentifier {
316                
317                private String serviceRoot;
318                private String resourceType;
319                private String id;
320                private String version;
321                private URI resourceLocation;
322                
323                public ResourceVersionedIdentifier(String serviceRoot, String resourceType, String id, String version, URI resourceLocation) {
324                        this.serviceRoot = serviceRoot;
325                        this.resourceType = resourceType;
326                        this.id = id;
327                        this.version = version;
328                        this.resourceLocation = resourceLocation;
329                }
330                
331                public ResourceVersionedIdentifier(String resourceType, String id, String version, URI resourceLocation) {
332                        this(null, resourceType, id, version, resourceLocation);
333                }
334                
335                public ResourceVersionedIdentifier(String serviceRoot, String resourceType, String id, String version) {
336                        this(serviceRoot, resourceType, id, version, null);
337                }
338                
339                public ResourceVersionedIdentifier(String resourceType, String id, String version) {
340                        this(null, resourceType, id, version, null);
341                }
342                
343                public ResourceVersionedIdentifier(String resourceType, String id) {
344                        this.id = id;
345                }
346                
347                public String getId() {
348                        return this.id;
349                }
350                
351                protected void setId(String id) {
352                        this.id = id;
353                }
354                
355                public String getVersionId() {
356                        return this.version;
357                }
358                
359                protected void setVersionId(String version) {
360                        this.version = version;
361                }
362                
363                public String getResourceType() {
364                        return resourceType;
365                }
366
367                public void setResourceType(String resourceType) {
368                        this.resourceType = resourceType;
369                }
370                
371                public String getServiceRoot() {
372                        return serviceRoot;
373                }
374
375                public void setServiceRoot(String serviceRoot) {
376                        this.serviceRoot = serviceRoot;
377                }
378                
379                public String getResourcePath() {
380                        return this.serviceRoot + "/" + this.resourceType + "/" + this.id;
381                }
382
383                public String getVersion() {
384                        return version;
385                }
386
387                public void setVersion(String version) {
388                        this.version = version;
389                }
390
391                public URI getResourceLocation() {
392                        return this.resourceLocation;
393                }
394                
395                public void setResourceLocation(URI resourceLocation) {
396                        this.resourceLocation = resourceLocation;
397                }
398        }
399        
400        public static String getCalendarDateInIsoTimeFormat(Calendar calendar) {
401                SimpleDateFormat format = new SimpleDateFormat("yyyy-MM-dd'T'hh:mm:ss", new Locale("en", "US"));//TODO Move out
402                format.setTimeZone(TimeZone.getTimeZone("GMT"));
403            return format.format(calendar.getTime());
404        }
405        
406        public static URI appendHttpParameter(URI basePath, String httpParameterName, String httpParameterValue) {
407                Map<String, String> parameters = new HashMap<String, String>();
408                parameters.put(httpParameterName, httpParameterValue);
409                return appendHttpParameters(basePath, parameters);
410        }
411        
412        public static URI appendHttpParameters(URI basePath, Map<String,String> parameters) {
413        try {
414                Set<String> httpParameterNames = parameters.keySet();
415                String query = basePath.getQuery();
416                
417                for(String httpParameterName : httpParameterNames) {
418                        if(query != null) {
419                                query += "&";
420                        } else {
421                                query = "";
422                        }
423                        query += httpParameterName + "=" + Utilities.encodeUri(parameters.get(httpParameterName));
424                }
425        
426                return new URI(basePath.getScheme(), basePath.getUserInfo(), basePath.getHost(),basePath.getPort(), basePath.getPath(), query, basePath.getFragment());
427        } catch(Exception e) {
428                throw new EFhirClientException("Error appending http parameter", e);
429        }
430    }
431        
432}