
001package ca.uhn.fhir.rest.server; 002 003/* 004 * #%L 005 * HAPI FHIR - Server Framework 006 * %% 007 * Copyright (C) 2014 - 2022 Smile CDR, Inc. 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 023import ca.uhn.fhir.i18n.Msg; 024import ca.uhn.fhir.context.FhirContext; 025import ca.uhn.fhir.context.FhirVersionEnum; 026import ca.uhn.fhir.interceptor.api.HookParams; 027import ca.uhn.fhir.interceptor.api.Pointcut; 028import ca.uhn.fhir.model.api.IResource; 029import ca.uhn.fhir.model.api.Include; 030import ca.uhn.fhir.model.api.ResourceMetadataKeyEnum; 031import ca.uhn.fhir.model.primitive.InstantDt; 032import ca.uhn.fhir.parser.IParser; 033import ca.uhn.fhir.rest.api.BundleLinks; 034import ca.uhn.fhir.rest.api.Constants; 035import ca.uhn.fhir.rest.api.DeleteCascadeModeEnum; 036import ca.uhn.fhir.rest.api.EncodingEnum; 037import ca.uhn.fhir.rest.api.PreferHandlingEnum; 038import ca.uhn.fhir.rest.api.PreferHeader; 039import ca.uhn.fhir.rest.api.PreferReturnEnum; 040import ca.uhn.fhir.rest.api.RequestTypeEnum; 041import ca.uhn.fhir.rest.api.RestOperationTypeEnum; 042import ca.uhn.fhir.rest.api.SummaryEnum; 043import ca.uhn.fhir.rest.api.server.IRestfulResponse; 044import ca.uhn.fhir.rest.api.server.IRestfulServer; 045import ca.uhn.fhir.rest.api.server.RequestDetails; 046import ca.uhn.fhir.rest.server.exceptions.InternalErrorException; 047import ca.uhn.fhir.rest.server.exceptions.InvalidRequestException; 048import ca.uhn.fhir.rest.server.method.ElementsParameter; 049import ca.uhn.fhir.rest.server.method.SummaryEnumParameter; 050import ca.uhn.fhir.rest.server.servlet.ServletRequestDetails; 051import ca.uhn.fhir.util.BinaryUtil; 052import ca.uhn.fhir.util.DateUtils; 053import ca.uhn.fhir.util.UrlUtil; 054import com.google.common.collect.Maps; 055import com.google.common.collect.Sets; 056import org.hl7.fhir.instance.model.api.IAnyResource; 057import org.hl7.fhir.instance.model.api.IBaseBinary; 058import org.hl7.fhir.instance.model.api.IBaseReference; 059import org.hl7.fhir.instance.model.api.IBaseResource; 060import org.hl7.fhir.instance.model.api.IDomainResource; 061import org.hl7.fhir.instance.model.api.IIdType; 062import org.hl7.fhir.instance.model.api.IPrimitiveType; 063 064import javax.annotation.Nonnull; 065import javax.annotation.Nullable; 066import javax.servlet.http.HttpServletRequest; 067import java.io.IOException; 068import java.io.Writer; 069import java.util.Arrays; 070import java.util.Collections; 071import java.util.Date; 072import java.util.EnumSet; 073import java.util.Enumeration; 074import java.util.HashMap; 075import java.util.HashSet; 076import java.util.Iterator; 077import java.util.List; 078import java.util.Map; 079import java.util.Set; 080import java.util.StringTokenizer; 081import java.util.TreeSet; 082import java.util.regex.Matcher; 083import java.util.regex.Pattern; 084import java.util.stream.Collectors; 085 086import static org.apache.commons.lang3.StringUtils.isBlank; 087import static org.apache.commons.lang3.StringUtils.isNotBlank; 088import static org.apache.commons.lang3.StringUtils.replace; 089import static org.apache.commons.lang3.StringUtils.trim; 090 091public class RestfulServerUtils { 092 static final Pattern ACCEPT_HEADER_PATTERN = Pattern.compile("\\s*([a-zA-Z0-9+.*/-]+)\\s*(;\\s*([a-zA-Z]+)\\s*=\\s*([a-zA-Z0-9.]+)\\s*)?(,?)"); 093 094 private static final org.slf4j.Logger ourLog = org.slf4j.LoggerFactory.getLogger(RestfulServerUtils.class); 095 096 private static final HashSet<String> TEXT_ENCODE_ELEMENTS = new HashSet<>(Arrays.asList("*.text", "*.id", "*.meta", "*.(mandatory)")); 097 private static Map<FhirVersionEnum, FhirContext> myFhirContextMap = Collections.synchronizedMap(new HashMap<>()); 098 private static EnumSet<RestOperationTypeEnum> ourOperationsWhichAllowPreferHeader = EnumSet.of(RestOperationTypeEnum.CREATE, RestOperationTypeEnum.UPDATE, RestOperationTypeEnum.PATCH); 099 100 private enum NarrativeModeEnum { 101 NORMAL, ONLY, SUPPRESS; 102 103 public static NarrativeModeEnum valueOfCaseInsensitive(String theCode) { 104 return valueOf(NarrativeModeEnum.class, theCode.toUpperCase()); 105 } 106 } 107 108 /** 109 * Return type for {@link RestfulServerUtils#determineRequestEncodingNoDefault(RequestDetails)} 110 */ 111 public static class ResponseEncoding { 112 private final String myContentType; 113 private final EncodingEnum myEncoding; 114 private final Boolean myNonLegacy; 115 116 public ResponseEncoding(FhirContext theCtx, EncodingEnum theEncoding, String theContentType) { 117 super(); 118 myEncoding = theEncoding; 119 myContentType = theContentType; 120 if (theContentType != null) { 121 FhirVersionEnum ctxtEnum = theCtx.getVersion().getVersion(); 122 if (theContentType.equals(EncodingEnum.JSON_PLAIN_STRING) || theContentType.equals(EncodingEnum.XML_PLAIN_STRING)) { 123 myNonLegacy = ctxtEnum.isNewerThan(FhirVersionEnum.DSTU2_1); 124 } else { 125 myNonLegacy = ctxtEnum.isNewerThan(FhirVersionEnum.DSTU2_1) && !EncodingEnum.isLegacy(theContentType); 126 } 127 } else { 128 FhirVersionEnum ctxtEnum = theCtx.getVersion().getVersion(); 129 if (ctxtEnum.isOlderThan(FhirVersionEnum.DSTU3)) { 130 myNonLegacy = null; 131 } else { 132 myNonLegacy = Boolean.TRUE; 133 } 134 } 135 } 136 137 public String getContentType() { 138 return myContentType; 139 } 140 141 public EncodingEnum getEncoding() { 142 return myEncoding; 143 } 144 145 public String getResourceContentType() { 146 if (Boolean.TRUE.equals(isNonLegacy())) { 147 return getEncoding().getResourceContentTypeNonLegacy(); 148 } 149 return getEncoding().getResourceContentType(); 150 } 151 152 Boolean isNonLegacy() { 153 return myNonLegacy; 154 } 155 } 156 157 public static void configureResponseParser(RequestDetails theRequestDetails, IParser parser) { 158 // Pretty print 159 boolean prettyPrint = RestfulServerUtils.prettyPrintResponse(theRequestDetails.getServer(), theRequestDetails); 160 161 parser.setPrettyPrint(prettyPrint); 162 parser.setServerBaseUrl(theRequestDetails.getFhirServerBase()); 163 164 // Summary mode 165 Set<SummaryEnum> summaryMode = RestfulServerUtils.determineSummaryMode(theRequestDetails); 166 167 // _elements 168 Set<String> elements = ElementsParameter.getElementsValueOrNull(theRequestDetails, false); 169 if (elements != null && !summaryMode.equals(Collections.singleton(SummaryEnum.FALSE))) { 170 throw new InvalidRequestException(Msg.code(304) + "Cannot combine the " + Constants.PARAM_SUMMARY + " and " + Constants.PARAM_ELEMENTS + " parameters"); 171 } 172 173 // _elements:exclude 174 Set<String> elementsExclude = ElementsParameter.getElementsValueOrNull(theRequestDetails, true); 175 if (elementsExclude != null) { 176 parser.setDontEncodeElements(elementsExclude); 177 } 178 179 boolean summaryModeCount = summaryMode.contains(SummaryEnum.COUNT) && summaryMode.size() == 1; 180 if (!summaryModeCount) { 181 String[] countParam = theRequestDetails.getParameters().get(Constants.PARAM_COUNT); 182 if (countParam != null && countParam.length > 0) { 183 summaryModeCount = "0".equalsIgnoreCase(countParam[0]); 184 } 185 } 186 187 if (summaryModeCount) { 188 parser.setEncodeElements(Sets.newHashSet("Bundle.total", "Bundle.type")); 189 } else if (summaryMode.contains(SummaryEnum.TEXT) && summaryMode.size() == 1) { 190 parser.setEncodeElements(TEXT_ENCODE_ELEMENTS); 191 parser.setEncodeElementsAppliesToChildResourcesOnly(true); 192 } else { 193 parser.setSuppressNarratives(summaryMode.contains(SummaryEnum.DATA)); 194 parser.setSummaryMode(summaryMode.contains(SummaryEnum.TRUE)); 195 } 196 197 if (elements != null && elements.size() > 0) { 198 String elementsAppliesTo = "*"; 199 if (isNotBlank(theRequestDetails.getResourceName())) { 200 elementsAppliesTo = theRequestDetails.getResourceName(); 201 } 202 203 Set<String> newElements = new HashSet<>(); 204 for (String next : elements) { 205 if (isNotBlank(next)) { 206 if (Character.isUpperCase(next.charAt(0))) { 207 newElements.add(next); 208 } else { 209 newElements.add(elementsAppliesTo + "." + next); 210 } 211 } 212 } 213 214 /* 215 * We try to be smart about what the user is asking for 216 * when they include an _elements parameter. If we're responding 217 * to something that returns a Bundle (e.g. a search) we assume 218 * the elements don't apply to the Bundle itself, unless 219 * the client has explicitly scoped the Bundle 220 * (i.e. with Bundle.total or something like that) 221 */ 222 boolean haveExplicitBundleElement = false; 223 for (String next : newElements) { 224 if (next.startsWith("Bundle.")) { 225 haveExplicitBundleElement = true; 226 break; 227 } 228 } 229 230 if (theRequestDetails.getRestOperationType() != null) { 231 switch (theRequestDetails.getRestOperationType()) { 232 case SEARCH_SYSTEM: 233 case SEARCH_TYPE: 234 case HISTORY_SYSTEM: 235 case HISTORY_TYPE: 236 case HISTORY_INSTANCE: 237 case GET_PAGE: 238 if (!haveExplicitBundleElement) { 239 parser.setEncodeElementsAppliesToChildResourcesOnly(true); 240 } 241 break; 242 default: 243 break; 244 } 245 } 246 247 parser.setEncodeElements(newElements); 248 } 249 } 250 251 252 public static String createLinkSelf(String theServerBase, RequestDetails theRequest) { 253 return createLinkSelfWithoutGivenParameters(theServerBase, theRequest, null); 254 } 255 256 /** 257 * This function will create a self link but omit any parameters passed in via the excludedParameterNames list. 258 */ 259 public static String createLinkSelfWithoutGivenParameters(String theServerBase, RequestDetails theRequest, List<String> excludedParameterNames) { 260 StringBuilder b = new StringBuilder(); 261 b.append(theServerBase); 262 263 if (isNotBlank(theRequest.getRequestPath())) { 264 b.append('/'); 265 if (isNotBlank(theRequest.getTenantId()) && theRequest.getRequestPath().startsWith(theRequest.getTenantId() + "/")) { 266 b.append(theRequest.getRequestPath().substring(theRequest.getTenantId().length() + 1)); 267 } else { 268 b.append(theRequest.getRequestPath()); 269 } 270 } 271 // For POST the URL parameters get jumbled with the post body parameters so don't include them, they might be huge 272 if (theRequest.getRequestType() == RequestTypeEnum.GET) { 273 boolean first = true; 274 Map<String, String[]> parameters = theRequest.getParameters(); 275 for (String nextParamName : new TreeSet<>(parameters.keySet())) { 276 if (excludedParameterNames == null || !excludedParameterNames.contains(nextParamName)) { 277 for (String nextParamValue : parameters.get(nextParamName)) { 278 if (first) { 279 b.append('?'); 280 first = false; 281 } else { 282 b.append('&'); 283 } 284 b.append(UrlUtil.escapeUrlParam(nextParamName)); 285 b.append('='); 286 b.append(UrlUtil.escapeUrlParam(nextParamValue)); 287 } 288 } 289 } 290 } 291 292 return b.toString(); 293 294 } 295 296 public static String createOffsetPagingLink(BundleLinks theBundleLinks, String requestPath, String tenantId, Integer theOffset, Integer theCount, Map<String, String[]> theRequestParameters) { 297 StringBuilder b = new StringBuilder(); 298 b.append(theBundleLinks.serverBase); 299 300 if (isNotBlank(requestPath)) { 301 b.append('/'); 302 if (isNotBlank(tenantId) && requestPath.startsWith(tenantId + "/")) { 303 b.append(requestPath.substring(tenantId.length() + 1)); 304 } else { 305 b.append(requestPath); 306 } 307 } 308 309 Map<String, String[]> params = Maps.newLinkedHashMap(theRequestParameters); 310 params.put(Constants.PARAM_OFFSET, new String[]{String.valueOf(theOffset)}); 311 params.put(Constants.PARAM_COUNT, new String[]{String.valueOf(theCount)}); 312 313 boolean first = true; 314 for (String nextParamName : new TreeSet<>(params.keySet())) { 315 for (String nextParamValue : params.get(nextParamName)) { 316 if (first) { 317 b.append('?'); 318 first = false; 319 } else { 320 b.append('&'); 321 } 322 b.append(UrlUtil.escapeUrlParam(nextParamName)); 323 b.append('='); 324 b.append(UrlUtil.escapeUrlParam(nextParamValue)); 325 } 326 } 327 328 return b.toString(); 329 } 330 331 public static String createPagingLink(BundleLinks theBundleLinks, RequestDetails theRequestDetails, String theSearchId, int theOffset, int theCount, Map<String, String[]> theRequestParameters) { 332 return createPagingLink(theBundleLinks, theRequestDetails, theSearchId, theOffset, theCount, theRequestParameters, null); 333 } 334 335 public static String createPagingLink(BundleLinks theBundleLinks, RequestDetails theRequestDetails, String theSearchId, String thePageId, Map<String, String[]> theRequestParameters) { 336 return createPagingLink(theBundleLinks, theRequestDetails, theSearchId, null, null, theRequestParameters, 337 thePageId); 338 } 339 340 private static String createPagingLink(BundleLinks theBundleLinks, RequestDetails theRequestDetails, String theSearchId, Integer theOffset, Integer theCount, Map<String, String[]> theRequestParameters, 341 String thePageId) { 342 343 String serverBase = theRequestDetails.getFhirServerBase(); 344 345 StringBuilder b = new StringBuilder(); 346 b.append(serverBase); 347 b.append('?'); 348 b.append(Constants.PARAM_PAGINGACTION); 349 b.append('='); 350 b.append(UrlUtil.escapeUrlParam(theSearchId)); 351 352 if (theOffset != null) { 353 b.append('&'); 354 b.append(Constants.PARAM_PAGINGOFFSET); 355 b.append('='); 356 b.append(theOffset); 357 } 358 if (theCount != null) { 359 b.append('&'); 360 b.append(Constants.PARAM_COUNT); 361 b.append('='); 362 b.append(theCount); 363 } 364 if (isNotBlank(thePageId)) { 365 b.append('&'); 366 b.append(Constants.PARAM_PAGEID); 367 b.append('='); 368 b.append(UrlUtil.escapeUrlParam(thePageId)); 369 } 370 String[] strings = theRequestParameters.get(Constants.PARAM_FORMAT); 371 if (strings != null && strings.length > 0) { 372 b.append('&'); 373 b.append(Constants.PARAM_FORMAT); 374 b.append('='); 375 String format = strings[0]; 376 format = replace(format, " ", "+"); 377 b.append(UrlUtil.escapeUrlParam(format)); 378 } 379 if (theBundleLinks.prettyPrint) { 380 b.append('&'); 381 b.append(Constants.PARAM_PRETTY); 382 b.append('='); 383 b.append(Constants.PARAM_PRETTY_VALUE_TRUE); 384 } 385 386 if (theBundleLinks.getIncludes() != null) { 387 for (Include nextInclude : theBundleLinks.getIncludes()) { 388 if (isNotBlank(nextInclude.getValue())) { 389 b.append('&'); 390 b.append(Constants.PARAM_INCLUDE); 391 b.append('='); 392 b.append(UrlUtil.escapeUrlParam(nextInclude.getValue())); 393 } 394 } 395 } 396 397 if (theBundleLinks.bundleType != null) { 398 b.append('&'); 399 b.append(Constants.PARAM_BUNDLETYPE); 400 b.append('='); 401 b.append(theBundleLinks.bundleType.getCode()); 402 } 403 404 // _elements 405 Set<String> elements = ElementsParameter.getElementsValueOrNull(theRequestDetails, false); 406 if (elements != null) { 407 b.append('&'); 408 b.append(Constants.PARAM_ELEMENTS); 409 b.append('='); 410 String nextValue = elements 411 .stream() 412 .sorted() 413 .map(UrlUtil::escapeUrlParam) 414 .collect(Collectors.joining(",")); 415 b.append(nextValue); 416 } 417 418 // _elements:exclude 419 if (theRequestDetails.getServer().getElementsSupport() == ElementsSupportEnum.EXTENDED) { 420 Set<String> elementsExclude = ElementsParameter.getElementsValueOrNull(theRequestDetails, true); 421 if (elementsExclude != null) { 422 b.append('&'); 423 b.append(Constants.PARAM_ELEMENTS + Constants.PARAM_ELEMENTS_EXCLUDE_MODIFIER); 424 b.append('='); 425 String nextValue = elementsExclude 426 .stream() 427 .sorted() 428 .map(UrlUtil::escapeUrlParam) 429 .collect(Collectors.joining(",")); 430 b.append(nextValue); 431 } 432 } 433 434 return b.toString(); 435 } 436 437 @Nullable 438 public static EncodingEnum determineRequestEncodingNoDefault(RequestDetails theReq) { 439 return determineRequestEncodingNoDefault(theReq, false); 440 } 441 442 @Nullable 443 public static EncodingEnum determineRequestEncodingNoDefault(RequestDetails theReq, boolean theStrict) { 444 ResponseEncoding retVal = determineRequestEncodingNoDefaultReturnRE(theReq, theStrict); 445 if (retVal == null) { 446 return null; 447 } 448 return retVal.getEncoding(); 449 } 450 451 private static ResponseEncoding determineRequestEncodingNoDefaultReturnRE(RequestDetails theReq, boolean theStrict) { 452 ResponseEncoding retVal = null; 453 List<String> headers = theReq.getHeaders(Constants.HEADER_CONTENT_TYPE); 454 if (headers != null) { 455 Iterator<String> acceptValues = headers.iterator(); 456 if (acceptValues != null) { 457 while (acceptValues.hasNext() && retVal == null) { 458 String nextAcceptHeaderValue = acceptValues.next(); 459 if (nextAcceptHeaderValue != null && isNotBlank(nextAcceptHeaderValue)) { 460 for (String nextPart : nextAcceptHeaderValue.split(",")) { 461 int scIdx = nextPart.indexOf(';'); 462 if (scIdx == 0) { 463 continue; 464 } 465 if (scIdx != -1) { 466 nextPart = nextPart.substring(0, scIdx); 467 } 468 nextPart = nextPart.trim(); 469 EncodingEnum encoding; 470 if (theStrict) { 471 encoding = EncodingEnum.forContentTypeStrict(nextPart); 472 } else { 473 encoding = EncodingEnum.forContentType(nextPart); 474 } 475 if (encoding != null) { 476 retVal = new ResponseEncoding(theReq.getServer().getFhirContext(), encoding, nextPart); 477 break; 478 } 479 } 480 } 481 } 482 } 483 } 484 return retVal; 485 } 486 487 /** 488 * Returns null if the request doesn't express that it wants FHIR. If it expresses that it wants XML and JSON 489 * equally, returns thePrefer. 490 */ 491 public static ResponseEncoding determineResponseEncodingNoDefault(RequestDetails theReq, EncodingEnum thePrefer) { 492 return determineResponseEncodingNoDefault(theReq, thePrefer, null); 493 } 494 495 /** 496 * Try to determing the response content type, given the request Accept header and 497 * _format parameter. If a value is provided to thePreferContents, we'll 498 * prefer to return that value over the native FHIR value. 499 */ 500 public static ResponseEncoding determineResponseEncodingNoDefault(RequestDetails theReq, EncodingEnum thePrefer, String thePreferContentType) { 501 String[] format = theReq.getParameters().get(Constants.PARAM_FORMAT); 502 if (format != null) { 503 for (String nextFormat : format) { 504 EncodingEnum retVal = EncodingEnum.forContentType(nextFormat); 505 if (retVal != null) { 506 return new ResponseEncoding(theReq.getServer().getFhirContext(), retVal, nextFormat); 507 } 508 } 509 } 510 511 /* 512 * Some browsers (e.g. FF) request "application/xml" in their Accept header, 513 * and we generally want to treat this as a preference for FHIR XML even if 514 * it's not the FHIR version of the CT, which should be "application/xml+fhir". 515 * 516 * When we're serving up Binary resources though, we are a bit more strict, 517 * since Binary is supposed to use native content types unless the client has 518 * explicitly requested FHIR. 519 */ 520 boolean strict = false; 521 if ("Binary".equals(theReq.getResourceName())) { 522 strict = true; 523 } 524 525 /* 526 * The Accept header is kind of ridiculous, e.g. 527 */ 528 // text/xml, application/xml, application/xhtml+xml, text/html;q=0.9, text/plain;q=0.8, image/png, */*;q=0.5 529 530 List<String> acceptValues = theReq.getHeaders(Constants.HEADER_ACCEPT); 531 float bestQ = -1f; 532 ResponseEncoding retVal = null; 533 if (acceptValues != null) { 534 for (String nextAcceptHeaderValue : acceptValues) { 535 StringTokenizer tok = new StringTokenizer(nextAcceptHeaderValue, ","); 536 while (tok.hasMoreTokens()) { 537 String nextToken = tok.nextToken(); 538 int startSpaceIndex = -1; 539 for (int i = 0; i < nextToken.length(); i++) { 540 if (nextToken.charAt(i) != ' ') { 541 startSpaceIndex = i; 542 break; 543 } 544 } 545 546 if (startSpaceIndex == -1) { 547 continue; 548 } 549 550 int endSpaceIndex = -1; 551 for (int i = startSpaceIndex; i < nextToken.length(); i++) { 552 if (nextToken.charAt(i) == ' ' || nextToken.charAt(i) == ';') { 553 endSpaceIndex = i; 554 break; 555 } 556 } 557 558 float q = 1.0f; 559 ResponseEncoding encoding; 560 if (endSpaceIndex == -1) { 561 if (startSpaceIndex == 0) { 562 encoding = getEncodingForContentType(theReq.getServer().getFhirContext(), strict, nextToken, thePreferContentType); 563 } else { 564 encoding = getEncodingForContentType(theReq.getServer().getFhirContext(), strict, nextToken.substring(startSpaceIndex), thePreferContentType); 565 } 566 } else { 567 encoding = getEncodingForContentType(theReq.getServer().getFhirContext(), strict, nextToken.substring(startSpaceIndex, endSpaceIndex), thePreferContentType); 568 String remaining = nextToken.substring(endSpaceIndex + 1); 569 StringTokenizer qualifierTok = new StringTokenizer(remaining, ";"); 570 while (qualifierTok.hasMoreTokens()) { 571 String nextQualifier = qualifierTok.nextToken(); 572 int equalsIndex = nextQualifier.indexOf('='); 573 if (equalsIndex != -1) { 574 String nextQualifierKey = nextQualifier.substring(0, equalsIndex).trim(); 575 String nextQualifierValue = nextQualifier.substring(equalsIndex + 1, nextQualifier.length()).trim(); 576 if (nextQualifierKey.equals("q")) { 577 try { 578 q = Float.parseFloat(nextQualifierValue); 579 q = Math.max(q, 0.0f); 580 } catch (NumberFormatException e) { 581 ourLog.debug("Invalid Accept header q value: {}", nextQualifierValue); 582 } 583 } 584 } 585 } 586 } 587 588 if (encoding != null) { 589 if (q > bestQ || (q == bestQ && encoding.getEncoding() == thePrefer)) { 590 retVal = encoding; 591 bestQ = q; 592 } 593 } 594 595 } 596 597 } 598 599 } 600 601 /* 602 * If the client hasn't given any indication about which response 603 * encoding they want, let's try the request encoding in case that 604 * is useful (basically this catches the case where the request 605 * has a Content-Type header but not an Accept header) 606 */ 607 if (retVal == null) { 608 retVal = determineRequestEncodingNoDefaultReturnRE(theReq, strict); 609 } 610 611 return retVal; 612 } 613 614 /** 615 * Determine whether a response should be given in JSON or XML format based on the incoming HttpServletRequest's 616 * <code>"_format"</code> parameter and <code>"Accept:"</code> HTTP header. 617 */ 618 public static ResponseEncoding determineResponseEncodingWithDefault(RequestDetails theReq) { 619 ResponseEncoding retVal = determineResponseEncodingNoDefault(theReq, theReq.getServer().getDefaultResponseEncoding()); 620 if (retVal == null) { 621 retVal = new ResponseEncoding(theReq.getServer().getFhirContext(), theReq.getServer().getDefaultResponseEncoding(), null); 622 } 623 return retVal; 624 } 625 626 @Nonnull 627 public static Set<SummaryEnum> determineSummaryMode(RequestDetails theRequest) { 628 Map<String, String[]> requestParams = theRequest.getParameters(); 629 630 Set<SummaryEnum> retVal = SummaryEnumParameter.getSummaryValueOrNull(theRequest); 631 632 if (retVal == null) { 633 /* 634 * HAPI originally supported a custom parameter called _narrative, but this has been superceded by an official 635 * parameter called _summary 636 */ 637 String[] narrative = requestParams.get(Constants.PARAM_NARRATIVE); 638 if (narrative != null && narrative.length > 0) { 639 try { 640 NarrativeModeEnum narrativeMode = NarrativeModeEnum.valueOfCaseInsensitive(narrative[0]); 641 switch (narrativeMode) { 642 case NORMAL: 643 retVal = Collections.singleton(SummaryEnum.FALSE); 644 break; 645 case ONLY: 646 retVal = Collections.singleton(SummaryEnum.TEXT); 647 break; 648 case SUPPRESS: 649 retVal = Collections.singleton(SummaryEnum.DATA); 650 break; 651 } 652 } catch (IllegalArgumentException e) { 653 ourLog.debug("Invalid {} parameter: {}", Constants.PARAM_NARRATIVE, narrative[0]); 654 } 655 } 656 } 657 if (retVal == null) { 658 retVal = Collections.singleton(SummaryEnum.FALSE); 659 } 660 661 return retVal; 662 } 663 664 public static Integer extractCountParameter(RequestDetails theRequest) { 665 return RestfulServerUtils.tryToExtractNamedParameter(theRequest, Constants.PARAM_COUNT); 666 } 667 668 public static Integer extractOffsetParameter(RequestDetails theRequest) { 669 return RestfulServerUtils.tryToExtractNamedParameter(theRequest, Constants.PARAM_OFFSET); 670 } 671 672 public static IPrimitiveType<Date> extractLastUpdatedFromResource(IBaseResource theResource) { 673 IPrimitiveType<Date> lastUpdated = null; 674 if (theResource instanceof IResource) { 675 lastUpdated = ResourceMetadataKeyEnum.UPDATED.get((IResource) theResource); 676 } else if (theResource instanceof IAnyResource) { 677 lastUpdated = new InstantDt(theResource.getMeta().getLastUpdated()); 678 } 679 return lastUpdated; 680 } 681 682 public static IIdType fullyQualifyResourceIdOrReturnNull(IRestfulServerDefaults theServer, IBaseResource theResource, String theServerBase, IIdType theResourceId) { 683 IIdType retVal = null; 684 if (theResourceId.hasIdPart() && isNotBlank(theServerBase)) { 685 String resName = theResourceId.getResourceType(); 686 if (theResource != null && isBlank(resName)) { 687 FhirContext context = theServer.getFhirContext(); 688 context = getContextForVersion(context, theResource.getStructureFhirVersionEnum()); 689 resName = context.getResourceType(theResource); 690 } 691 if (isNotBlank(resName)) { 692 retVal = theResourceId.withServerBase(theServerBase, resName); 693 } 694 } 695 return retVal; 696 } 697 698 private static FhirContext getContextForVersion(FhirContext theContext, FhirVersionEnum theForVersion) { 699 FhirContext context = theContext; 700 if (context.getVersion().getVersion() != theForVersion) { 701 context = myFhirContextMap.get(theForVersion); 702 if (context == null) { 703 context = theForVersion.newContext(); 704 myFhirContextMap.put(theForVersion, context); 705 } 706 } 707 return context; 708 } 709 710 private static ResponseEncoding getEncodingForContentType(FhirContext theFhirContext, boolean theStrict, String theContentType, String thePreferContentType) { 711 EncodingEnum encoding; 712 if (theStrict) { 713 encoding = EncodingEnum.forContentTypeStrict(theContentType); 714 } else { 715 encoding = EncodingEnum.forContentType(theContentType); 716 } 717 if (isNotBlank(thePreferContentType)) { 718 if (thePreferContentType.equals(theContentType)) { 719 return new ResponseEncoding(theFhirContext, encoding, theContentType); 720 } 721 } 722 if (encoding == null) { 723 return null; 724 } 725 return new ResponseEncoding(theFhirContext, encoding, theContentType); 726 } 727 728 public static IParser getNewParser(FhirContext theContext, FhirVersionEnum theForVersion, RequestDetails theRequestDetails) { 729 FhirContext context = getContextForVersion(theContext, theForVersion); 730 731 // Determine response encoding 732 EncodingEnum responseEncoding = RestfulServerUtils.determineResponseEncodingWithDefault(theRequestDetails).getEncoding(); 733 IParser parser; 734 switch (responseEncoding) { 735 case JSON: 736 parser = context.newJsonParser(); 737 break; 738 case RDF: 739 parser = context.newRDFParser(); 740 break; 741 case XML: 742 default: 743 parser = context.newXmlParser(); 744 break; 745 } 746 747 configureResponseParser(theRequestDetails, parser); 748 749 return parser; 750 } 751 752 public static Set<String> parseAcceptHeaderAndReturnHighestRankedOptions(HttpServletRequest theRequest) { 753 Set<String> retVal = new HashSet<String>(); 754 755 Enumeration<String> acceptValues = theRequest.getHeaders(Constants.HEADER_ACCEPT); 756 if (acceptValues != null) { 757 float bestQ = -1f; 758 while (acceptValues.hasMoreElements()) { 759 String nextAcceptHeaderValue = acceptValues.nextElement(); 760 Matcher m = ACCEPT_HEADER_PATTERN.matcher(nextAcceptHeaderValue); 761 float q = 1.0f; 762 while (m.find()) { 763 String contentTypeGroup = m.group(1); 764 if (isNotBlank(contentTypeGroup)) { 765 766 String name = m.group(3); 767 String value = m.group(4); 768 if (name != null && value != null) { 769 if ("q".equals(name)) { 770 try { 771 q = Float.parseFloat(value); 772 q = Math.max(q, 0.0f); 773 } catch (NumberFormatException e) { 774 ourLog.debug("Invalid Accept header q value: {}", value); 775 } 776 } 777 } 778 779 if (q > bestQ) { 780 retVal.clear(); 781 bestQ = q; 782 } 783 784 if (q == bestQ) { 785 retVal.add(contentTypeGroup.trim()); 786 } 787 788 } 789 790 if (!",".equals(m.group(5))) { 791 break; 792 } 793 } 794 795 } 796 } 797 798 return retVal; 799 } 800 801 public static boolean respectPreferHeader(RestOperationTypeEnum theRestOperationType) { 802 return ourOperationsWhichAllowPreferHeader.contains(theRestOperationType); 803 } 804 805 @Nonnull 806 public static PreferHeader parsePreferHeader(IRestfulServer<?> theServer, String theValue) { 807 PreferHeader retVal = new PreferHeader(); 808 809 if (isNotBlank(theValue)) { 810 StringTokenizer tok = new StringTokenizer(theValue, ";,"); 811 while (tok.hasMoreTokens()) { 812 String next = trim(tok.nextToken()); 813 int eqIndex = next.indexOf('='); 814 815 String key; 816 String value; 817 if (eqIndex == -1 || eqIndex >= next.length() - 2) { 818 key = next; 819 value = ""; 820 } else { 821 key = next.substring(0, eqIndex).trim(); 822 value = next.substring(eqIndex + 1).trim(); 823 } 824 825 if (key.equals(Constants.HEADER_PREFER_RETURN)) { 826 827 value = cleanUpValue(value); 828 retVal.setReturn(PreferReturnEnum.fromHeaderValue(value)); 829 830 } else if (key.equals(Constants.HEADER_PREFER_HANDLING)) { 831 832 value = cleanUpValue(value); 833 retVal.setHanding(PreferHandlingEnum.fromHeaderValue(value)); 834 835 } else if (key.equals(Constants.HEADER_PREFER_RESPOND_ASYNC)) { 836 837 retVal.setRespondAsync(true); 838 839 } 840 } 841 } 842 843 if (retVal.getReturn() == null && theServer != null && theServer.getDefaultPreferReturn() != null) { 844 retVal.setReturn(theServer.getDefaultPreferReturn()); 845 } 846 847 return retVal; 848 } 849 850 private static String cleanUpValue(String value) { 851 if (value.length() < 2) { 852 value = ""; 853 } 854 if ('"' == value.charAt(0) && '"' == value.charAt(value.length() - 1)) { 855 value = value.substring(1, value.length() - 1); 856 } 857 return value; 858 } 859 860 861 public static boolean prettyPrintResponse(IRestfulServerDefaults theServer, RequestDetails theRequest) { 862 Map<String, String[]> requestParams = theRequest.getParameters(); 863 String[] pretty = requestParams.get(Constants.PARAM_PRETTY); 864 boolean prettyPrint; 865 if (pretty != null && pretty.length > 0) { 866 prettyPrint = Constants.PARAM_PRETTY_VALUE_TRUE.equals(pretty[0]); 867 } else { 868 prettyPrint = theServer.isDefaultPrettyPrint(); 869 List<String> acceptValues = theRequest.getHeaders(Constants.HEADER_ACCEPT); 870 if (acceptValues != null) { 871 for (String nextAcceptHeaderValue : acceptValues) { 872 if (nextAcceptHeaderValue.contains("pretty=true")) { 873 prettyPrint = true; 874 } 875 } 876 } 877 } 878 return prettyPrint; 879 } 880 881 public static Object streamResponseAsResource(IRestfulServerDefaults theServer, IBaseResource theResource, Set<SummaryEnum> theSummaryMode, int stausCode, boolean theAddContentLocationHeader, 882 boolean respondGzip, RequestDetails theRequestDetails) throws IOException { 883 return streamResponseAsResource(theServer, theResource, theSummaryMode, stausCode, null, theAddContentLocationHeader, respondGzip, theRequestDetails, null, null); 884 } 885 886 public static Object streamResponseAsResource(IRestfulServerDefaults theServer, IBaseResource theResource, Set<SummaryEnum> theSummaryMode, int theStatusCode, String theStatusMessage, 887 boolean theAddContentLocationHeader, boolean respondGzip, RequestDetails theRequestDetails, IIdType theOperationResourceId, IPrimitiveType<Date> theOperationResourceLastUpdated) 888 throws IOException { 889 IRestfulResponse response = theRequestDetails.getResponse(); 890 891 // Determine response encoding 892 ResponseEncoding responseEncoding = RestfulServerUtils.determineResponseEncodingNoDefault(theRequestDetails, theServer.getDefaultResponseEncoding()); 893 894 String serverBase = theRequestDetails.getFhirServerBase(); 895 IIdType fullId = null; 896 if (theOperationResourceId != null) { 897 fullId = theOperationResourceId; 898 } else if (theResource != null) { 899 if (theResource.getIdElement() != null) { 900 IIdType resourceId = theResource.getIdElement(); 901 fullId = fullyQualifyResourceIdOrReturnNull(theServer, theResource, serverBase, resourceId); 902 } 903 } 904 905 if (theAddContentLocationHeader && fullId != null) { 906 if (theRequestDetails.getRequestType() == RequestTypeEnum.POST) { 907 response.addHeader(Constants.HEADER_LOCATION, fullId.getValue()); 908 } 909 response.addHeader(Constants.HEADER_CONTENT_LOCATION, fullId.getValue()); 910 } 911 912 if (theServer.getETagSupport() == ETagSupportEnum.ENABLED) { 913 if (theRequestDetails.getRestOperationType() != null) { 914 switch (theRequestDetails.getRestOperationType()) { 915 case CREATE: 916 case UPDATE: 917 case READ: 918 case VREAD: 919 if (fullId != null && fullId.hasVersionIdPart()) { 920 String versionIdPart = fullId.getVersionIdPart(); 921 response.addHeader(Constants.HEADER_ETAG, createEtag(versionIdPart)); 922 } else if (theResource != null && theResource.getMeta() != null && isNotBlank(theResource.getMeta().getVersionId())) { 923 String versionId = theResource.getMeta().getVersionId(); 924 response.addHeader(Constants.HEADER_ETAG, createEtag(versionId)); 925 } 926 } 927 } 928 } 929 930 // Binary handling 931 String contentType; 932 if (theResource instanceof IBaseBinary) { 933 IBaseBinary bin = (IBaseBinary) theResource; 934 935 // Add a security context header 936 IBaseReference securityContext = BinaryUtil.getSecurityContext(theServer.getFhirContext(), bin); 937 if (securityContext != null) { 938 String securityContextRef = securityContext.getReferenceElement().getValue(); 939 if (isNotBlank(securityContextRef)) { 940 response.addHeader(Constants.HEADER_X_SECURITY_CONTEXT, securityContextRef); 941 } 942 } 943 944 // If the user didn't explicitly request FHIR as a response, return binary 945 // content directly 946 if (responseEncoding == null) { 947 if (isNotBlank(bin.getContentType())) { 948 contentType = bin.getContentType(); 949 } else { 950 contentType = Constants.CT_OCTET_STREAM; 951 } 952 953 // Force binary resources to download - This is a security measure to prevent 954 // malicious images or HTML blocks being served up as content. 955 response.addHeader(Constants.HEADER_CONTENT_DISPOSITION, "Attachment;"); 956 957 return response.sendAttachmentResponse(bin, theStatusCode, contentType); 958 } 959 } 960 961 // Ok, we're not serving a binary resource, so apply default encoding 962 if (responseEncoding == null) { 963 responseEncoding = new ResponseEncoding(theServer.getFhirContext(), theServer.getDefaultResponseEncoding(), null); 964 } 965 966 boolean encodingDomainResourceAsText = theSummaryMode.size() == 1 && theSummaryMode.contains(SummaryEnum.TEXT); 967 if (encodingDomainResourceAsText) { 968 /* 969 * If the user requests "text" for a bundle, only suppress the non text elements in the Element.entry.resource 970 * parts, we're not streaming just the narrative as HTML (since bundles don't even 971 * have one) 972 */ 973 if ("Bundle".equals(theServer.getFhirContext().getResourceType(theResource))) { 974 encodingDomainResourceAsText = false; 975 } 976 } 977 978 /* 979 * Last-Modified header 980 */ 981 982 IPrimitiveType<Date> lastUpdated; 983 if (theOperationResourceLastUpdated != null) { 984 lastUpdated = theOperationResourceLastUpdated; 985 } else { 986 lastUpdated = extractLastUpdatedFromResource(theResource); 987 } 988 if (lastUpdated != null && lastUpdated.isEmpty() == false) { 989 response.addHeader(Constants.HEADER_LAST_MODIFIED, DateUtils.formatDate(lastUpdated.getValue())); 990 } 991 992 /* 993 * Stream the response body 994 */ 995 996 if (theResource == null) { 997 contentType = null; 998 } else if (encodingDomainResourceAsText) { 999 contentType = Constants.CT_HTML; 1000 } else { 1001 contentType = responseEncoding.getResourceContentType(); 1002 } 1003 String charset = Constants.CHARSET_NAME_UTF8; 1004 1005 Writer writer = response.getResponseWriter(theStatusCode, theStatusMessage, contentType, charset, respondGzip); 1006 1007 // Interceptor call: SERVER_OUTGOING_WRITER_CREATED 1008 if (theServer.getInterceptorService() != null && theServer.getInterceptorService().hasHooks(Pointcut.SERVER_OUTGOING_WRITER_CREATED)) { 1009 HookParams params = new HookParams() 1010 .add(Writer.class, writer) 1011 .add(RequestDetails.class, theRequestDetails) 1012 .addIfMatchesType(ServletRequestDetails.class, theRequestDetails); 1013 Object newWriter = theServer.getInterceptorService().callHooksAndReturnObject(Pointcut.SERVER_OUTGOING_WRITER_CREATED, params); 1014 if (newWriter != null) { 1015 writer = (Writer) newWriter; 1016 } 1017 } 1018 1019 if (theResource == null) { 1020 // No response is being returned 1021 } else if (encodingDomainResourceAsText && theResource instanceof IResource) { 1022 // DSTU2 1023 writer.append(((IResource) theResource).getText().getDiv().getValueAsString()); 1024 } else if (encodingDomainResourceAsText && theResource instanceof IDomainResource) { 1025 // DSTU3+ 1026 try { 1027 writer.append(((IDomainResource) theResource).getText().getDivAsString()); 1028 } catch (Exception e) { 1029 throw new InternalErrorException(Msg.code(305) + e); 1030 } 1031 } else { 1032 FhirVersionEnum forVersion = theResource.getStructureFhirVersionEnum(); 1033 IParser parser = getNewParser(theServer.getFhirContext(), forVersion, theRequestDetails); 1034 parser.encodeResourceToWriter(theResource, writer); 1035 } 1036 1037 return response.sendWriterResponse(theStatusCode, contentType, charset, writer); 1038 } 1039 1040 public static String createEtag(String theVersionId) { 1041 return "W/\"" + theVersionId + '"'; 1042 } 1043 1044 public static Integer tryToExtractNamedParameter(RequestDetails theRequest, String theParamName) { 1045 String[] retVal = theRequest.getParameters().get(theParamName); 1046 if (retVal == null) { 1047 return null; 1048 } 1049 try { 1050 return Integer.parseInt(retVal[0]); 1051 } catch (NumberFormatException e) { 1052 ourLog.debug("Failed to parse {} value '{}': {}", new Object[]{theParamName, retVal[0], e}); 1053 return null; 1054 } 1055 } 1056 1057 public static void validateResourceListNotNull(List<? extends IBaseResource> theResourceList) { 1058 if (theResourceList == null) { 1059 throw new InternalErrorException(Msg.code(306) + "IBundleProvider returned a null list of resources - This is not allowed"); 1060 } 1061 } 1062 1063 1064 /** 1065 * @since 5.0.0 1066 */ 1067 public static DeleteCascadeModeEnum extractDeleteCascadeParameter(RequestDetails theRequest) { 1068 if (theRequest != null) { 1069 String[] cascadeParameters = theRequest.getParameters().get(Constants.PARAMETER_CASCADE_DELETE); 1070 if (cascadeParameters != null && Arrays.asList(cascadeParameters).contains(Constants.CASCADE_DELETE)) { 1071 return DeleteCascadeModeEnum.DELETE; 1072 } 1073 1074 String cascadeHeader = theRequest.getHeader(Constants.HEADER_CASCADE); 1075 if (Constants.CASCADE_DELETE.equals(cascadeHeader)) { 1076 return DeleteCascadeModeEnum.DELETE; 1077 } 1078 } 1079 1080 return DeleteCascadeModeEnum.NONE; 1081 } 1082}