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