001package ca.uhn.fhir.rest.method; 002 003import static org.apache.commons.lang3.StringUtils.isBlank; 004 005import java.io.IOException; 006import java.io.InputStream; 007import java.io.Reader; 008import java.io.UnsupportedEncodingException; 009import java.nio.charset.Charset; 010 011/* 012 * #%L 013 * HAPI FHIR - Core Library 014 * %% 015 * Copyright (C) 2014 - 2017 University Health Network 016 * %% 017 * Licensed under the Apache License, Version 2.0 (the "License"); 018 * you may not use this file except in compliance with the License. 019 * You may obtain a copy of the License at 020 * 021 * http://www.apache.org/licenses/LICENSE-2.0 022 * 023 * Unless required by applicable law or agreed to in writing, software 024 * distributed under the License is distributed on an "AS IS" BASIS, 025 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 026 * See the License for the specific language governing permissions and 027 * limitations under the License. 028 * #L% 029 */ 030 031import java.util.ArrayList; 032import java.util.Collection; 033import java.util.Collections; 034import java.util.HashMap; 035import java.util.List; 036import java.util.Map; 037 038import org.hl7.fhir.instance.model.api.IBaseResource; 039import org.hl7.fhir.instance.model.api.IIdType; 040 041import ca.uhn.fhir.rest.api.RequestTypeEnum; 042import ca.uhn.fhir.rest.api.RestOperationTypeEnum; 043import ca.uhn.fhir.rest.server.Constants; 044import ca.uhn.fhir.rest.server.IRestfulResponse; 045import ca.uhn.fhir.rest.server.IRestfulServerDefaults; 046import ca.uhn.fhir.rest.server.interceptor.IServerInterceptor; 047import ca.uhn.fhir.rest.server.interceptor.IServerOperationInterceptor; 048 049public abstract class RequestDetails { 050 051 private String myCompartmentName; 052 private String myCompleteUrl; 053 private String myFhirServerBase; 054 private IIdType myId; 055 private String myOperation; 056 private Map<String, String[]> myParameters; 057 private byte[] myRequestContents; 058 private IRequestOperationCallback myRequestOperationCallback = new RequestOperationCallback(); 059 private String myRequestPath; 060 private RequestTypeEnum myRequestType; 061 private String myResourceName; 062 private boolean myRespondGzip; 063 private IRestfulResponse myResponse; 064 private RestOperationTypeEnum myRestOperationType; 065 private String mySecondaryOperation; 066 private boolean mySubRequest; 067 private Map<String, List<String>> myUnqualifiedToQualifiedNames; 068 private Map<Object, Object> myUserData; 069 protected abstract byte[] getByteStreamRequestContents(); 070 071 /** 072 * Return the charset as defined by the header contenttype. Return null if it is not set. 073 */ 074 public abstract Charset getCharset(); 075 076 public String getCompartmentName() { 077 return myCompartmentName; 078 } 079 080 public String getCompleteUrl() { 081 return myCompleteUrl; 082 } 083 /** 084 * Returns the <b>conditional URL</b> if this request has one, or <code>null</code> otherwise. For an 085 * update or delete method, this is the part of the URL after the <code>?</code>. For a create, this 086 * is the value of the <code>If-None-Exist</code> header. 087 * 088 * @param theOperationType The operation type to find the conditional URL for 089 * @return Returns the <b>conditional URL</b> if this request has one, or <code>null</code> otherwise 090 */ 091 public String getConditionalUrl(RestOperationTypeEnum theOperationType) { 092 if (theOperationType == RestOperationTypeEnum.CREATE) { 093 String retVal = this.getHeader(Constants.HEADER_IF_NONE_EXIST); 094 if (isBlank(retVal)) { 095 return null; 096 } 097 if (retVal.startsWith(this.getFhirServerBase())) { 098 retVal = retVal.substring(this.getFhirServerBase().length()); 099 } 100 return retVal; 101 } else if (theOperationType != RestOperationTypeEnum.DELETE && theOperationType != RestOperationTypeEnum.UPDATE) { 102 return null; 103 } 104 105 if (this.getId() != null && this.getId().hasIdPart()) { 106 return null; 107 } 108 109 int questionMarkIndex = this.getCompleteUrl().indexOf('?'); 110 if (questionMarkIndex == -1) { 111 return null; 112 } 113 114 return this.getResourceName() + this.getCompleteUrl().substring(questionMarkIndex); 115 } 116 117 /** 118 * The fhir server base url, independant of the query being executed 119 * 120 * @return the fhir server base url 121 */ 122 public String getFhirServerBase() { 123 return myFhirServerBase; 124 } 125 126 public abstract String getHeader(String name); 127 128 public abstract List<String> getHeaders(String name); 129 130 public IIdType getId() { 131 return myId; 132 } 133 134 /** 135 * Retrieves the body of the request as binary data. Either this method or {@link #getReader} may be called to read 136 * the body, not both. 137 * 138 * @return a {@link InputStream} object containing the body of the request 139 * 140 * @exception IllegalStateException 141 * if the {@link #getReader} method has already been called for this request 142 * 143 * @exception IOException 144 * if an input or output exception occurred 145 */ 146 public abstract InputStream getInputStream() throws IOException; 147 148 public String getOperation() { 149 return myOperation; 150 } 151 152 public Map<String, String[]> getParameters() { 153 if (myParameters == null) { 154 return Collections.emptyMap(); 155 } 156 return myParameters; 157 } 158 159 /** 160 * Retrieves the body of the request as character data using a <code>BufferedReader</code>. The reader translates the 161 * character data according to the character encoding used on the body. Either this method or {@link #getInputStream} 162 * may be called to read the body, not both. 163 * 164 * @return a <code>Reader</code> containing the body of the request 165 * 166 * @exception UnsupportedEncodingException 167 * if the character set encoding used is not supported and the text cannot be decoded 168 * 169 * @exception IllegalStateException 170 * if {@link #getInputStream} method has been called on this request 171 * 172 * @exception IOException 173 * if an input or output exception occurred 174 * 175 * @see javax.servlet.http.HttpServletRequest#getInputStream 176 */ 177 public abstract Reader getReader() throws IOException; 178 179 /** 180 * Returns an invoker that can be called from user code to advise the server interceptors 181 * of any nested operations being invoked within operations. This invoker acts as a proxy for 182 * all interceptors 183 */ 184 public IRequestOperationCallback getRequestOperationCallback() { 185 return myRequestOperationCallback; 186 } 187 188 /** 189 * The part of the request URL that comes after the server base. 190 * <p> 191 * Will not contain a leading '/' 192 * </p> 193 */ 194 public String getRequestPath() { 195 return myRequestPath; 196 } 197 198 public RequestTypeEnum getRequestType() { 199 return myRequestType; 200 } 201 202 public String getResourceName() { 203 return myResourceName; 204 } 205 206 public IRestfulResponse getResponse() { 207 return myResponse; 208 } 209 210 public RestOperationTypeEnum getRestOperationType() { 211 return myRestOperationType; 212 } 213 214 public String getSecondaryOperation() { 215 return mySecondaryOperation; 216 } 217 218 public abstract IRestfulServerDefaults getServer(); 219 220 /** 221 * Returns the server base URL (with no trailing '/') for a given request 222 */ 223 public abstract String getServerBaseForRequest(); 224 225 public Map<String, List<String>> getUnqualifiedToQualifiedNames() { 226 return myUnqualifiedToQualifiedNames; 227 } 228 229 /** 230 * Returns a map which can be used to hold any user specific data to pass it from one 231 * part of the request handling chain to another. Data in this map can use any key, although 232 * user code should try to use keys which are specific enough to avoid conflicts. 233 * <p> 234 * A new map is created for each individual request that is handled by the server, 235 * so this map can be used (for example) to pass authorization details from an interceptor 236 * to the resource providers, or from an interceptor's {@link IServerInterceptor#incomingRequestPreHandled(RestOperationTypeEnum, ca.uhn.fhir.rest.server.interceptor.IServerInterceptor.ActionRequestDetails)} 237 * method to the {@link IServerInterceptor#outgoingResponse(RequestDetails, org.hl7.fhir.instance.model.api.IBaseResource, javax.servlet.http.HttpServletRequest, javax.servlet.http.HttpServletResponse)} 238 * method. 239 * </p> 240 */ 241 public Map<Object, Object> getUserData() { 242 if (myUserData == null) { 243 myUserData = new HashMap<Object, Object>(); 244 } 245 return myUserData; 246 } 247 248 public boolean isRespondGzip() { 249 return myRespondGzip; 250 } 251 252 /** 253 * Is this request a sub-request (i.e. a request within a batch or transaction)? This 254 * flag is used internally by hapi-fhir-jpaserver-base, but not used in the plain server 255 * library. You may use it in your client code as a hint when implementing transaction logic in the plain 256 * server. 257 * <p> 258 * Defaults to {@literal false} 259 * </p> 260 */ 261 public boolean isSubRequest() { 262 return mySubRequest; 263 } 264 265 public final byte[] loadRequestContents() { 266 if (myRequestContents == null) { 267 myRequestContents = getByteStreamRequestContents(); 268 } 269 return myRequestContents; 270 } 271 272 public void setCompartmentName(String theCompartmentName) { 273 myCompartmentName = theCompartmentName; 274 } 275 276 public void setCompleteUrl(String theCompleteUrl) { 277 myCompleteUrl = theCompleteUrl; 278 } 279 280 public void setFhirServerBase(String theFhirServerBase) { 281 myFhirServerBase = theFhirServerBase; 282 } 283 284 public void setId(IIdType theId) { 285 myId = theId; 286 } 287 288 public void setOperation(String theOperation) { 289 myOperation = theOperation; 290 } 291 292 public void setParameters(Map<String, String[]> theParams) { 293 myParameters = theParams; 294 295 for (String next : theParams.keySet()) { 296 for (int i = 0; i < next.length(); i++) { 297 char nextChar = next.charAt(i); 298 if (nextChar == ':' || nextChar == '.') { 299 if (myUnqualifiedToQualifiedNames == null) { 300 myUnqualifiedToQualifiedNames = new HashMap<String, List<String>>(); 301 } 302 String unqualified = next.substring(0, i); 303 List<String> list = myUnqualifiedToQualifiedNames.get(unqualified); 304 if (list == null) { 305 list = new ArrayList<String>(4); 306 myUnqualifiedToQualifiedNames.put(unqualified, list); 307 } 308 list.add(next); 309 break; 310 } 311 } 312 } 313 314 if (myUnqualifiedToQualifiedNames == null) { 315 myUnqualifiedToQualifiedNames = Collections.emptyMap(); 316 } 317 318 } 319 320 public void setRequestPath(String theRequestPath) { 321 assert theRequestPath.length() == 0 || theRequestPath.charAt(0) != '/'; 322 myRequestPath = theRequestPath; 323 } 324 325 public void setRequestType(RequestTypeEnum theRequestType) { 326 myRequestType = theRequestType; 327 } 328 329 public void setResourceName(String theResourceName) { 330 myResourceName = theResourceName; 331 } 332 333 public void setRespondGzip(boolean theRespondGzip) { 334 myRespondGzip = theRespondGzip; 335 } 336 337 public void setResponse(IRestfulResponse theResponse) { 338 this.myResponse = theResponse; 339 } 340 341 public void setRestOperationType(RestOperationTypeEnum theRestOperationType) { 342 myRestOperationType = theRestOperationType; 343 } 344 345 public void setSecondaryOperation(String theSecondaryOperation) { 346 mySecondaryOperation = theSecondaryOperation; 347 } 348 349 /** 350 * Is this request a sub-request (i.e. a request within a batch or transaction)? This 351 * flag is used internally by hapi-fhir-jpaserver-base, but not used in the plain server 352 * library. You may use it in your client code as a hint when implementing transaction logic in the plain 353 * server. 354 * <p> 355 * Defaults to {@literal false} 356 * </p> 357 */ 358 public void setSubRequest(boolean theSubRequest) { 359 mySubRequest = theSubRequest; 360 } 361 362 private class RequestOperationCallback implements IRequestOperationCallback { 363 364 private List<IServerInterceptor> getInterceptors() { 365 if (getServer() == null) { 366 return Collections.emptyList(); 367 } 368 return getServer().getInterceptors(); 369 } 370 371 @Override 372 public void resourceCreated(IBaseResource theResource) { 373 for (IServerInterceptor next : getInterceptors()) { 374 if (next instanceof IServerOperationInterceptor) { 375 ((IServerOperationInterceptor) next).resourceCreated(RequestDetails.this, theResource); 376 } 377 } 378 } 379 380 @Override 381 public void resourceDeleted(IBaseResource theResource) { 382 for (IServerInterceptor next : getInterceptors()) { 383 if (next instanceof IServerOperationInterceptor) { 384 ((IServerOperationInterceptor) next).resourceDeleted(RequestDetails.this, theResource); 385 } 386 } 387 } 388 389 @Override 390 public void resourcesCreated(Collection<? extends IBaseResource> theResource) { 391 for (IBaseResource next : theResource) { 392 resourceCreated(next); 393 } 394 } 395 396 @Override 397 public void resourcesDeleted(Collection<? extends IBaseResource> theResource) { 398 for (IBaseResource next : theResource) { 399 resourceDeleted(next); 400 } 401 } 402 403 @Override 404 public void resourcesUpdated(Collection<? extends IBaseResource> theResource) { 405 for (IBaseResource next : theResource) { 406 resourceUpdated(next); 407 } 408 } 409 410 @Override 411 public void resourceUpdated(IBaseResource theResource) { 412 for (IServerInterceptor next : getInterceptors()) { 413 if (next instanceof IServerOperationInterceptor) { 414 ((IServerOperationInterceptor) next).resourceUpdated(RequestDetails.this, theResource); 415 } 416 } 417 } 418 419 } 420 421}