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