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