
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}