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