001/*
002 * Copyright 2002-2019 the original author or authors.
003 *
004 * Licensed under the Apache License, Version 2.0 (the "License");
005 * you may not use this file except in compliance with the License.
006 * You may obtain a copy of the License at
007 *
008 *      https://www.apache.org/licenses/LICENSE-2.0
009 *
010 * Unless required by applicable law or agreed to in writing, software
011 * distributed under the License is distributed on an "AS IS" BASIS,
012 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
013 * See the License for the specific language governing permissions and
014 * limitations under the License.
015 */
016
017package org.springframework.web.context.request;
018
019import java.util.HashSet;
020import java.util.Map;
021import java.util.Set;
022import java.util.concurrent.ConcurrentHashMap;
023
024import javax.servlet.http.HttpServletRequest;
025import javax.servlet.http.HttpServletResponse;
026import javax.servlet.http.HttpSession;
027
028import org.springframework.lang.Nullable;
029import org.springframework.util.Assert;
030import org.springframework.util.NumberUtils;
031import org.springframework.util.StringUtils;
032import org.springframework.web.util.WebUtils;
033
034/**
035 * Servlet-based implementation of the {@link RequestAttributes} interface.
036 *
037 * <p>Accesses objects from servlet request and HTTP session scope,
038 * with no distinction between "session" and "global session".
039 *
040 * @author Juergen Hoeller
041 * @since 2.0
042 * @see javax.servlet.ServletRequest#getAttribute
043 * @see javax.servlet.http.HttpSession#getAttribute
044 */
045public class ServletRequestAttributes extends AbstractRequestAttributes {
046
047        /**
048         * Constant identifying the {@link String} prefixed to the name of a
049         * destruction callback when it is stored in a {@link HttpSession}.
050         */
051        public static final String DESTRUCTION_CALLBACK_NAME_PREFIX =
052                        ServletRequestAttributes.class.getName() + ".DESTRUCTION_CALLBACK.";
053
054        protected static final Set<Class<?>> immutableValueTypes = new HashSet<>(16);
055
056        static {
057                immutableValueTypes.addAll(NumberUtils.STANDARD_NUMBER_TYPES);
058                immutableValueTypes.add(Boolean.class);
059                immutableValueTypes.add(Character.class);
060                immutableValueTypes.add(String.class);
061        }
062
063
064        private final HttpServletRequest request;
065
066        @Nullable
067        private HttpServletResponse response;
068
069        @Nullable
070        private volatile HttpSession session;
071
072        private final Map<String, Object> sessionAttributesToUpdate = new ConcurrentHashMap<>(1);
073
074
075        /**
076         * Create a new ServletRequestAttributes instance for the given request.
077         * @param request current HTTP request
078         */
079        public ServletRequestAttributes(HttpServletRequest request) {
080                Assert.notNull(request, "Request must not be null");
081                this.request = request;
082        }
083
084        /**
085         * Create a new ServletRequestAttributes instance for the given request.
086         * @param request current HTTP request
087         * @param response current HTTP response (for optional exposure)
088         */
089        public ServletRequestAttributes(HttpServletRequest request, @Nullable HttpServletResponse response) {
090                this(request);
091                this.response = response;
092        }
093
094
095        /**
096         * Exposes the native {@link HttpServletRequest} that we're wrapping.
097         */
098        public final HttpServletRequest getRequest() {
099                return this.request;
100        }
101
102        /**
103         * Exposes the native {@link HttpServletResponse} that we're wrapping (if any).
104         */
105        @Nullable
106        public final HttpServletResponse getResponse() {
107                return this.response;
108        }
109
110        /**
111         * Exposes the {@link HttpSession} that we're wrapping.
112         * @param allowCreate whether to allow creation of a new session if none exists yet
113         */
114        @Nullable
115        protected final HttpSession getSession(boolean allowCreate) {
116                if (isRequestActive()) {
117                        HttpSession session = this.request.getSession(allowCreate);
118                        this.session = session;
119                        return session;
120                }
121                else {
122                        // Access through stored session reference, if any...
123                        HttpSession session = this.session;
124                        if (session == null) {
125                                if (allowCreate) {
126                                        throw new IllegalStateException(
127                                                        "No session found and request already completed - cannot create new session!");
128                                }
129                                else {
130                                        session = this.request.getSession(false);
131                                        this.session = session;
132                                }
133                        }
134                        return session;
135                }
136        }
137
138        private HttpSession obtainSession() {
139                HttpSession session = getSession(true);
140                Assert.state(session != null, "No HttpSession");
141                return session;
142        }
143
144
145        @Override
146        public Object getAttribute(String name, int scope) {
147                if (scope == SCOPE_REQUEST) {
148                        if (!isRequestActive()) {
149                                throw new IllegalStateException(
150                                                "Cannot ask for request attribute - request is not active anymore!");
151                        }
152                        return this.request.getAttribute(name);
153                }
154                else {
155                        HttpSession session = getSession(false);
156                        if (session != null) {
157                                try {
158                                        Object value = session.getAttribute(name);
159                                        if (value != null) {
160                                                this.sessionAttributesToUpdate.put(name, value);
161                                        }
162                                        return value;
163                                }
164                                catch (IllegalStateException ex) {
165                                        // Session invalidated - shouldn't usually happen.
166                                }
167                        }
168                        return null;
169                }
170        }
171
172        @Override
173        public void setAttribute(String name, Object value, int scope) {
174                if (scope == SCOPE_REQUEST) {
175                        if (!isRequestActive()) {
176                                throw new IllegalStateException(
177                                                "Cannot set request attribute - request is not active anymore!");
178                        }
179                        this.request.setAttribute(name, value);
180                }
181                else {
182                        HttpSession session = obtainSession();
183                        this.sessionAttributesToUpdate.remove(name);
184                        session.setAttribute(name, value);
185                }
186        }
187
188        @Override
189        public void removeAttribute(String name, int scope) {
190                if (scope == SCOPE_REQUEST) {
191                        if (isRequestActive()) {
192                                removeRequestDestructionCallback(name);
193                                this.request.removeAttribute(name);
194                        }
195                }
196                else {
197                        HttpSession session = getSession(false);
198                        if (session != null) {
199                                this.sessionAttributesToUpdate.remove(name);
200                                try {
201                                        session.removeAttribute(DESTRUCTION_CALLBACK_NAME_PREFIX + name);
202                                        session.removeAttribute(name);
203                                }
204                                catch (IllegalStateException ex) {
205                                        // Session invalidated - shouldn't usually happen.
206                                }
207                        }
208                }
209        }
210
211        @Override
212        public String[] getAttributeNames(int scope) {
213                if (scope == SCOPE_REQUEST) {
214                        if (!isRequestActive()) {
215                                throw new IllegalStateException(
216                                                "Cannot ask for request attributes - request is not active anymore!");
217                        }
218                        return StringUtils.toStringArray(this.request.getAttributeNames());
219                }
220                else {
221                        HttpSession session = getSession(false);
222                        if (session != null) {
223                                try {
224                                        return StringUtils.toStringArray(session.getAttributeNames());
225                                }
226                                catch (IllegalStateException ex) {
227                                        // Session invalidated - shouldn't usually happen.
228                                }
229                        }
230                        return new String[0];
231                }
232        }
233
234        @Override
235        public void registerDestructionCallback(String name, Runnable callback, int scope) {
236                if (scope == SCOPE_REQUEST) {
237                        registerRequestDestructionCallback(name, callback);
238                }
239                else {
240                        registerSessionDestructionCallback(name, callback);
241                }
242        }
243
244        @Override
245        public Object resolveReference(String key) {
246                if (REFERENCE_REQUEST.equals(key)) {
247                        return this.request;
248                }
249                else if (REFERENCE_SESSION.equals(key)) {
250                        return getSession(true);
251                }
252                else {
253                        return null;
254                }
255        }
256
257        @Override
258        public String getSessionId() {
259                return obtainSession().getId();
260        }
261
262        @Override
263        public Object getSessionMutex() {
264                return WebUtils.getSessionMutex(obtainSession());
265        }
266
267
268        /**
269         * Update all accessed session attributes through {@code session.setAttribute}
270         * calls, explicitly indicating to the container that they might have been modified.
271         */
272        @Override
273        protected void updateAccessedSessionAttributes() {
274                if (!this.sessionAttributesToUpdate.isEmpty()) {
275                        // Update all affected session attributes.
276                        HttpSession session = getSession(false);
277                        if (session != null) {
278                                try {
279                                        for (Map.Entry<String, Object> entry : this.sessionAttributesToUpdate.entrySet()) {
280                                                String name = entry.getKey();
281                                                Object newValue = entry.getValue();
282                                                Object oldValue = session.getAttribute(name);
283                                                if (oldValue == newValue && !isImmutableSessionAttribute(name, newValue)) {
284                                                        session.setAttribute(name, newValue);
285                                                }
286                                        }
287                                }
288                                catch (IllegalStateException ex) {
289                                        // Session invalidated - shouldn't usually happen.
290                                }
291                        }
292                        this.sessionAttributesToUpdate.clear();
293                }
294        }
295
296        /**
297         * Determine whether the given value is to be considered as an immutable session
298         * attribute, that is, doesn't have to be re-set via {@code session.setAttribute}
299         * since its value cannot meaningfully change internally.
300         * <p>The default implementation returns {@code true} for {@code String},
301         * {@code Character}, {@code Boolean} and standard {@code Number} values.
302         * @param name the name of the attribute
303         * @param value the corresponding value to check
304         * @return {@code true} if the value is to be considered as immutable for the
305         * purposes of session attribute management; {@code false} otherwise
306         * @see #updateAccessedSessionAttributes()
307         */
308        protected boolean isImmutableSessionAttribute(String name, @Nullable Object value) {
309                return (value == null || immutableValueTypes.contains(value.getClass()));
310        }
311
312        /**
313         * Register the given callback as to be executed after session termination.
314         * <p>Note: The callback object should be serializable in order to survive
315         * web app restarts.
316         * @param name the name of the attribute to register the callback for
317         * @param callback the callback to be executed for destruction
318         */
319        protected void registerSessionDestructionCallback(String name, Runnable callback) {
320                HttpSession session = obtainSession();
321                session.setAttribute(DESTRUCTION_CALLBACK_NAME_PREFIX + name,
322                                new DestructionCallbackBindingListener(callback));
323        }
324
325
326        @Override
327        public String toString() {
328                return this.request.toString();
329        }
330
331}