Skip to content

Commit 15662b8

Browse files
fil512Ken Stevens
andauthored
replace non-servlet usages of request details get/set attribute with getUserData (#7068)
* move three keys that were in request details attributes to user data * updated cdr side * cleanup * inconsequential change to force CI * final fixme * final fixme * change log * cleanup * make RequestDetails thread-safe * fix test regressions * claude review * claude review * claude review * spotless * fix test --------- Co-authored-by: Ken Stevens <[email protected]>
1 parent d221ac4 commit 15662b8

File tree

16 files changed

+598
-78
lines changed

16 files changed

+598
-78
lines changed
Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
---
2+
type: change
3+
issue: 7068
4+
title: "RequestDetails.getAttribute() and RequestDetails.setAttribute() have been deprecated. Users are encouraged to
5+
use getUserData() to share data. If Servlet Attributes are required, users can use the new getServletAttribute()
6+
and setServletAttribute() methods available on ServletRequestDetails."

hapi-fhir-docs/src/main/resources/ca/uhn/hapi/fhir/docs/interceptors/interceptors.md

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -47,3 +47,29 @@ The following example shows an exception handling interceptor which overrides th
4747
{{snippet:classpath:/ca/uhn/hapi/fhir/docs/RequestExceptionInterceptor.java|interceptor}}
4848
```
4949

50+
## Working with RequestDetails in Interceptors
51+
52+
Many interceptor hook methods receive a [RequestDetails](/hapi-fhir/apidocs/hapi-fhir-server/ca/uhn/fhir/rest/api/server/RequestDetails.html) parameter which provides access to request context information. When working with RequestDetails in interceptors:
53+
54+
* **Use `getUserData()`** to pass information between different interceptor methods and hook points. This is the recommended approach for most interceptor use cases.
55+
56+
* **For servlet attributes** (used for communication between servlet filters), use the `getServletAttribute()` and `setServletAttribute()` methods available on servlet-based RequestDetails implementations.
57+
58+
```java
59+
@Hook(Pointcut.SERVER_INCOMING_REQUEST_PRE_HANDLED)
60+
public void preHandle(RequestDetails theRequestDetails) {
61+
// Store data for use in later interceptor methods
62+
theRequestDetails.getUserData().put("START_TIME", System.currentTimeMillis());
63+
}
64+
65+
@Hook(Pointcut.SERVER_OUTGOING_RESPONSE)
66+
public void postHandle(RequestDetails theRequestDetails) {
67+
// Retrieve data stored in earlier interceptor method
68+
Long startTime = (Long) theRequestDetails.getUserData().get("START_TIME");
69+
if (startTime != null) {
70+
long duration = System.currentTimeMillis() - startTime;
71+
// Log or process the duration...
72+
}
73+
}
74+
```
75+

hapi-fhir-docs/src/main/resources/ca/uhn/hapi/fhir/docs/server_plain/resource_providers.md

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -72,6 +72,47 @@ In some cases, it may be useful to have access to the underlying HttpServletRequ
7272
{{snippet:classpath:/ca/uhn/hapi/fhir/docs/RestfulPatientResourceProviderMore.java|underlyingReq}}
7373
```
7474

75+
## Accessing RequestDetails
76+
77+
Many server methods can accept a parameter of type [RequestDetails](/hapi-fhir/apidocs/hapi-fhir-server/ca/uhn/fhir/rest/api/server/RequestDetails.html), which provides access to request context information including headers, parameters, and user data. RequestDetails is commonly used in multitenancy scenarios, interceptors, and custom server logic.
78+
79+
```java
80+
@Read()
81+
public Patient read(@IdParam IdType theId, RequestDetails theRequestDetails) {
82+
// Access tenant information
83+
String tenantId = theRequestDetails.getTenantId();
84+
85+
// Access user data (for passing information between interceptors)
86+
Map<Object, Object> userData = theRequestDetails.getUserData();
87+
88+
// Access headers
89+
String authHeader = theRequestDetails.getHeader("Authorization");
90+
91+
// Your implementation here...
92+
return patient;
93+
}
94+
```
95+
96+
### RequestDetails Best Practices
97+
98+
When working with RequestDetails, it's important to understand the distinction between different types of data storage:
99+
100+
* **User Data** - Use `RequestDetails.getUserData()` to pass information between interceptor methods. This is the recommended approach for most use cases involving interceptors, security, and custom business logic.
101+
102+
* **Servlet Attributes** - For servlet-specific request attributes (used for communication between servlet filters), use the `getServletAttribute()` and `setServletAttribute()` methods available on servlet-based RequestDetails implementations.
103+
104+
```java
105+
// Recommended: Use getUserData() to pass values between different interceptors
106+
theRequestDetails.getUserData().put("CUSTOM_KEY_A", someValue);
107+
theRequestDetails.getUserData().get("CUSTOM_KEY_A");
108+
109+
// To retrieve servlet attributes you can use
110+
if (theRequestDetails instanceof ServletRequestDetails) {
111+
ServletRequestDetails servletDetails = (ServletRequestDetails) theRequestDetails;
112+
Object value = servletDetails.getServletAttribute("SERVLET_ATTR");
113+
}
114+
```
115+
75116
<a name="exceptions"/>
76117

77118
# REST Exception/Error Handling

hapi-fhir-jaxrsserver-base/src/main/java/ca/uhn/fhir/jaxrs/server/util/JaxRsRequest.java

Lines changed: 48 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@
2727
import ca.uhn.fhir.rest.api.Constants;
2828
import ca.uhn.fhir.rest.api.RequestTypeEnum;
2929
import ca.uhn.fhir.rest.api.RestOperationTypeEnum;
30+
import ca.uhn.fhir.rest.api.server.IHasServletAttributes;
3031
import ca.uhn.fhir.rest.api.server.IRestfulResponse;
3132
import ca.uhn.fhir.rest.api.server.RequestDetails;
3233
import ca.uhn.fhir.rest.server.exceptions.InvalidRequestException;
@@ -36,7 +37,6 @@
3637
import jakarta.ws.rs.core.MediaType;
3738
import org.apache.commons.lang3.StringUtils;
3839

39-
import java.io.IOException;
4040
import java.io.InputStream;
4141
import java.io.Reader;
4242
import java.nio.charset.Charset;
@@ -50,7 +50,7 @@
5050
*
5151
* @author Peter Van Houte | [email protected] | Agfa Healthcare
5252
*/
53-
public class JaxRsRequest extends RequestDetails {
53+
public class JaxRsRequest extends RequestDetails implements IHasServletAttributes {
5454

5555
private HttpHeaders myHeaders;
5656
private String myResourceString;
@@ -127,24 +127,60 @@ public void setHeaders(String theName, List<String> theValue) {
127127
throw new UnsupportedOperationException(Msg.code(2500) + "Headers can not be modified in JAX-RS");
128128
}
129129

130+
/**
131+
* Gets an attribute from the servlet request. Attributes are used for interacting with servlet request
132+
* attributes to communicate between servlet filters. These methods should not be used to pass information
133+
* between interceptor methods. Use {@link #getUserData()} instead to pass information
134+
* between interceptor methods.
135+
*
136+
* @param theAttributeName The attribute name
137+
* @return The attribute value, or null if the attribute is not set
138+
*/
130139
@Override
131-
public Object getAttribute(String theAttributeName) {
140+
public Object getServletAttribute(String theAttributeName) {
132141
return myAttributes.get(theAttributeName);
133142
}
134143

144+
/**
145+
* Sets an attribute on the servlet request. Attributes are used for interacting with servlet request
146+
* attributes to communicate between servlet filters. These methods should not be used to pass information
147+
* between interceptor methods. Use {@link #getUserData()} instead to pass information
148+
* between interceptor methods.
149+
*
150+
* @param theAttributeName The attribute name
151+
* @param theAttributeValue The attribute value
152+
*/
135153
@Override
136-
public void setAttribute(String theAttributeName, Object theAttributeValue) {
154+
public void setServletAttribute(String theAttributeName, Object theAttributeValue) {
137155
myAttributes.put(theAttributeName, theAttributeValue);
138156
}
139157

158+
/**
159+
* @deprecated Use {@link #getUserData()}. If servlet attributes are truly required, then use {@link IHasServletAttributes#getServletAttribute(String)}.
160+
*/
161+
@Deprecated
162+
@Override
163+
public Object getAttribute(String theAttributeName) {
164+
return getServletAttribute(theAttributeName);
165+
}
166+
167+
/**
168+
* @deprecated Use {@link #getUserData()}. If servlet attributes are truly required, then use {@link IHasServletAttributes#setServletAttribute(String, Object)}.
169+
*/
170+
@Deprecated
171+
@Override
172+
public void setAttribute(String theAttributeName, Object theAttributeValue) {
173+
setServletAttribute(theAttributeName, theAttributeValue);
174+
}
175+
140176
@Override
141177
public InputStream getInputStream() {
142178
// not yet implemented
143179
throw new UnsupportedOperationException(Msg.code(599));
144180
}
145181

146182
@Override
147-
public Reader getReader() throws IOException {
183+
public Reader getReader() {
148184
// not yet implemented
149185
throw new UnsupportedOperationException(Msg.code(600));
150186
}
@@ -181,13 +217,14 @@ public String getServerBaseForRequest() {
181217
*/
182218
public static class Builder {
183219
private final String myResourceName;
220+
private final RequestTypeEnum myRequestType;
221+
private final String myRequestUrl;
222+
private final RestOperationTypeEnum myRestOperation;
223+
private final AbstractJaxRsProvider myServer;
224+
225+
private String myResource;
184226
private String myCompartment;
185227
private String myId;
186-
private RequestTypeEnum myRequestType;
187-
private String myRequestUrl;
188-
private String myResource;
189-
private RestOperationTypeEnum myRestOperation;
190-
private AbstractJaxRsProvider myServer;
191228
private String myVersion;
192229

193230
/**
@@ -196,7 +233,7 @@ public static class Builder {
196233
* @param theServer the server
197234
* @param theRequestType the request type
198235
* @param theRestOperation the rest operation
199-
* @param theRequestUrl
236+
* @param theRequestUrl the request url
200237
*/
201238
public Builder(
202239
AbstractJaxRsProvider theServer,
Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
/*
2+
* #%L
3+
* HAPI FHIR - Server Framework
4+
* %%
5+
* Copyright (C) 2014 - 2025 Smile CDR, Inc.
6+
* %%
7+
* Licensed under the Apache License, Version 2.0 (the "License");
8+
* you may not use this file except in compliance with the License.
9+
* You may obtain a copy of the License at
10+
*
11+
* http://www.apache.org/licenses/LICENSE-2.0
12+
*
13+
* Unless required by applicable law or agreed to in writing, software
14+
* distributed under the License is distributed on an "AS IS" BASIS,
15+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
16+
* See the License for the specific language governing permissions and
17+
* limitations under the License.
18+
* #L%
19+
*/
20+
package ca.uhn.fhir.rest.api.server;
21+
22+
public interface IHasServletAttributes {
23+
24+
/**
25+
* Gets an attribute from the servlet request. Attributes are used for interacting with servlet request
26+
* attributes to communicate between servlet filters. These methods should not be used to pass information
27+
* between interceptor methods. Use {@link RequestDetails#getUserData()} instead to pass information
28+
* between interceptor methods.
29+
*
30+
* @param theAttributeName The attribute name
31+
* @return The attribute value, or null if the attribute is not set
32+
*/
33+
Object getServletAttribute(String theAttributeName);
34+
35+
/**
36+
* Sets an attribute on the servlet request. Attributes are used for interacting with servlet request
37+
* attributes to communicate between servlet filters. These methods should not be used to pass information
38+
* between interceptor methods. Use {@link RequestDetails#getUserData()} instead to pass information
39+
* between interceptor methods.
40+
*
41+
* @param theAttributeName The attribute name
42+
* @param theAttributeValue The attribute value
43+
*/
44+
void setServletAttribute(String theAttributeName, Object theAttributeValue);
45+
}

hapi-fhir-server/src/main/java/ca/uhn/fhir/rest/api/server/RequestDetails.java

Lines changed: 38 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -31,12 +31,15 @@
3131
import ca.uhn.fhir.rest.server.interceptor.IServerInterceptor;
3232
import ca.uhn.fhir.util.StopWatch;
3333
import ca.uhn.fhir.util.UrlUtil;
34+
import jakarta.annotation.Nonnull;
3435
import jakarta.servlet.http.HttpServletRequest;
3536
import jakarta.servlet.http.HttpServletResponse;
3637
import org.apache.commons.lang3.ArrayUtils;
3738
import org.apache.commons.lang3.Validate;
3839
import org.hl7.fhir.instance.model.api.IBaseResource;
3940
import org.hl7.fhir.instance.model.api.IIdType;
41+
import org.slf4j.Logger;
42+
import org.slf4j.LoggerFactory;
4043

4144
import java.io.IOException;
4245
import java.io.InputStream;
@@ -54,11 +57,14 @@
5457
import static org.apache.commons.lang3.StringUtils.isBlank;
5558

5659
public abstract class RequestDetails {
57-
60+
private static final Logger ourLog = LoggerFactory.getLogger(RequestDetails.class);
5861
public static final byte[] BAD_STREAM_PLACEHOLDER =
5962
(Msg.code(2543) + "PLACEHOLDER WHEN READING FROM BAD STREAM").getBytes(StandardCharsets.UTF_8);
63+
6064
private final StopWatch myRequestStopwatch;
61-
private IInterceptorBroadcaster myInterceptorBroadcaster;
65+
private final IInterceptorBroadcaster myInterceptorBroadcaster;
66+
private final Map<Object, Object> myUserData = new UserDataMap();
67+
6268
private String myTenantId;
6369
private String myCompartmentName;
6470
private String myCompleteUrl;
@@ -76,7 +82,6 @@ public abstract class RequestDetails {
7682
private String mySecondaryOperation;
7783
private boolean mySubRequest;
7884
private Map<String, List<String>> myUnqualifiedToQualifiedNames;
79-
private Map<Object, Object> myUserData;
8085
private IBaseResource myResource;
8186
private String myRequestId;
8287
private String myTransactionGuid;
@@ -116,11 +121,14 @@ public RequestDetails(RequestDetails theRequestDetails) {
116121
mySecondaryOperation = theRequestDetails.getSecondaryOperation();
117122
mySubRequest = theRequestDetails.isSubRequest();
118123
myUnqualifiedToQualifiedNames = theRequestDetails.getUnqualifiedToQualifiedNames();
119-
myUserData = theRequestDetails.getUserData();
124+
myUserData.putAll(theRequestDetails.getUserData());
120125
myResource = theRequestDetails.getResource();
121126
myRequestId = theRequestDetails.getRequestId();
122127
myTransactionGuid = theRequestDetails.getTransactionGuid();
123128
myFixedConditionalUrl = theRequestDetails.getFixedConditionalUrl();
129+
myRewriteHistory = theRequestDetails.isRewriteHistory();
130+
myMaxRetries = theRequestDetails.getMaxRetries();
131+
myRetry = theRequestDetails.isRetry();
124132
}
125133

126134
public String getFixedConditionalUrl() {
@@ -259,7 +267,7 @@ public void setFhirServerBase(String theFhirServerBase) {
259267
/**
260268
* Adds a new header
261269
*
262-
* @param theName The header name
270+
* @param theName The header name
263271
* @param theValue The header value
264272
* @since 7.2.0
265273
*/
@@ -268,7 +276,7 @@ public void setFhirServerBase(String theFhirServerBase) {
268276
/**
269277
* Replaces any existing header(s) with the given name using a List of new header values
270278
*
271-
* @param theName The header name
279+
* @param theName The header name
272280
* @param theValue The header value
273281
* @since 7.2.0
274282
*/
@@ -283,18 +291,27 @@ public void setId(IIdType theId) {
283291
}
284292

285293
/**
286-
* Returns the attribute map for this request. Attributes are a place for user-supplied
287-
* objects of any type to be attached to an individual request. They can be used to pass information
288-
* between interceptor methods.
294+
* @deprecated Use {@link #getUserData()}. If servlet attributes are truly required, then use {@link IHasServletAttributes#getServletAttribute(String)}.
289295
*/
290-
public abstract Object getAttribute(String theAttributeName);
296+
@Deprecated
297+
public Object getAttribute(String theAttributeName) {
298+
ourLog.error(
299+
"{} is not a servlet request. Unable to get attribute {}",
300+
getClass().getName(),
301+
theAttributeName);
302+
return null;
303+
}
291304

292305
/**
293-
* Returns the attribute map for this request. Attributes are a place for user-supplied
294-
* objects of any type to be attached to an individual request. They can be used to pass information
295-
* between interceptor methods.
306+
* @deprecated Use {@link #getUserData()}. If servlet attributes are truly required, then use {@link IHasServletAttributes#setServletAttribute(String, Object)}.
296307
*/
297-
public abstract void setAttribute(String theAttributeName, Object theAttributeValue);
308+
@Deprecated
309+
public void setAttribute(String theAttributeName, Object theAttributeValue) {
310+
ourLog.error(
311+
"{} is not a servlet request. Unable to set attribute {}",
312+
getClass().getName(),
313+
theAttributeName);
314+
}
298315

299316
/**
300317
* Retrieves the body of the request as binary data. Either this method or {@link #getReader} may be called to read
@@ -374,7 +391,7 @@ public String getRequestPath() {
374391
}
375392

376393
public void setRequestPath(String theRequestPath) {
377-
assert theRequestPath.length() == 0 || theRequestPath.charAt(0) != '/';
394+
assert theRequestPath.isEmpty() || theRequestPath.charAt(0) != '/';
378395
myRequestPath = theRequestPath;
379396
}
380397

@@ -488,11 +505,14 @@ public Map<String, List<String>> getUnqualifiedToQualifiedNames() {
488505
* to a later hook method on the {@link ca.uhn.fhir.interceptor.api.Pointcut#SERVER_OUTGOING_RESPONSE}
489506
* pointcut.
490507
* </p>
508+
* <p>
509+
* This method should be used to pass information between interceptor methods. For servlet-specific
510+
* request attributes used to communicate between servlet filters, use {@link IHasServletAttributes}
511+
* methods available on implementations that support them.
512+
* </p>
491513
*/
514+
@Nonnull
492515
public Map<Object, Object> getUserData() {
493-
if (myUserData == null) {
494-
myUserData = new HashMap<>();
495-
}
496516
return myUserData;
497517
}
498518

@@ -545,7 +565,6 @@ public final synchronized byte[] loadRequestContents() {
545565
myRequestContents = BAD_STREAM_PLACEHOLDER;
546566
}
547567
}
548-
assert myRequestContents != null : "We must not re-read the stream.";
549568
}
550569
return getRequestContentsIfLoaded();
551570
}

0 commit comments

Comments
 (0)