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