package ca.uhn.fhir.rest.method; import static org.apache.commons.lang3.StringUtils.isBlank; import java.io.IOException; import java.io.InputStream; import java.io.Reader; import java.io.UnsupportedEncodingException; import java.nio.charset.Charset; /* * #%L * HAPI FHIR - Core Library * %% * Copyright (C) 2014 - 2017 University Health Network * %% * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * #L% */ import java.util.ArrayList; import java.util.Collection; import java.util.Collections; import java.util.HashMap; import java.util.List; import java.util.Map; import org.hl7.fhir.instance.model.api.IBaseResource; import org.hl7.fhir.instance.model.api.IIdType; import ca.uhn.fhir.rest.api.RequestTypeEnum; import ca.uhn.fhir.rest.api.RestOperationTypeEnum; import ca.uhn.fhir.rest.server.Constants; import ca.uhn.fhir.rest.server.IRestfulResponse; import ca.uhn.fhir.rest.server.IRestfulServerDefaults; import ca.uhn.fhir.rest.server.interceptor.IServerInterceptor; import ca.uhn.fhir.rest.server.interceptor.IServerOperationInterceptor; public abstract class RequestDetails { private String myCompartmentName; private String myCompleteUrl; private String myFhirServerBase; private IIdType myId; private String myOperation; private Map<String, String[]> myParameters; private byte[] myRequestContents; private IRequestOperationCallback myRequestOperationCallback = new RequestOperationCallback(); private String myRequestPath; private RequestTypeEnum myRequestType; private String myResourceName; private boolean myRespondGzip; private IRestfulResponse myResponse; private RestOperationTypeEnum myRestOperationType; private String mySecondaryOperation; private boolean mySubRequest; private Map<String, List<String>> myUnqualifiedToQualifiedNames; private Map<Object, Object> myUserData; protected abstract byte[] getByteStreamRequestContents(); /** * Return the charset as defined by the header contenttype. Return null if it is not set. */ public abstract Charset getCharset(); public String getCompartmentName() { return myCompartmentName; } public String getCompleteUrl() { return myCompleteUrl; } /** * Returns the <b>conditional URL</b> if this request has one, or <code>null</code> otherwise. For an * update or delete method, this is the part of the URL after the <code>?</code>. For a create, this * is the value of the <code>If-None-Exist</code> header. * * @param theOperationType The operation type to find the conditional URL for * @return Returns the <b>conditional URL</b> if this request has one, or <code>null</code> otherwise */ public String getConditionalUrl(RestOperationTypeEnum theOperationType) { if (theOperationType == RestOperationTypeEnum.CREATE) { String retVal = this.getHeader(Constants.HEADER_IF_NONE_EXIST); if (isBlank(retVal)) { return null; } if (retVal.startsWith(this.getFhirServerBase())) { retVal = retVal.substring(this.getFhirServerBase().length()); } return retVal; } else if (theOperationType != RestOperationTypeEnum.DELETE && theOperationType != RestOperationTypeEnum.UPDATE) { return null; } if (this.getId() != null && this.getId().hasIdPart()) { return null; } int questionMarkIndex = this.getCompleteUrl().indexOf('?'); if (questionMarkIndex == -1) { return null; } return this.getResourceName() + this.getCompleteUrl().substring(questionMarkIndex); } /** * The fhir server base url, independant of the query being executed * * @return the fhir server base url */ public String getFhirServerBase() { return myFhirServerBase; } public abstract String getHeader(String name); public abstract List<String> getHeaders(String name); public IIdType getId() { return myId; } /** * Retrieves the body of the request as binary data. Either this method or {@link #getReader} may be called to read * the body, not both. * * @return a {@link InputStream} object containing the body of the request * * @exception IllegalStateException * if the {@link #getReader} method has already been called for this request * * @exception IOException * if an input or output exception occurred */ public abstract InputStream getInputStream() throws IOException; public String getOperation() { return myOperation; } public Map<String, String[]> getParameters() { if (myParameters == null) { return Collections.emptyMap(); } return myParameters; } /** * Retrieves the body of the request as character data using a <code>BufferedReader</code>. The reader translates the * character data according to the character encoding used on the body. Either this method or {@link #getInputStream} * may be called to read the body, not both. * * @return a <code>Reader</code> containing the body of the request * * @exception UnsupportedEncodingException * if the character set encoding used is not supported and the text cannot be decoded * * @exception IllegalStateException * if {@link #getInputStream} method has been called on this request * * @exception IOException * if an input or output exception occurred * * @see javax.servlet.http.HttpServletRequest#getInputStream */ public abstract Reader getReader() throws IOException; /** * Returns an invoker that can be called from user code to advise the server interceptors * of any nested operations being invoked within operations. This invoker acts as a proxy for * all interceptors */ public IRequestOperationCallback getRequestOperationCallback() { return myRequestOperationCallback; } /** * The part of the request URL that comes after the server base. * <p> * Will not contain a leading '/' * </p> */ public String getRequestPath() { return myRequestPath; } public RequestTypeEnum getRequestType() { return myRequestType; } public String getResourceName() { return myResourceName; } public IRestfulResponse getResponse() { return myResponse; } public RestOperationTypeEnum getRestOperationType() { return myRestOperationType; } public String getSecondaryOperation() { return mySecondaryOperation; } public abstract IRestfulServerDefaults getServer(); /** * Returns the server base URL (with no trailing '/') for a given request */ public abstract String getServerBaseForRequest(); public Map<String, List<String>> getUnqualifiedToQualifiedNames() { return myUnqualifiedToQualifiedNames; } /** * Returns a map which can be used to hold any user specific data to pass it from one * part of the request handling chain to another. Data in this map can use any key, although * user code should try to use keys which are specific enough to avoid conflicts. * <p> * A new map is created for each individual request that is handled by the server, * so this map can be used (for example) to pass authorization details from an interceptor * to the resource providers, or from an interceptor's {@link IServerInterceptor#incomingRequestPreHandled(RestOperationTypeEnum, ca.uhn.fhir.rest.server.interceptor.IServerInterceptor.ActionRequestDetails)} * method to the {@link IServerInterceptor#outgoingResponse(RequestDetails, org.hl7.fhir.instance.model.api.IBaseResource, javax.servlet.http.HttpServletRequest, javax.servlet.http.HttpServletResponse)} * method. * </p> */ public Map<Object, Object> getUserData() { if (myUserData == null) { myUserData = new HashMap<Object, Object>(); } return myUserData; } public boolean isRespondGzip() { return myRespondGzip; } /** * Is this request a sub-request (i.e. a request within a batch or transaction)? This * flag is used internally by hapi-fhir-jpaserver-base, but not used in the plain server * library. You may use it in your client code as a hint when implementing transaction logic in the plain * server. * <p> * Defaults to {@literal false} * </p> */ public boolean isSubRequest() { return mySubRequest; } public final byte[] loadRequestContents() { if (myRequestContents == null) { myRequestContents = getByteStreamRequestContents(); } return myRequestContents; } public void setCompartmentName(String theCompartmentName) { myCompartmentName = theCompartmentName; } public void setCompleteUrl(String theCompleteUrl) { myCompleteUrl = theCompleteUrl; } public void setFhirServerBase(String theFhirServerBase) { myFhirServerBase = theFhirServerBase; } public void setId(IIdType theId) { myId = theId; } public void setOperation(String theOperation) { myOperation = theOperation; } public void setParameters(Map<String, String[]> theParams) { myParameters = theParams; for (String next : theParams.keySet()) { for (int i = 0; i < next.length(); i++) { char nextChar = next.charAt(i); if (nextChar == ':' || nextChar == '.') { if (myUnqualifiedToQualifiedNames == null) { myUnqualifiedToQualifiedNames = new HashMap<String, List<String>>(); } String unqualified = next.substring(0, i); List<String> list = myUnqualifiedToQualifiedNames.get(unqualified); if (list == null) { list = new ArrayList<String>(4); myUnqualifiedToQualifiedNames.put(unqualified, list); } list.add(next); break; } } } if (myUnqualifiedToQualifiedNames == null) { myUnqualifiedToQualifiedNames = Collections.emptyMap(); } } public void setRequestPath(String theRequestPath) { assert theRequestPath.length() == 0 || theRequestPath.charAt(0) != '/'; myRequestPath = theRequestPath; } public void setRequestType(RequestTypeEnum theRequestType) { myRequestType = theRequestType; } public void setResourceName(String theResourceName) { myResourceName = theResourceName; } public void setRespondGzip(boolean theRespondGzip) { myRespondGzip = theRespondGzip; } public void setResponse(IRestfulResponse theResponse) { this.myResponse = theResponse; } public void setRestOperationType(RestOperationTypeEnum theRestOperationType) { myRestOperationType = theRestOperationType; } public void setSecondaryOperation(String theSecondaryOperation) { mySecondaryOperation = theSecondaryOperation; } /** * Is this request a sub-request (i.e. a request within a batch or transaction)? This * flag is used internally by hapi-fhir-jpaserver-base, but not used in the plain server * library. You may use it in your client code as a hint when implementing transaction logic in the plain * server. * <p> * Defaults to {@literal false} * </p> */ public void setSubRequest(boolean theSubRequest) { mySubRequest = theSubRequest; } private class RequestOperationCallback implements IRequestOperationCallback { private List<IServerInterceptor> getInterceptors() { if (getServer() == null) { return Collections.emptyList(); } return getServer().getInterceptors(); } @Override public void resourceCreated(IBaseResource theResource) { for (IServerInterceptor next : getInterceptors()) { if (next instanceof IServerOperationInterceptor) { ((IServerOperationInterceptor) next).resourceCreated(RequestDetails.this, theResource); } } } @Override public void resourceDeleted(IBaseResource theResource) { for (IServerInterceptor next : getInterceptors()) { if (next instanceof IServerOperationInterceptor) { ((IServerOperationInterceptor) next).resourceDeleted(RequestDetails.this, theResource); } } } @Override public void resourcesCreated(Collection<? extends IBaseResource> theResource) { for (IBaseResource next : theResource) { resourceCreated(next); } } @Override public void resourcesDeleted(Collection<? extends IBaseResource> theResource) { for (IBaseResource next : theResource) { resourceDeleted(next); } } @Override public void resourcesUpdated(Collection<? extends IBaseResource> theResource) { for (IBaseResource next : theResource) { resourceUpdated(next); } } @Override public void resourceUpdated(IBaseResource theResource) { for (IServerInterceptor next : getInterceptors()) { if (next instanceof IServerOperationInterceptor) { ((IServerOperationInterceptor) next).resourceUpdated(RequestDetails.this, theResource); } } } } }