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