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