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}