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