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