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