
001/* 002 * #%L 003 * HAPI FHIR - Server Framework 004 * %% 005 * Copyright (C) 2014 - 2025 Smile CDR, Inc. 006 * %% 007 * Licensed under the Apache License, Version 2.0 (the "License"); 008 * you may not use this file except in compliance with the License. 009 * You may obtain a copy of the License at 010 * 011 * http://www.apache.org/licenses/LICENSE-2.0 012 * 013 * Unless required by applicable law or agreed to in writing, software 014 * distributed under the License is distributed on an "AS IS" BASIS, 015 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 016 * See the License for the specific language governing permissions and 017 * limitations under the License. 018 * #L% 019 */ 020package ca.uhn.fhir.rest.api.server; 021 022import ca.uhn.fhir.context.FhirContext; 023import ca.uhn.fhir.i18n.Msg; 024import ca.uhn.fhir.interceptor.api.IInterceptorBroadcaster; 025import ca.uhn.fhir.rest.api.Constants; 026import ca.uhn.fhir.rest.api.PreferHeader; 027import ca.uhn.fhir.rest.api.RequestTypeEnum; 028import ca.uhn.fhir.rest.api.RestOperationTypeEnum; 029import ca.uhn.fhir.rest.server.IRestfulServerDefaults; 030import ca.uhn.fhir.rest.server.RestfulServerUtils; 031import ca.uhn.fhir.rest.server.interceptor.IServerInterceptor; 032import ca.uhn.fhir.util.StopWatch; 033import ca.uhn.fhir.util.UrlUtil; 034import jakarta.annotation.Nonnull; 035import jakarta.servlet.http.HttpServletRequest; 036import jakarta.servlet.http.HttpServletResponse; 037import org.apache.commons.lang3.ArrayUtils; 038import org.apache.commons.lang3.Validate; 039import org.hl7.fhir.instance.model.api.IBaseResource; 040import org.hl7.fhir.instance.model.api.IIdType; 041import org.slf4j.Logger; 042import org.slf4j.LoggerFactory; 043 044import java.io.IOException; 045import java.io.InputStream; 046import java.io.Reader; 047import java.io.UnsupportedEncodingException; 048import java.nio.charset.Charset; 049import java.nio.charset.StandardCharsets; 050import java.util.ArrayList; 051import java.util.Collections; 052import java.util.HashMap; 053import java.util.List; 054import java.util.Map; 055import java.util.stream.Collectors; 056 057import static org.apache.commons.lang3.StringUtils.isBlank; 058 059public abstract class RequestDetails { 060 private static final Logger ourLog = LoggerFactory.getLogger(RequestDetails.class); 061 public static final byte[] BAD_STREAM_PLACEHOLDER = 062 (Msg.code(2543) + "PLACEHOLDER WHEN READING FROM BAD STREAM").getBytes(StandardCharsets.UTF_8); 063 064 private final StopWatch myRequestStopwatch; 065 private final IInterceptorBroadcaster myInterceptorBroadcaster; 066 private final Map<Object, Object> myUserData = new UserDataMap(); 067 068 private String myTenantId; 069 private String myCompartmentName; 070 private String myCompleteUrl; 071 private String myFhirServerBase; 072 private IIdType myId; 073 private String myOperation; 074 private Map<String, String[]> myParameters; 075 private byte[] myRequestContents; 076 private String myRequestPath; 077 private RequestTypeEnum myRequestType; 078 private String myResourceName; 079 private boolean myRespondGzip; 080 private IRestfulResponse myResponse; 081 private RestOperationTypeEnum myRestOperationType; 082 private String mySecondaryOperation; 083 private boolean mySubRequest; 084 private Map<String, List<String>> myUnqualifiedToQualifiedNames; 085 private IBaseResource myResource; 086 private String myRequestId; 087 private String myTransactionGuid; 088 private String myFixedConditionalUrl; 089 private boolean myRewriteHistory; 090 private int myMaxRetries; 091 private boolean myRetry; 092 093 /** 094 * Constructor 095 */ 096 public RequestDetails(IInterceptorBroadcaster theInterceptorBroadcaster) { 097 myInterceptorBroadcaster = theInterceptorBroadcaster; 098 myRequestStopwatch = new StopWatch(); 099 } 100 101 /** 102 * Copy constructor 103 */ 104 public RequestDetails(RequestDetails theRequestDetails) { 105 myInterceptorBroadcaster = theRequestDetails.getInterceptorBroadcaster(); 106 myRequestStopwatch = theRequestDetails.getRequestStopwatch(); 107 myTenantId = theRequestDetails.getTenantId(); 108 myCompartmentName = theRequestDetails.getCompartmentName(); 109 myCompleteUrl = theRequestDetails.getCompleteUrl(); 110 myFhirServerBase = theRequestDetails.getFhirServerBase(); 111 myId = theRequestDetails.getId(); 112 myOperation = theRequestDetails.getOperation(); 113 myParameters = theRequestDetails.getParameters(); 114 myRequestContents = theRequestDetails.getRequestContentsIfLoaded(); 115 myRequestPath = theRequestDetails.getRequestPath(); 116 myRequestType = theRequestDetails.getRequestType(); 117 myResourceName = theRequestDetails.getResourceName(); 118 myRespondGzip = theRequestDetails.isRespondGzip(); 119 myResponse = theRequestDetails.getResponse(); 120 myRestOperationType = theRequestDetails.getRestOperationType(); 121 mySecondaryOperation = theRequestDetails.getSecondaryOperation(); 122 mySubRequest = theRequestDetails.isSubRequest(); 123 myUnqualifiedToQualifiedNames = theRequestDetails.getUnqualifiedToQualifiedNames(); 124 myUserData.putAll(theRequestDetails.getUserData()); 125 myResource = theRequestDetails.getResource(); 126 myRequestId = theRequestDetails.getRequestId(); 127 myTransactionGuid = theRequestDetails.getTransactionGuid(); 128 myFixedConditionalUrl = theRequestDetails.getFixedConditionalUrl(); 129 myRewriteHistory = theRequestDetails.isRewriteHistory(); 130 myMaxRetries = theRequestDetails.getMaxRetries(); 131 myRetry = theRequestDetails.isRetry(); 132 } 133 134 public String getFixedConditionalUrl() { 135 return myFixedConditionalUrl; 136 } 137 138 public void setFixedConditionalUrl(String theFixedConditionalUrl) { 139 myFixedConditionalUrl = theFixedConditionalUrl; 140 } 141 142 public String getRequestId() { 143 return myRequestId; 144 } 145 146 public void setRequestId(String theRequestId) { 147 myRequestId = theRequestId; 148 } 149 150 public StopWatch getRequestStopwatch() { 151 return myRequestStopwatch; 152 } 153 154 /** 155 * Returns the request resource (as provided in the request body) if it has been parsed. 156 * Note that this value is only set fairly late in the processing pipeline, so it 157 * may not always be set, even for operations that take a resource as input. 158 * 159 * @since 4.0.0 160 */ 161 public IBaseResource getResource() { 162 return myResource; 163 } 164 165 /** 166 * Sets the request resource (as provided in the request body) if it has been parsed. 167 * Note that this value is only set fairly late in the processing pipeline, so it 168 * may not always be set, even for operations that take a resource as input. 169 * 170 * @since 4.0.0 171 */ 172 public void setResource(IBaseResource theResource) { 173 myResource = theResource; 174 } 175 176 public void addParameter(String theName, String[] theValues) { 177 getParameters(); 178 myParameters.put(theName, theValues); 179 } 180 181 protected abstract byte[] getByteStreamRequestContents(); 182 183 /** 184 * Return the charset as defined by the header contenttype. Return null if it is not set. 185 */ 186 public abstract Charset getCharset(); 187 188 public String getCompartmentName() { 189 return myCompartmentName; 190 } 191 192 public void setCompartmentName(String theCompartmentName) { 193 myCompartmentName = theCompartmentName; 194 } 195 196 public String getCompleteUrl() { 197 return myCompleteUrl; 198 } 199 200 public void setCompleteUrl(String theCompleteUrl) { 201 myCompleteUrl = theCompleteUrl; 202 } 203 204 /** 205 * Returns the <b>conditional URL</b> if this request has one, or <code>null</code> otherwise. For an 206 * update or delete method, this is the part of the URL after the <code>?</code>. For a create, this 207 * is the value of the <code>If-None-Exist</code> header. 208 * 209 * @param theOperationType The operation type to find the conditional URL for 210 * @return Returns the <b>conditional URL</b> if this request has one, or <code>null</code> otherwise 211 */ 212 @SuppressWarnings("EnumSwitchStatementWhichMissesCases") 213 public String getConditionalUrl(RestOperationTypeEnum theOperationType) { 214 if (myFixedConditionalUrl != null) { 215 return myFixedConditionalUrl; 216 } 217 switch (theOperationType) { 218 case CREATE: 219 String retVal = this.getHeader(Constants.HEADER_IF_NONE_EXIST); 220 if (isBlank(retVal)) { 221 return null; 222 } 223 if (retVal.startsWith(this.getFhirServerBase())) { 224 retVal = retVal.substring(this.getFhirServerBase().length()); 225 } 226 return retVal; 227 case DELETE: 228 case UPDATE: 229 case PATCH: 230 if (this.getId() != null && this.getId().hasIdPart()) { 231 return null; 232 } 233 234 int questionMarkIndex = this.getCompleteUrl().indexOf('?'); 235 if (questionMarkIndex == -1) { 236 return null; 237 } 238 239 return this.getResourceName() + this.getCompleteUrl().substring(questionMarkIndex); 240 default: 241 return null; 242 } 243 } 244 245 /** 246 * Returns the HAPI FHIR Context associated with this request 247 */ 248 public abstract FhirContext getFhirContext(); 249 250 /** 251 * The fhir server base url, independant of the query being executed 252 * 253 * @return the fhir server base url 254 */ 255 public String getFhirServerBase() { 256 return myFhirServerBase; 257 } 258 259 public void setFhirServerBase(String theFhirServerBase) { 260 myFhirServerBase = theFhirServerBase; 261 } 262 263 public abstract String getHeader(String name); 264 265 public abstract List<String> getHeaders(String name); 266 267 /** 268 * Adds a new header 269 * 270 * @param theName The header name 271 * @param theValue The header value 272 * @since 7.2.0 273 */ 274 public abstract void addHeader(String theName, String theValue); 275 276 /** 277 * Replaces any existing header(s) with the given name using a List of new header values 278 * 279 * @param theName The header name 280 * @param theValue The header value 281 * @since 7.2.0 282 */ 283 public abstract void setHeaders(String theName, List<String> theValue); 284 285 public IIdType getId() { 286 return myId; 287 } 288 289 public void setId(IIdType theId) { 290 myId = theId; 291 } 292 293 /** 294 * @deprecated Use {@link #getUserData()}. If servlet attributes are truly required, then use {@link IHasServletAttributes#getServletAttribute(String)}. 295 */ 296 @Deprecated 297 public Object getAttribute(String theAttributeName) { 298 ourLog.error( 299 "{} is not a servlet request. Unable to get attribute {}", 300 getClass().getName(), 301 theAttributeName); 302 return null; 303 } 304 305 /** 306 * @deprecated Use {@link #getUserData()}. If servlet attributes are truly required, then use {@link IHasServletAttributes#setServletAttribute(String, Object)}. 307 */ 308 @Deprecated 309 public void setAttribute(String theAttributeName, Object theAttributeValue) { 310 ourLog.error( 311 "{} is not a servlet request. Unable to set attribute {}", 312 getClass().getName(), 313 theAttributeName); 314 } 315 316 /** 317 * Retrieves the body of the request as binary data. Either this method or {@link #getReader} may be called to read 318 * the body, not both. 319 * 320 * @return a {@link InputStream} object containing the body of the request 321 * @throws IllegalStateException if the {@link #getReader} method has already been called for this request 322 * @throws IOException if an input or output exception occurred 323 */ 324 public abstract InputStream getInputStream() throws IOException; 325 326 public String getOperation() { 327 return myOperation; 328 } 329 330 public void setOperation(String theOperation) { 331 myOperation = theOperation; 332 } 333 334 public Map<String, String[]> getParameters() { 335 if (myParameters == null) { 336 myParameters = new HashMap<>(); 337 } 338 return Collections.unmodifiableMap(myParameters); 339 } 340 341 public void setParameters(Map<String, String[]> theParams) { 342 myParameters = theParams; 343 myUnqualifiedToQualifiedNames = null; 344 345 // Sanitize keys if necessary to prevent injection attacks 346 boolean needsSanitization = false; 347 for (String nextKey : theParams.keySet()) { 348 if (UrlUtil.isNeedsSanitization(nextKey)) { 349 needsSanitization = true; 350 break; 351 } 352 } 353 if (needsSanitization) { 354 myParameters = myParameters.entrySet().stream() 355 .collect( 356 Collectors.toMap(t -> UrlUtil.sanitizeUrlPart((String) ((Map.Entry<?, ?>) t).getKey()), t -> 357 (String[]) ((Map.Entry<?, ?>) t).getValue())); 358 } 359 } 360 361 /** 362 * Retrieves the body of the request as character data using a <code>BufferedReader</code>. The reader translates the 363 * character data according to the character encoding used on the body. Either this method or {@link #getInputStream} 364 * may be called to read the body, not both. 365 * 366 * @return a <code>Reader</code> containing the body of the request 367 * @throws UnsupportedEncodingException if the character set encoding used is not supported and the text cannot be decoded 368 * @throws IllegalStateException if {@link #getInputStream} method has been called on this request 369 * @throws IOException if an input or output exception occurred 370 * @see jakarta.servlet.http.HttpServletRequest#getInputStream 371 */ 372 public abstract Reader getReader() throws IOException; 373 374 /** 375 * Returns an invoker that can be called from user code to advise the server interceptors 376 * of any nested operations being invoked within operations. This invoker acts as a proxy for 377 * all interceptors 378 */ 379 public IInterceptorBroadcaster getInterceptorBroadcaster() { 380 return myInterceptorBroadcaster; 381 } 382 383 /** 384 * The part of the request URL that comes after the server base. 385 * <p> 386 * Will not contain a leading '/' 387 * </p> 388 */ 389 public String getRequestPath() { 390 return myRequestPath; 391 } 392 393 public void setRequestPath(String theRequestPath) { 394 assert theRequestPath.isEmpty() || theRequestPath.charAt(0) != '/'; 395 myRequestPath = theRequestPath; 396 } 397 398 public RequestTypeEnum getRequestType() { 399 return myRequestType; 400 } 401 402 public void setRequestType(RequestTypeEnum theRequestType) { 403 myRequestType = theRequestType; 404 } 405 406 public String getResourceName() { 407 return myResourceName; 408 } 409 410 public void setResourceName(String theResourceName) { 411 myResourceName = theResourceName; 412 } 413 414 public IRestfulResponse getResponse() { 415 return myResponse; 416 } 417 418 public void setResponse(IRestfulResponse theResponse) { 419 this.myResponse = theResponse; 420 } 421 422 public RestOperationTypeEnum getRestOperationType() { 423 return myRestOperationType; 424 } 425 426 public void setRestOperationType(RestOperationTypeEnum theRestOperationType) { 427 myRestOperationType = theRestOperationType; 428 } 429 430 public String getSecondaryOperation() { 431 return mySecondaryOperation; 432 } 433 434 public void setSecondaryOperation(String theSecondaryOperation) { 435 mySecondaryOperation = theSecondaryOperation; 436 } 437 438 public abstract IRestfulServerDefaults getServer(); 439 440 /** 441 * Returns the server base URL (with no trailing '/') for a given request 442 * 443 * @deprecated Use {@link #getFhirServerBase()} instead. Deprecated in HAPI FHIR 7.0.0 444 */ 445 @Deprecated 446 public abstract String getServerBaseForRequest(); 447 448 /** 449 * Gets the tenant ID associated with the request. Note that the tenant ID 450 * and the partition ID are not the same thing - Depending on the specific 451 * partition interceptors in use, the tenant ID might be used internally 452 * to derive the partition ID or it might not. Do not assume that it will 453 * be used for this purpose. 454 */ 455 public String getTenantId() { 456 return myTenantId; 457 } 458 459 /** 460 * Sets the tenant ID associated with the request. Note that the tenant ID 461 * and the partition ID are not the same thing - Depending on the specific 462 * partition interceptors in use, the tenant ID might be used internally 463 * to derive the partition ID or it might not. Do not assume that it will 464 * be used for this purpose. 465 */ 466 public void setTenantId(String theTenantId) { 467 myTenantId = theTenantId; 468 } 469 470 public Map<String, List<String>> getUnqualifiedToQualifiedNames() { 471 if (myUnqualifiedToQualifiedNames == null) { 472 for (String next : myParameters.keySet()) { 473 for (int i = 0; i < next.length(); i++) { 474 char nextChar = next.charAt(i); 475 if (nextChar == ':' || nextChar == '.') { 476 if (myUnqualifiedToQualifiedNames == null) { 477 myUnqualifiedToQualifiedNames = new HashMap<>(); 478 } 479 String unqualified = next.substring(0, i); 480 List<String> list = 481 myUnqualifiedToQualifiedNames.computeIfAbsent(unqualified, k -> new ArrayList<>(4)); 482 list.add(next); 483 break; 484 } 485 } 486 } 487 } 488 489 if (myUnqualifiedToQualifiedNames == null) { 490 myUnqualifiedToQualifiedNames = Collections.emptyMap(); 491 } 492 493 return myUnqualifiedToQualifiedNames; 494 } 495 496 /** 497 * Returns a map which can be used to hold any user specific data to pass it from one 498 * part of the request handling chain to another. Data in this map can use any key, although 499 * user code should try to use keys which are specific enough to avoid conflicts. 500 * <p> 501 * A new map is created for each individual request that is handled by the server, 502 * so this map can be used (for example) to pass authorization details from an interceptor 503 * to the resource providers, or for example to pass data from a hook method 504 * on the {@link ca.uhn.fhir.interceptor.api.Pointcut#SERVER_INCOMING_REQUEST_POST_PROCESSED} 505 * to a later hook method on the {@link ca.uhn.fhir.interceptor.api.Pointcut#SERVER_OUTGOING_RESPONSE} 506 * pointcut. 507 * </p> 508 * <p> 509 * This method should be used to pass information between interceptor methods. For servlet-specific 510 * request attributes used to communicate between servlet filters, use {@link IHasServletAttributes} 511 * methods available on implementations that support them. 512 * </p> 513 */ 514 @Nonnull 515 public Map<Object, Object> getUserData() { 516 return myUserData; 517 } 518 519 public boolean isRespondGzip() { 520 return myRespondGzip; 521 } 522 523 public void setRespondGzip(boolean theRespondGzip) { 524 myRespondGzip = theRespondGzip; 525 } 526 527 /** 528 * Is this request a sub-request (i.e. a request within a batch or transaction)? This 529 * flag is used internally by hapi-fhir-jpaserver-base, but not used in the plain server 530 * library. You may use it in your client code as a hint when implementing transaction logic in the plain 531 * server. 532 * <p> 533 * Defaults to {@literal false} 534 * </p> 535 */ 536 public boolean isSubRequest() { 537 return mySubRequest; 538 } 539 540 /** 541 * Is this request a sub-request (i.e. a request within a batch or transaction)? This 542 * flag is used internally by hapi-fhir-jpaserver-base, but not used in the plain server 543 * library. You may use it in your client code as a hint when implementing transaction logic in the plain 544 * server. 545 * <p> 546 * Defaults to {@literal false} 547 * </p> 548 */ 549 public void setSubRequest(boolean theSubRequest) { 550 mySubRequest = theSubRequest; 551 } 552 553 public final synchronized byte[] loadRequestContents() { 554 if (myRequestContents == null) { 555 // Initialize the byte array to a non-null value to avoid repeated calls to getByteStreamRequestContents() 556 // which can occur when getByteStreamRequestContents() throws an Exception 557 myRequestContents = ArrayUtils.EMPTY_BYTE_ARRAY; 558 try { 559 myRequestContents = getByteStreamRequestContents(); 560 } finally { 561 if (myRequestContents == null) { 562 // if reading the stream throws an exception, then our contents are still null, but the stream is 563 // dead. 564 // Set a placeholder value so nobody tries to read again. 565 myRequestContents = BAD_STREAM_PLACEHOLDER; 566 } 567 } 568 } 569 return getRequestContentsIfLoaded(); 570 } 571 572 /** 573 * Returns the request contents if they were loaded, returns <code>null</code> otherwise 574 * 575 * @see #loadRequestContents() 576 */ 577 public byte[] getRequestContentsIfLoaded() { 578 return myRequestContents; 579 } 580 581 public void removeParameter(String theName) { 582 Validate.notNull(theName, "theName must not be null"); 583 getParameters(); 584 myParameters.remove(theName); 585 } 586 587 /** 588 * This method may be used to modify the contents of the incoming 589 * request by hardcoding a value which will be used instead of the 590 * value received by the client. 591 * <p> 592 * This method is useful for modifying the request body prior 593 * to parsing within interceptors. It generally only has an 594 * impact when called in the {@link IServerInterceptor#incomingRequestPostProcessed(RequestDetails, HttpServletRequest, HttpServletResponse)} 595 * method 596 * </p> 597 */ 598 public void setRequestContents(byte[] theRequestContents) { 599 myRequestContents = theRequestContents; 600 } 601 602 public String getTransactionGuid() { 603 return myTransactionGuid; 604 } 605 606 public void setTransactionGuid(String theTransactionGuid) { 607 myTransactionGuid = theTransactionGuid; 608 } 609 610 public boolean isRewriteHistory() { 611 return myRewriteHistory; 612 } 613 614 public void setRewriteHistory(boolean theRewriteHistory) { 615 myRewriteHistory = theRewriteHistory; 616 } 617 618 public int getMaxRetries() { 619 return myMaxRetries; 620 } 621 622 public void setMaxRetries(int theMaxRetries) { 623 myMaxRetries = theMaxRetries; 624 } 625 626 public boolean isRetry() { 627 return myRetry; 628 } 629 630 public void setRetry(boolean theRetry) { 631 myRetry = theRetry; 632 } 633 634 public boolean isPreferAsync() { 635 String prefer = getHeader(Constants.HEADER_PREFER); 636 PreferHeader preferHeader = RestfulServerUtils.parsePreferHeader(prefer); 637 return preferHeader.getRespondAsync(); 638 } 639}