001/*
002 * Copyright 2002-2018 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.method.annotation;
018
019import java.util.Collections;
020import java.util.HashMap;
021import java.util.HashSet;
022import java.util.Map;
023import java.util.Set;
024import java.util.concurrent.ConcurrentHashMap;
025
026import org.springframework.core.annotation.AnnotatedElementUtils;
027import org.springframework.lang.Nullable;
028import org.springframework.util.Assert;
029import org.springframework.web.bind.annotation.SessionAttributes;
030import org.springframework.web.bind.support.SessionAttributeStore;
031import org.springframework.web.bind.support.SessionStatus;
032import org.springframework.web.context.request.WebRequest;
033
034/**
035 * Manages controller-specific session attributes declared via
036 * {@link SessionAttributes @SessionAttributes}. Actual storage is
037 * delegated to a {@link SessionAttributeStore} instance.
038 *
039 * <p>When a controller annotated with {@code @SessionAttributes} adds
040 * attributes to its model, those attributes are checked against names and
041 * types specified via {@code @SessionAttributes}. Matching model attributes
042 * are saved in the HTTP session and remain there until the controller calls
043 * {@link SessionStatus#setComplete()}.
044 *
045 * @author Rossen Stoyanchev
046 * @author Juergen Hoeller
047 * @since 3.1
048 */
049public class SessionAttributesHandler {
050
051        private final Set<String> attributeNames = new HashSet<>();
052
053        private final Set<Class<?>> attributeTypes = new HashSet<>();
054
055        private final Set<String> knownAttributeNames = Collections.newSetFromMap(new ConcurrentHashMap<>(4));
056
057        private final SessionAttributeStore sessionAttributeStore;
058
059
060        /**
061         * Create a new session attributes handler. Session attribute names and types
062         * are extracted from the {@code @SessionAttributes} annotation, if present,
063         * on the given type.
064         * @param handlerType the controller type
065         * @param sessionAttributeStore used for session access
066         */
067        public SessionAttributesHandler(Class<?> handlerType, SessionAttributeStore sessionAttributeStore) {
068                Assert.notNull(sessionAttributeStore, "SessionAttributeStore may not be null");
069                this.sessionAttributeStore = sessionAttributeStore;
070
071                SessionAttributes ann = AnnotatedElementUtils.findMergedAnnotation(handlerType, SessionAttributes.class);
072                if (ann != null) {
073                        Collections.addAll(this.attributeNames, ann.names());
074                        Collections.addAll(this.attributeTypes, ann.types());
075                }
076                this.knownAttributeNames.addAll(this.attributeNames);
077        }
078
079
080        /**
081         * Whether the controller represented by this instance has declared any
082         * session attributes through an {@link SessionAttributes} annotation.
083         */
084        public boolean hasSessionAttributes() {
085                return (!this.attributeNames.isEmpty() || !this.attributeTypes.isEmpty());
086        }
087
088        /**
089         * Whether the attribute name or type match the names and types specified
090         * via {@code @SessionAttributes} on the underlying controller.
091         * <p>Attributes successfully resolved through this method are "remembered"
092         * and subsequently used in {@link #retrieveAttributes(WebRequest)} and
093         * {@link #cleanupAttributes(WebRequest)}.
094         * @param attributeName the attribute name to check
095         * @param attributeType the type for the attribute
096         */
097        public boolean isHandlerSessionAttribute(String attributeName, Class<?> attributeType) {
098                Assert.notNull(attributeName, "Attribute name must not be null");
099                if (this.attributeNames.contains(attributeName) || this.attributeTypes.contains(attributeType)) {
100                        this.knownAttributeNames.add(attributeName);
101                        return true;
102                }
103                else {
104                        return false;
105                }
106        }
107
108        /**
109         * Store a subset of the given attributes in the session. Attributes not
110         * declared as session attributes via {@code @SessionAttributes} are ignored.
111         * @param request the current request
112         * @param attributes candidate attributes for session storage
113         */
114        public void storeAttributes(WebRequest request, Map<String, ?> attributes) {
115                attributes.forEach((name, value) -> {
116                        if (value != null && isHandlerSessionAttribute(name, value.getClass())) {
117                                this.sessionAttributeStore.storeAttribute(request, name, value);
118                        }
119                });
120        }
121
122        /**
123         * Retrieve "known" attributes from the session, i.e. attributes listed
124         * by name in {@code @SessionAttributes} or attributes previously stored
125         * in the model that matched by type.
126         * @param request the current request
127         * @return a map with handler session attributes, possibly empty
128         */
129        public Map<String, Object> retrieveAttributes(WebRequest request) {
130                Map<String, Object> attributes = new HashMap<>();
131                for (String name : this.knownAttributeNames) {
132                        Object value = this.sessionAttributeStore.retrieveAttribute(request, name);
133                        if (value != null) {
134                                attributes.put(name, value);
135                        }
136                }
137                return attributes;
138        }
139
140        /**
141         * Remove "known" attributes from the session, i.e. attributes listed
142         * by name in {@code @SessionAttributes} or attributes previously stored
143         * in the model that matched by type.
144         * @param request the current request
145         */
146        public void cleanupAttributes(WebRequest request) {
147                for (String attributeName : this.knownAttributeNames) {
148                        this.sessionAttributeStore.cleanupAttribute(request, attributeName);
149                }
150        }
151
152        /**
153         * A pass-through call to the underlying {@link SessionAttributeStore}.
154         * @param request the current request
155         * @param attributeName the name of the attribute of interest
156         * @return the attribute value, or {@code null} if none
157         */
158        @Nullable
159        Object retrieveAttribute(WebRequest request, String attributeName) {
160                return this.sessionAttributeStore.retrieveAttribute(request, attributeName);
161        }
162
163}