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