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