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