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}