001/*
002 * Copyright 2002-2020 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.servlet.support;
018
019import java.util.Collections;
020import java.util.LinkedList;
021import java.util.List;
022import java.util.Map;
023import java.util.concurrent.CopyOnWriteArrayList;
024
025import javax.servlet.http.HttpServletRequest;
026import javax.servlet.http.HttpServletResponse;
027
028import org.apache.commons.logging.Log;
029import org.apache.commons.logging.LogFactory;
030
031import org.springframework.lang.Nullable;
032import org.springframework.util.Assert;
033import org.springframework.util.CollectionUtils;
034import org.springframework.util.MultiValueMap;
035import org.springframework.util.StringUtils;
036import org.springframework.web.servlet.FlashMap;
037import org.springframework.web.servlet.FlashMapManager;
038import org.springframework.web.util.UrlPathHelper;
039
040/**
041 * A base class for {@link FlashMapManager} implementations.
042 *
043 * @author Rossen Stoyanchev
044 * @author Juergen Hoeller
045 * @author Sam Brannen
046 * @since 3.1.1
047 */
048public abstract class AbstractFlashMapManager implements FlashMapManager {
049
050        private static final Object DEFAULT_FLASH_MAPS_MUTEX = new Object();
051
052
053        protected final Log logger = LogFactory.getLog(getClass());
054
055        private int flashMapTimeout = 180;
056
057        private UrlPathHelper urlPathHelper = UrlPathHelper.defaultInstance;
058
059
060        /**
061         * Set the amount of time in seconds after a {@link FlashMap} is saved
062         * (at request completion) and before it expires.
063         * <p>The default value is 180 seconds.
064         */
065        public void setFlashMapTimeout(int flashMapTimeout) {
066                this.flashMapTimeout = flashMapTimeout;
067        }
068
069        /**
070         * Return the amount of time in seconds before a FlashMap expires.
071         */
072        public int getFlashMapTimeout() {
073                return this.flashMapTimeout;
074        }
075
076        /**
077         * Set the UrlPathHelper to use to match FlashMap instances to requests.
078         */
079        public void setUrlPathHelper(UrlPathHelper urlPathHelper) {
080                Assert.notNull(urlPathHelper, "UrlPathHelper must not be null");
081                this.urlPathHelper = urlPathHelper;
082        }
083
084        /**
085         * Return the UrlPathHelper implementation to use.
086         */
087        public UrlPathHelper getUrlPathHelper() {
088                return this.urlPathHelper;
089        }
090
091
092        @Override
093        @Nullable
094        public final FlashMap retrieveAndUpdate(HttpServletRequest request, HttpServletResponse response) {
095                List<FlashMap> allFlashMaps = retrieveFlashMaps(request);
096                if (CollectionUtils.isEmpty(allFlashMaps)) {
097                        return null;
098                }
099
100                List<FlashMap> mapsToRemove = getExpiredFlashMaps(allFlashMaps);
101                FlashMap match = getMatchingFlashMap(allFlashMaps, request);
102                if (match != null) {
103                        mapsToRemove.add(match);
104                }
105
106                if (!mapsToRemove.isEmpty()) {
107                        Object mutex = getFlashMapsMutex(request);
108                        if (mutex != null) {
109                                synchronized (mutex) {
110                                        allFlashMaps = retrieveFlashMaps(request);
111                                        if (allFlashMaps != null) {
112                                                allFlashMaps.removeAll(mapsToRemove);
113                                                updateFlashMaps(allFlashMaps, request, response);
114                                        }
115                                }
116                        }
117                        else {
118                                allFlashMaps.removeAll(mapsToRemove);
119                                updateFlashMaps(allFlashMaps, request, response);
120                        }
121                }
122
123                return match;
124        }
125
126        /**
127         * Return a list of expired FlashMap instances contained in the given list.
128         */
129        private List<FlashMap> getExpiredFlashMaps(List<FlashMap> allMaps) {
130                List<FlashMap> result = new LinkedList<>();
131                for (FlashMap map : allMaps) {
132                        if (map.isExpired()) {
133                                result.add(map);
134                        }
135                }
136                return result;
137        }
138
139        /**
140         * Return a FlashMap contained in the given list that matches the request.
141         * @return a matching FlashMap or {@code null}
142         */
143        @Nullable
144        private FlashMap getMatchingFlashMap(List<FlashMap> allMaps, HttpServletRequest request) {
145                List<FlashMap> result = new LinkedList<>();
146                for (FlashMap flashMap : allMaps) {
147                        if (isFlashMapForRequest(flashMap, request)) {
148                                result.add(flashMap);
149                        }
150                }
151                if (!result.isEmpty()) {
152                        Collections.sort(result);
153                        if (logger.isTraceEnabled()) {
154                                logger.trace("Found " + result.get(0));
155                        }
156                        return result.get(0);
157                }
158                return null;
159        }
160
161        /**
162         * Whether the given FlashMap matches the current request.
163         * Uses the expected request path and query parameters saved in the FlashMap.
164         */
165        protected boolean isFlashMapForRequest(FlashMap flashMap, HttpServletRequest request) {
166                String expectedPath = flashMap.getTargetRequestPath();
167                if (expectedPath != null) {
168                        String requestUri = getUrlPathHelper().getOriginatingRequestUri(request);
169                        if (!requestUri.equals(expectedPath) && !requestUri.equals(expectedPath + "/")) {
170                                return false;
171                        }
172                }
173                MultiValueMap<String, String> actualParams = getOriginatingRequestParams(request);
174                MultiValueMap<String, String> expectedParams = flashMap.getTargetRequestParams();
175                for (Map.Entry<String, List<String>> entry : expectedParams.entrySet()) {
176                        List<String> actualValues = actualParams.get(entry.getKey());
177                        if (actualValues == null) {
178                                return false;
179                        }
180                        for (String expectedValue : entry.getValue()) {
181                                if (!actualValues.contains(expectedValue)) {
182                                        return false;
183                                }
184                        }
185                }
186                return true;
187        }
188
189        private MultiValueMap<String, String> getOriginatingRequestParams(HttpServletRequest request) {
190                String query = getUrlPathHelper().getOriginatingQueryString(request);
191                return ServletUriComponentsBuilder.fromPath("/").query(query).build().getQueryParams();
192        }
193
194        @Override
195        public final void saveOutputFlashMap(FlashMap flashMap, HttpServletRequest request, HttpServletResponse response) {
196                if (CollectionUtils.isEmpty(flashMap)) {
197                        return;
198                }
199
200                String path = decodeAndNormalizePath(flashMap.getTargetRequestPath(), request);
201                flashMap.setTargetRequestPath(path);
202
203                flashMap.startExpirationPeriod(getFlashMapTimeout());
204
205                Object mutex = getFlashMapsMutex(request);
206                if (mutex != null) {
207                        synchronized (mutex) {
208                                List<FlashMap> allFlashMaps = retrieveFlashMaps(request);
209                                allFlashMaps = (allFlashMaps != null ? allFlashMaps : new CopyOnWriteArrayList<>());
210                                allFlashMaps.add(flashMap);
211                                updateFlashMaps(allFlashMaps, request, response);
212                        }
213                }
214                else {
215                        List<FlashMap> allFlashMaps = retrieveFlashMaps(request);
216                        allFlashMaps = (allFlashMaps != null ? allFlashMaps : new LinkedList<>());
217                        allFlashMaps.add(flashMap);
218                        updateFlashMaps(allFlashMaps, request, response);
219                }
220        }
221
222        @Nullable
223        private String decodeAndNormalizePath(@Nullable String path, HttpServletRequest request) {
224                if (path != null && !path.isEmpty()) {
225                        path = getUrlPathHelper().decodeRequestString(request, path);
226                        if (path.charAt(0) != '/') {
227                                String requestUri = getUrlPathHelper().getRequestUri(request);
228                                path = requestUri.substring(0, requestUri.lastIndexOf('/') + 1) + path;
229                                path = StringUtils.cleanPath(path);
230                        }
231                }
232                return path;
233        }
234
235        /**
236         * Retrieve saved FlashMap instances from the underlying storage.
237         * @param request the current request
238         * @return a List with FlashMap instances, or {@code null} if none found
239         */
240        @Nullable
241        protected abstract List<FlashMap> retrieveFlashMaps(HttpServletRequest request);
242
243        /**
244         * Update the FlashMap instances in the underlying storage.
245         * @param flashMaps a (potentially empty) list of FlashMap instances to save
246         * @param request the current request
247         * @param response the current response
248         */
249        protected abstract void updateFlashMaps(
250                        List<FlashMap> flashMaps, HttpServletRequest request, HttpServletResponse response);
251
252        /**
253         * Obtain a mutex for modifying the FlashMap List as handled by
254         * {@link #retrieveFlashMaps} and {@link #updateFlashMaps},
255         * <p>The default implementation returns a shared static mutex.
256         * Subclasses are encouraged to return a more specific mutex, or
257         * {@code null} to indicate that no synchronization is necessary.
258         * @param request the current request
259         * @return the mutex to use (may be {@code null} if none applicable)
260         * @since 4.0.3
261         */
262        @Nullable
263        protected Object getFlashMapsMutex(HttpServletRequest request) {
264                return DEFAULT_FLASH_MAPS_MUTEX;
265        }
266
267}