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