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