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