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