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);
105  }
106
107  public <T extends Resource> URI resolveGetUriFromResourceClass(Class<T> resourceClass) {
108    return baseServiceUri.resolve(nameForClass(resourceClass));
109  }
110
111  public <T extends Resource> URI resolveGetUriFromResourceClassAndId(Class<T> resourceClass, String id) {
112    return baseServiceUri.resolve(nameForClass(resourceClass) + "/" + id);
113  }
114
115  public URI resolveGetUriFromResourceClassAndId(String resourceClass, String id) {
116    return baseServiceUri.resolve(resourceClass + "/" + id);
117  }
118
119  public <T extends Resource> URI resolveGetUriFromResourceClassAndIdAndVersion(Class<T> resourceClass, String id,
120      String version) {
121    return baseServiceUri.resolve(nameForClass(resourceClass) + "/" + id + "/_history/" + version);
122  }
123
124  public <T extends Resource> URI resolveGetUriFromResourceClassAndCanonical(Class<T> resourceClass,
125      String canonicalUrl) {
126    if (canonicalUrl.contains("|"))
127      return baseServiceUri
128          .resolve(nameForClass(resourceClass) + "?url=" + canonicalUrl.substring(0, canonicalUrl.indexOf("|"))
129              + "&version=" + canonicalUrl.substring(canonicalUrl.indexOf("|") + 1));
130    else
131      return baseServiceUri.resolve(nameForClass(resourceClass) + "?url=" + canonicalUrl);
132  }
133
134  public URI resolveGetHistoryForAllResources(int count) {
135    if (count > 0) {
136      return appendHttpParameter(baseServiceUri.resolve("_history"), "_count", "" + count);
137    } else {
138      return baseServiceUri.resolve("_history");
139    }
140  }
141
142  public <T extends Resource> URI resolveGetHistoryForResourceId(Class<T> resourceClass, String id, int count) {
143    return resolveGetHistoryUriForResourceId(resourceClass, id, null, count);
144  }
145
146  protected <T extends Resource> URI resolveGetHistoryUriForResourceId(Class<T> resourceClass, String id, Object since,
147      int count) {
148    Map<String, String> parameters = getHistoryParameters(since, count);
149    return appendHttpParameters(baseServiceUri.resolve(nameForClass(resourceClass) + "/" + id + "/_history"),
150        parameters);
151  }
152
153  public <T extends Resource> URI resolveGetHistoryForResourceType(Class<T> resourceClass, int count) {
154    Map<String, String> parameters = getHistoryParameters(null, count);
155    return appendHttpParameters(baseServiceUri.resolve(nameForClass(resourceClass) + "/_history"), parameters);
156  }
157
158  public <T extends Resource> URI resolveGetHistoryForResourceType(Class<T> resourceClass, Object since, int count) {
159    Map<String, String> parameters = getHistoryParameters(since, count);
160    return appendHttpParameters(baseServiceUri.resolve(nameForClass(resourceClass) + "/_history"), parameters);
161  }
162
163  public URI resolveGetHistoryForAllResources(Calendar since, int count) {
164    Map<String, String> parameters = getHistoryParameters(since, count);
165    return appendHttpParameters(baseServiceUri.resolve("_history"), parameters);
166  }
167
168  public URI resolveGetHistoryForAllResources(Date since, int count) {
169    Map<String, String> parameters = getHistoryParameters(since, count);
170    return appendHttpParameters(baseServiceUri.resolve("_history"), parameters);
171  }
172
173  public Map<String, String> getHistoryParameters(Object since, int count) {
174    Map<String, String> parameters = new HashMap<String, String>();
175    if (since != null) {
176      parameters.put("_since", since.toString());
177    }
178    if (count > 0) {
179      parameters.put("_count", "" + count);
180    }
181    return parameters;
182  }
183
184  public <T extends Resource> URI resolveGetHistoryForResourceId(Class<T> resourceClass, String id, Calendar since,
185      int count) {
186    return resolveGetHistoryUriForResourceId(resourceClass, id, since, count);
187  }
188
189  public <T extends Resource> URI resolveGetHistoryForResourceId(Class<T> resourceClass, String id, Date since,
190      int count) {
191    return resolveGetHistoryUriForResourceId(resourceClass, id, since, count);
192  }
193
194  public <T extends Resource> URI resolveGetHistoryForResourceType(Class<T> resourceClass, Calendar since, int count) {
195    return resolveGetHistoryForResourceType(resourceClass, getCalendarDateInIsoTimeFormat(since), count);
196  }
197
198  public <T extends Resource> URI resolveGetHistoryForResourceType(Class<T> resourceClass, Date since, int count) {
199    return resolveGetHistoryForResourceType(resourceClass, since.toString(), count);
200  }
201
202  public <T extends Resource> URI resolveGetAllTags() {
203    return baseServiceUri.resolve("_tags");
204  }
205
206  public <T extends Resource> URI resolveGetAllTagsForResourceType(Class<T> resourceClass) {
207    return baseServiceUri.resolve(nameForClass(resourceClass) + "/_tags");
208  }
209
210  public <T extends Resource> URI resolveGetTagsForReference(Class<T> resourceClass, String id) {
211    return baseServiceUri.resolve(nameForClass(resourceClass) + "/" + id + "/_tags");
212  }
213
214  public <T extends Resource> URI resolveGetTagsForResourceVersion(Class<T> resourceClass, String id, String version) {
215    return baseServiceUri.resolve(nameForClass(resourceClass) + "/" + id + "/_history/" + version + "/_tags");
216  }
217
218  public <T extends Resource> URI resolveDeleteTagsForResourceVersion(Class<T> resourceClass, String id,
219      String version) {
220    return baseServiceUri.resolve(nameForClass(resourceClass) + "/" + id + "/_history/" + version + "/_tags/_delete");
221  }
222
223  public <T extends Resource> String nameForClass(Class<T> resourceClass) {
224    if (resourceClass == null)
225      return null;
226    String res = resourceClass.getSimpleName();
227    if (res.equals("List_"))
228      return "List";
229    else
230      return res;
231  }
232
233  public URI resolveMetadataUri(boolean quick) {
234    return baseServiceUri.resolve(quick ? "metadata?_summary=true" : "metadata");
235  }
236
237  public URI resolveMetadataTxCaps() {
238    return baseServiceUri.resolve("metadata?mode=terminology");
239  }
240
241  /**
242   * For now, assume this type of location header structure. Generalize later:
243   * http://hl7connect.healthintersections.com.au/svc/fhir/318/_history/1
244   */
245  public static ResourceVersionedIdentifier parseCreateLocation(String locationResponseHeader) {
246    Pattern pattern = Pattern.compile(REGEX_ID_WITH_HISTORY);
247    Matcher matcher = pattern.matcher(locationResponseHeader);
248    ResourceVersionedIdentifier parsedHeader = null;
249    if (matcher.matches()) {
250      String serviceRoot = matcher.group(1);
251      String resourceType = matcher.group(3);
252      String id = matcher.group(5);
253      String version = matcher.group(7);
254      parsedHeader = new ResourceVersionedIdentifier(serviceRoot, resourceType, id, version);
255    }
256    return parsedHeader;
257  }
258
259  public static URI buildAbsoluteURI(String absoluteURI) {
260
261    if (StringUtils.isBlank(absoluteURI)) {
262      throw new EFhirClientException(0, "Invalid URI", new URISyntaxException(absoluteURI, "URI/URL cannot be blank"));
263    }
264
265    String endpoint = appendForwardSlashToPath(absoluteURI);
266
267    return buildEndpointUriFromString(endpoint);
268  }
269
270  public static String appendForwardSlashToPath(String path) {
271    if (path.lastIndexOf('/') != path.length() - 1) {
272      path += "/";
273    }
274    return path;
275  }
276
277  public static URI buildEndpointUriFromString(String endpointPath) {
278    URI uri = null;
279    try {
280      URIBuilder uriBuilder = new URIBuilder(endpointPath);
281      uri = uriBuilder.build();
282      String scheme = uri.getScheme();
283      String host = uri.getHost();
284      if (!scheme.equalsIgnoreCase("http") && !scheme.equalsIgnoreCase("https")) {
285        throw new EFhirClientException("Scheme must be 'http' or 'https': " + uri);
286      }
287      if (StringUtils.isBlank(host)) {
288        throw new EFhirClientException("host cannot be blank: " + uri);
289      }
290    } catch (URISyntaxException e) {
291      throw new EFhirClientException(0, "Invalid URI", e);
292    }
293    return uri;
294  }
295
296  public static URI appendQueryStringToUri(URI uri, String parameterName, String parameterValue) {
297    URI modifiedUri = null;
298    try {
299      URIBuilder uriBuilder = new URIBuilder(uri);
300      uriBuilder.setQuery(parameterName + "=" + parameterValue);
301      modifiedUri = uriBuilder.build();
302    } catch (Exception e) {
303      throw new EFhirClientException(0, 
304          "Unable to append query parameter '" + parameterName + "=" + parameterValue + " to URI " + uri, e);
305    }
306    return modifiedUri;
307  }
308
309  public static String buildRelativePathFromResourceType(ResourceType resourceType) {
310    // return resourceType.toString().toLowerCase()+"/";
311    return resourceType.toString() + "/";
312  }
313
314  public static String buildRelativePathFromResourceType(ResourceType resourceType, String id) {
315    return buildRelativePathFromResourceType(resourceType) + "@" + id;
316  }
317
318  public static String buildRelativePathFromReference(Resource resource) {
319    return buildRelativePathFromResourceType(resource.getResourceType());
320  }
321
322  public static String buildRelativePathFromReference(Resource resource, String id) {
323    return buildRelativePathFromResourceType(resource.getResourceType(), id);
324  }
325
326  public static class ResourceVersionedIdentifier {
327
328    private String serviceRoot;
329    private String resourceType;
330    private String id;
331    private String version;
332    private URI resourceLocation;
333
334    public ResourceVersionedIdentifier(String serviceRoot, String resourceType, String id, String version,
335        URI resourceLocation) {
336      this.serviceRoot = serviceRoot;
337      this.resourceType = resourceType;
338      this.id = id;
339      this.version = version;
340      this.resourceLocation = resourceLocation;
341    }
342
343    public ResourceVersionedIdentifier(String resourceType, String id, String version, URI resourceLocation) {
344      this(null, resourceType, id, version, resourceLocation);
345    }
346
347    public ResourceVersionedIdentifier(String serviceRoot, String resourceType, String id, String version) {
348      this(serviceRoot, resourceType, id, version, null);
349    }
350
351    public ResourceVersionedIdentifier(String resourceType, String id, String version) {
352      this(null, resourceType, id, version, null);
353    }
354
355    public ResourceVersionedIdentifier(String resourceType, String id) {
356      this.id = id;
357    }
358
359    public String getId() {
360      return this.id;
361    }
362
363    protected void setId(String id) {
364      this.id = id;
365    }
366
367    public String getVersionId() {
368      return this.version;
369    }
370
371    protected void setVersionId(String version) {
372      this.version = version;
373    }
374
375    public String getResourceType() {
376      return resourceType;
377    }
378
379    public void setResourceType(String resourceType) {
380      this.resourceType = resourceType;
381    }
382
383    public String getServiceRoot() {
384      return serviceRoot;
385    }
386
387    public void setServiceRoot(String serviceRoot) {
388      this.serviceRoot = serviceRoot;
389    }
390
391    public String getResourcePath() {
392      return this.serviceRoot + "/" + this.resourceType + "/" + this.id;
393    }
394
395    public String getVersion() {
396      return version;
397    }
398
399    public void setVersion(String version) {
400      this.version = version;
401    }
402
403    public URI getResourceLocation() {
404      return this.resourceLocation;
405    }
406
407    public void setResourceLocation(URI resourceLocation) {
408      this.resourceLocation = resourceLocation;
409    }
410  }
411
412  public static String getCalendarDateInIsoTimeFormat(Calendar calendar) {
413    SimpleDateFormat format = new SimpleDateFormat("yyyy-MM-dd'T'hh:mm:ss", new Locale("en", "US"));// TODO Move out
414    format.setTimeZone(TimeZone.getTimeZone("GMT"));
415    return format.format(calendar.getTime());
416  }
417
418  public static URI appendHttpParameter(URI basePath, String httpParameterName, String httpParameterValue) {
419    Map<String, String> parameters = new HashMap<String, String>();
420    parameters.put(httpParameterName, httpParameterValue);
421    return appendHttpParameters(basePath, parameters);
422  }
423
424  public static URI appendHttpParameters(URI basePath, Map<String, String> parameters) {
425    try {
426
427      List<NameValuePair> existingParams = URLEncodedUtils.parse(basePath.getQuery(), StandardCharsets.UTF_8);
428
429      URIBuilder uriBuilder = new URIBuilder()
430        .setScheme(basePath.getScheme())
431        .setHost(basePath.getHost())
432        .setPort(basePath.getPort())
433        .setUserInfo(basePath.getUserInfo())
434        .setFragment(basePath.getFragment());
435      for (NameValuePair pair : existingParams) {
436        uriBuilder.addParameter(pair.getName(), pair.getValue());
437      }
438      for (Map.Entry<String, String> entry : parameters.entrySet()) {
439        uriBuilder.addParameter(entry.getKey(), entry.getValue());
440      }
441      uriBuilder.setPath(basePath.getPath());
442
443      return uriBuilder.build();
444
445    } catch (Exception e) {
446      throw new EFhirClientException(0, "Error appending http parameter", e);
447    }
448  }
449}