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