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