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