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.server.servlet;
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.server.IHasServletAttributes;
028import ca.uhn.fhir.rest.api.server.RequestDetails;
029import ca.uhn.fhir.rest.server.RestfulServer;
030import ca.uhn.fhir.rest.server.RestfulServerUtils;
031import ca.uhn.fhir.rest.server.exceptions.InvalidRequestException;
032import com.google.common.collect.ListMultimap;
033import com.google.common.collect.MultimapBuilder;
034import jakarta.annotation.Nonnull;
035import jakarta.servlet.http.HttpServletRequest;
036import jakarta.servlet.http.HttpServletResponse;
037import org.apache.commons.io.IOUtils;
038import org.apache.commons.lang3.Validate;
039
040import java.io.ByteArrayInputStream;
041import java.io.IOException;
042import java.io.InputStream;
043import java.io.Reader;
044import java.nio.charset.Charset;
045import java.util.ArrayList;
046import java.util.Collections;
047import java.util.Enumeration;
048import java.util.HashMap;
049import java.util.Iterator;
050import java.util.List;
051import java.util.Map;
052import java.util.StringTokenizer;
053import java.util.zip.GZIPInputStream;
054
055import static org.apache.commons.lang3.StringUtils.isNotBlank;
056import static org.apache.commons.lang3.StringUtils.trim;
057
058public class ServletRequestDetails extends RequestDetails implements IHasServletAttributes {
059
060        private static final org.slf4j.Logger ourLog = org.slf4j.LoggerFactory.getLogger(ServletRequestDetails.class);
061
062        private RestfulServer myServer;
063        private HttpServletRequest myServletRequest;
064        private HttpServletResponse myServletResponse;
065        private ListMultimap<String, String> myHeaders;
066
067        /**
068         * Constructor for testing only
069         */
070        public ServletRequestDetails() {
071                this((IInterceptorBroadcaster) null);
072        }
073
074        /**
075         * Constructor
076         */
077        public ServletRequestDetails(IInterceptorBroadcaster theInterceptorBroadcaster) {
078                super(theInterceptorBroadcaster);
079                setResponse(new ServletRestfulResponse(this));
080        }
081
082        /**
083         * Copy constructor
084         */
085        public ServletRequestDetails(ServletRequestDetails theRequestDetails) {
086                super(theRequestDetails);
087
088                myServer = theRequestDetails.getServer();
089                myServletRequest = theRequestDetails.getServletRequest();
090                myServletResponse = theRequestDetails.getServletResponse();
091                if (myHeaders != null) {
092                        myHeaders.putAll(theRequestDetails.myHeaders);
093                }
094        }
095
096        @Override
097        protected byte[] getByteStreamRequestContents() {
098                try {
099                        InputStream inputStream = getInputStream();
100                        byte[] requestContents = IOUtils.toByteArray(inputStream);
101
102                        if (myServer.isUncompressIncomingContents()) {
103                                String contentEncoding = myServletRequest.getHeader(Constants.HEADER_CONTENT_ENCODING);
104                                if ("gzip".equals(contentEncoding)) {
105                                        ourLog.debug("Uncompressing (GZip) incoming content");
106                                        if (requestContents.length > 0) {
107                                                GZIPInputStream gis = new GZIPInputStream(new ByteArrayInputStream(requestContents));
108                                                requestContents = IOUtils.toByteArray(gis);
109                                        }
110                                }
111                        }
112                        return requestContents;
113                } catch (IOException e) {
114                        ourLog.error("Could not load request resource", e);
115                        throw new InvalidRequestException(
116                                        Msg.code(308) + String.format("Could not load request resource: %s", e.getMessage()));
117                }
118        }
119
120        @Override
121        public Charset getCharset() {
122                Charset charset = null;
123
124                String charsetString = myServletRequest.getCharacterEncoding();
125                if (isNotBlank(charsetString)) {
126                        charset = Charset.forName(charsetString);
127                }
128
129                return charset;
130        }
131
132        @Override
133        public FhirContext getFhirContext() {
134                return getServer().getFhirContext();
135        }
136
137        @Override
138        public String getHeader(String name) {
139                // For efficiency, we only make a copy of the request headers if we need to
140                // modify them
141                if (myHeaders != null) {
142                        List<String> values = myHeaders.get(name);
143                        if (values.isEmpty()) {
144                                return null;
145                        } else {
146                                return values.get(0);
147                        }
148                }
149                return getServletRequest().getHeader(name);
150        }
151
152        @Override
153        public List<String> getHeaders(String name) {
154                // For efficiency, we only make a copy of the request headers if we need to
155                // modify them
156                if (myHeaders != null) {
157                        return myHeaders.get(name);
158                }
159                Enumeration<String> headers = getServletRequest().getHeaders(name);
160                return headers == null
161                                ? Collections.emptyList()
162                                : Collections.list(getServletRequest().getHeaders(name));
163        }
164
165        @Override
166        public void addHeader(String theName, String theValue) {
167                initHeaders();
168                myHeaders.put(theName, theValue);
169        }
170
171        @Override
172        public void setHeaders(String theName, List<String> theValue) {
173                initHeaders();
174                myHeaders.removeAll(theName);
175                myHeaders.putAll(theName, theValue);
176        }
177
178        private void initHeaders() {
179                if (myHeaders == null) {
180                        // Make sure we are case-insensitive for header names
181                        myHeaders = MultimapBuilder.treeKeys(String.CASE_INSENSITIVE_ORDER)
182                                        .arrayListValues()
183                                        .build();
184
185                        Enumeration<String> headerNames = getServletRequest().getHeaderNames();
186                        while (headerNames.hasMoreElements()) {
187                                String nextName = headerNames.nextElement();
188                                Enumeration<String> values = getServletRequest().getHeaders(nextName);
189                                while (values.hasMoreElements()) {
190                                        myHeaders.put(nextName, values.nextElement());
191                                }
192                        }
193                }
194        }
195
196        /**
197         * Gets an attribute from the servlet request. Attributes are used for interacting with servlet request
198         * attributes to communicate between servlet filters. These methods should not be used to pass information
199         * between interceptor methods. Use {@link #getUserData()} instead to pass information
200         * between interceptor methods.
201         *
202         * @param theAttributeName The attribute name
203         * @return The attribute value, or null if the attribute is not set
204         */
205        @Override
206        public Object getServletAttribute(String theAttributeName) {
207                Validate.notBlank(theAttributeName, "theAttributeName must not be null or blank");
208                return getServletRequest().getAttribute(theAttributeName);
209        }
210
211        /**
212         * Sets an attribute on the servlet request. Attributes are used for interacting with servlet request
213         * attributes to communicate between servlet filters. These methods should not be used to pass information
214         * between interceptor methods. Use {@link #getUserData()} instead to pass information
215         * between interceptor methods.
216         *
217         * @param theAttributeName The attribute name
218         * @param theAttributeValue The attribute value
219         */
220        @Override
221        public void setServletAttribute(String theAttributeName, Object theAttributeValue) {
222                Validate.notBlank(theAttributeName, "theAttributeName must not be null or blank");
223                getServletRequest().setAttribute(theAttributeName, theAttributeValue);
224        }
225
226        /**
227         * @deprecated Use {@link #getUserData()}. If servlet attributes are truly required, then use {@link IHasServletAttributes#getServletAttribute(String)}.
228         */
229        @Deprecated
230        @Override
231        public Object getAttribute(String theAttributeName) {
232                return getServletAttribute(theAttributeName);
233        }
234
235        /**
236         * @deprecated Use {@link #getUserData()}. If servlet attributes are truly required, then use {@link IHasServletAttributes#setServletAttribute(String, Object)}.
237         */
238        @Deprecated
239        @Override
240        public void setAttribute(String theAttributeName, Object theAttributeValue) {
241                setServletAttribute(theAttributeName, theAttributeValue);
242        }
243
244        @Override
245        public InputStream getInputStream() throws IOException {
246                return getServletRequest().getInputStream();
247        }
248
249        @Override
250        public Reader getReader() throws IOException {
251                return getServletRequest().getReader();
252        }
253
254        @Override
255        public RestfulServer getServer() {
256                return myServer;
257        }
258
259        @Override
260        public String getServerBaseForRequest() {
261                return getServer().getServerBaseForRequest(this);
262        }
263
264        public HttpServletRequest getServletRequest() {
265                return myServletRequest;
266        }
267
268        public HttpServletResponse getServletResponse() {
269                return myServletResponse;
270        }
271
272        public void setServer(RestfulServer theServer) {
273                this.myServer = theServer;
274        }
275
276        public ServletRequestDetails setServletRequest(@Nonnull HttpServletRequest myServletRequest) {
277                this.myServletRequest = myServletRequest;
278
279                // TODO KHS move a bunch of other initialization from RestfulServer into this method
280                if ("true".equals(myServletRequest.getHeader(Constants.HEADER_REWRITE_HISTORY))) {
281                        setRewriteHistory(true);
282                }
283                setRetryFields(myServletRequest);
284                return this;
285        }
286
287        private void setRetryFields(HttpServletRequest theRequest) {
288                if (theRequest == null) {
289                        return;
290                }
291                Enumeration<String> headers = theRequest.getHeaders(Constants.HEADER_RETRY_ON_VERSION_CONFLICT);
292                if (headers != null) {
293                        Iterator<String> headerIterator = headers.asIterator();
294                        while (headerIterator.hasNext()) {
295                                String headerValue = headerIterator.next();
296                                if (isNotBlank(headerValue)) {
297                                        StringTokenizer tok = new StringTokenizer(headerValue, ";");
298                                        while (tok.hasMoreTokens()) {
299                                                String next = trim(tok.nextToken());
300                                                if (next.equals(Constants.HEADER_RETRY)) {
301                                                        setRetry(true);
302                                                } else if (next.startsWith(Constants.HEADER_MAX_RETRIES + "=")) {
303                                                        String val = trim(next.substring((Constants.HEADER_MAX_RETRIES + "=").length()));
304                                                        int maxRetries = Integer.parseInt(val);
305                                                        setMaxRetries(maxRetries);
306                                                }
307                                        }
308                                }
309                        }
310                }
311        }
312
313        public void setServletResponse(HttpServletResponse myServletResponse) {
314                this.myServletResponse = myServletResponse;
315        }
316
317        public Map<String, List<String>> getHeaders() {
318                Map<String, List<String>> retVal = new HashMap<>();
319                Enumeration<String> names = myServletRequest.getHeaderNames();
320                while (names.hasMoreElements()) {
321                        String nextName = names.nextElement();
322                        ArrayList<String> headerValues = new ArrayList<>();
323                        retVal.put(nextName, headerValues);
324                        Enumeration<String> valuesEnum = myServletRequest.getHeaders(nextName);
325                        while (valuesEnum.hasMoreElements()) {
326                                headerValues.add(valuesEnum.nextElement());
327                        }
328                }
329                return Collections.unmodifiableMap(retVal);
330        }
331
332        /**
333         * Returns true if the `Prefer` header contains a value of `respond-async`
334         */
335        public boolean isPreferRespondAsync() {
336                String preferHeader = getHeader(Constants.HEADER_PREFER);
337                PreferHeader prefer = RestfulServerUtils.parsePreferHeader(null, preferHeader);
338                return prefer.getRespondAsync();
339        }
340}