
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}