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}