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        public IIdType getId() {
253                return myId;
254        }
255
256        public void setId(IIdType theId) {
257                myId = theId;
258        }
259
260        /**
261         * Returns the attribute map for this request. Attributes are a place for user-supplied
262         * objects of any type to be attached to an individual request. They can be used to pass information
263         * between interceptor methods.
264         */
265        public abstract Object getAttribute(String theAttributeName);
266
267        /**
268         * Returns the attribute map for this request. Attributes are a place for user-supplied
269         * objects of any type to be attached to an individual request. They can be used to pass information
270         * between interceptor methods.
271         */
272        public abstract void setAttribute(String theAttributeName, Object theAttributeValue);
273
274        /**
275         * Retrieves the body of the request as binary data. Either this method or {@link #getReader} may be called to read
276         * the body, not both.
277         *
278         * @return a {@link InputStream} object containing the body of the request
279         * @throws IllegalStateException if the {@link #getReader} method has already been called for this request
280         * @throws IOException           if an input or output exception occurred
281         */
282        public abstract InputStream getInputStream() throws IOException;
283
284        public String getOperation() {
285                return myOperation;
286        }
287
288        public void setOperation(String theOperation) {
289                myOperation = theOperation;
290        }
291
292        public Map<String, String[]> getParameters() {
293                if (myParameters == null) {
294                        myParameters = new HashMap<>();
295                }
296                return Collections.unmodifiableMap(myParameters);
297        }
298
299        public void setParameters(Map<String, String[]> theParams) {
300                myParameters = theParams;
301                myUnqualifiedToQualifiedNames = null;
302
303                // Sanitize keys if necessary to prevent injection attacks
304                boolean needsSanitization = false;
305                for (String nextKey : theParams.keySet()) {
306                        if (UrlUtil.isNeedsSanitization(nextKey)) {
307                                needsSanitization = true;
308                                break;
309                        }
310                }
311                if (needsSanitization) {
312                        myParameters = myParameters.entrySet().stream()
313                                        .collect(
314                                                        Collectors.toMap(t -> UrlUtil.sanitizeUrlPart((String) ((Map.Entry<?, ?>) t).getKey()), t ->
315                                                                        (String[]) ((Map.Entry<?, ?>) t).getValue()));
316                }
317        }
318
319        /**
320         * Retrieves the body of the request as character data using a <code>BufferedReader</code>. The reader translates the
321         * character data according to the character encoding used on the body. Either this method or {@link #getInputStream}
322         * may be called to read the body, not both.
323         *
324         * @return a <code>Reader</code> containing the body of the request
325         * @throws UnsupportedEncodingException if the character set encoding used is not supported and the text cannot be decoded
326         * @throws IllegalStateException        if {@link #getInputStream} method has been called on this request
327         * @throws IOException                  if an input or output exception occurred
328         * @see jakarta.servlet.http.HttpServletRequest#getInputStream
329         */
330        public abstract Reader getReader() throws IOException;
331
332        /**
333         * Returns an invoker that can be called from user code to advise the server interceptors
334         * of any nested operations being invoked within operations. This invoker acts as a proxy for
335         * all interceptors
336         */
337        public IInterceptorBroadcaster getInterceptorBroadcaster() {
338                return myInterceptorBroadcaster;
339        }
340
341        /**
342         * The part of the request URL that comes after the server base.
343         * <p>
344         * Will not contain a leading '/'
345         * </p>
346         */
347        public String getRequestPath() {
348                return myRequestPath;
349        }
350
351        public void setRequestPath(String theRequestPath) {
352                assert theRequestPath.length() == 0 || theRequestPath.charAt(0) != '/';
353                myRequestPath = theRequestPath;
354        }
355
356        public RequestTypeEnum getRequestType() {
357                return myRequestType;
358        }
359
360        public void setRequestType(RequestTypeEnum theRequestType) {
361                myRequestType = theRequestType;
362        }
363
364        public String getResourceName() {
365                return myResourceName;
366        }
367
368        public void setResourceName(String theResourceName) {
369                myResourceName = theResourceName;
370        }
371
372        public IRestfulResponse getResponse() {
373                return myResponse;
374        }
375
376        public void setResponse(IRestfulResponse theResponse) {
377                this.myResponse = theResponse;
378        }
379
380        public RestOperationTypeEnum getRestOperationType() {
381                return myRestOperationType;
382        }
383
384        public void setRestOperationType(RestOperationTypeEnum theRestOperationType) {
385                myRestOperationType = theRestOperationType;
386        }
387
388        public String getSecondaryOperation() {
389                return mySecondaryOperation;
390        }
391
392        public void setSecondaryOperation(String theSecondaryOperation) {
393                mySecondaryOperation = theSecondaryOperation;
394        }
395
396        public abstract IRestfulServerDefaults getServer();
397
398        /**
399         * Returns the server base URL (with no trailing '/') for a given request
400         *
401         * @deprecated Use {@link #getFhirServerBase()} instead. Deprecated in HAPI FHIR 7.0.0
402         */
403        @Deprecated
404        public abstract String getServerBaseForRequest();
405
406        /**
407         * Gets the tenant ID associated with the request. Note that the tenant ID
408         * and the partition ID are not the same thing - Depending on the specific
409         * partition interceptors in use, the tenant ID might be used internally
410         * to derive the partition ID or it might not. Do not assume that it will
411         * be used for this purpose.
412         */
413        public String getTenantId() {
414                return myTenantId;
415        }
416
417        /**
418         * Sets the tenant ID associated with the request. Note that the tenant ID
419         * and the partition ID are not the same thing - Depending on the specific
420         * partition interceptors in use, the tenant ID might be used internally
421         * to derive the partition ID or it might not. Do not assume that it will
422         * be used for this purpose.
423         */
424        public void setTenantId(String theTenantId) {
425                myTenantId = theTenantId;
426        }
427
428        public Map<String, List<String>> getUnqualifiedToQualifiedNames() {
429                if (myUnqualifiedToQualifiedNames == null) {
430                        for (String next : myParameters.keySet()) {
431                                for (int i = 0; i < next.length(); i++) {
432                                        char nextChar = next.charAt(i);
433                                        if (nextChar == ':' || nextChar == '.') {
434                                                if (myUnqualifiedToQualifiedNames == null) {
435                                                        myUnqualifiedToQualifiedNames = new HashMap<>();
436                                                }
437                                                String unqualified = next.substring(0, i);
438                                                List<String> list =
439                                                                myUnqualifiedToQualifiedNames.computeIfAbsent(unqualified, k -> new ArrayList<>(4));
440                                                list.add(next);
441                                                break;
442                                        }
443                                }
444                        }
445                }
446
447                if (myUnqualifiedToQualifiedNames == null) {
448                        myUnqualifiedToQualifiedNames = Collections.emptyMap();
449                }
450
451                return myUnqualifiedToQualifiedNames;
452        }
453
454        /**
455         * Returns a map which can be used to hold any user specific data to pass it from one
456         * part of the request handling chain to another. Data in this map can use any key, although
457         * user code should try to use keys which are specific enough to avoid conflicts.
458         * <p>
459         * A new map is created for each individual request that is handled by the server,
460         * so this map can be used (for example) to pass authorization details from an interceptor
461         * to the resource providers, or for example to pass data from a hook method
462         * on the {@link ca.uhn.fhir.interceptor.api.Pointcut#SERVER_INCOMING_REQUEST_POST_PROCESSED}
463         * to a later hook method on the {@link ca.uhn.fhir.interceptor.api.Pointcut#SERVER_OUTGOING_RESPONSE}
464         * pointcut.
465         * </p>
466         */
467        public Map<Object, Object> getUserData() {
468                if (myUserData == null) {
469                        myUserData = new HashMap<>();
470                }
471                return myUserData;
472        }
473
474        public boolean isRespondGzip() {
475                return myRespondGzip;
476        }
477
478        public void setRespondGzip(boolean theRespondGzip) {
479                myRespondGzip = theRespondGzip;
480        }
481
482        /**
483         * Is this request a sub-request (i.e. a request within a batch or transaction)? This
484         * flag is used internally by hapi-fhir-jpaserver-base, but not used in the plain server
485         * library. You may use it in your client code as a hint when implementing transaction logic in the plain
486         * server.
487         * <p>
488         * Defaults to {@literal false}
489         * </p>
490         */
491        public boolean isSubRequest() {
492                return mySubRequest;
493        }
494
495        /**
496         * Is this request a sub-request (i.e. a request within a batch or transaction)? This
497         * flag is used internally by hapi-fhir-jpaserver-base, but not used in the plain server
498         * library. You may use it in your client code as a hint when implementing transaction logic in the plain
499         * server.
500         * <p>
501         * Defaults to {@literal false}
502         * </p>
503         */
504        public void setSubRequest(boolean theSubRequest) {
505                mySubRequest = theSubRequest;
506        }
507
508        public final byte[] loadRequestContents() {
509                if (myRequestContents == null) {
510                        myRequestContents = getByteStreamRequestContents();
511                }
512                return getRequestContentsIfLoaded();
513        }
514
515        /**
516         * Returns the request contents if they were loaded, returns <code>null</code> otherwise
517         *
518         * @see #loadRequestContents()
519         */
520        public byte[] getRequestContentsIfLoaded() {
521                return myRequestContents;
522        }
523
524        public void removeParameter(String theName) {
525                Validate.notNull(theName, "theName must not be null");
526                getParameters();
527                myParameters.remove(theName);
528        }
529
530        /**
531         * This method may be used to modify the contents of the incoming
532         * request by hardcoding a value which will be used instead of the
533         * value received by the client.
534         * <p>
535         * This method is useful for modifying the request body prior
536         * to parsing within interceptors. It generally only has an
537         * impact when called in the {@link IServerInterceptor#incomingRequestPostProcessed(RequestDetails, HttpServletRequest, HttpServletResponse)}
538         * method
539         * </p>
540         */
541        public void setRequestContents(byte[] theRequestContents) {
542                myRequestContents = theRequestContents;
543        }
544
545        public String getTransactionGuid() {
546                return myTransactionGuid;
547        }
548
549        public void setTransactionGuid(String theTransactionGuid) {
550                myTransactionGuid = theTransactionGuid;
551        }
552
553        public boolean isRewriteHistory() {
554                return myRewriteHistory;
555        }
556
557        public void setRewriteHistory(boolean theRewriteHistory) {
558                myRewriteHistory = theRewriteHistory;
559        }
560
561        public int getMaxRetries() {
562                return myMaxRetries;
563        }
564
565        public void setMaxRetries(int theMaxRetries) {
566                myMaxRetries = theMaxRetries;
567        }
568
569        public boolean isRetry() {
570                return myRetry;
571        }
572
573        public void setRetry(boolean theRetry) {
574                myRetry = theRetry;
575        }
576}