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}