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