
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}