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