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