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