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