@ -17,12 +17,18 @@
@@ -17,12 +17,18 @@
package org.springframework.web.context.request ;
import java.security.Principal ;
import java.util.Date ;
import java.text.ParseException ;
import java.text.SimpleDateFormat ;
import java.util.Arrays ;
import java.util.Enumeration ;
import java.util.Iterator ;
import java.util.List ;
import java.util.Locale ;
import java.util.Map ;
import java.util.TimeZone ;
import java.util.regex.Matcher ;
import java.util.regex.Pattern ;
import javax.servlet.http.HttpServletRequest ;
import javax.servlet.http.HttpServletResponse ;
import javax.servlet.http.HttpSession ;
@ -44,25 +50,17 @@ import org.springframework.web.util.WebUtils;
@@ -44,25 +50,17 @@ import org.springframework.web.util.WebUtils;
* /
public class ServletWebRequest extends ServletRequestAttributes implements NativeWebRequest {
private static final String HEADER_ETAG = "ETag" ;
private static final String HEADER_IF_MODIFIED_SINCE = "If-Modified-Since" ;
private static final String HEADER_IF_UNMODIFIED_SINCE = "If-Unmodified-Since" ;
private static final String HEADER_IF_NONE_MATCH = "If-None-Match" ;
private static final String HEADER_LAST_MODIFIED = "Last-Modified" ;
private static final String ETAG = "ETag" ;
private static final String METHOD_GET = "GET ";
private static final String IF_MODIFIED_SINCE = "If-Modified-Since" ;
private static final String METHOD_HEAD = "HEAD ";
private static final String IF_UNMODIFIED_SINCE = "If-Unmodified-Since" ;
private static final String METHOD_POST = "POST ";
private static final String IF_NONE_MATCH = "If-None-Match" ;
private static final String METHO D_PUT = "PUT " ;
private static final String LAST_MODIFIED = "Last-Modified" ;
private static final String METHOD_DELETE = "DELETE" ;
private static final List < String > SAFE_METHODS = Arrays . asList ( "GET" , "HEAD" ) ;
/ * *
* Pattern matching ETag multiple field values in headers such as "If-Match" , "If-None-Match"
@ -70,6 +68,17 @@ public class ServletWebRequest extends ServletRequestAttributes implements Nativ
@@ -70,6 +68,17 @@ public class ServletWebRequest extends ServletRequestAttributes implements Nativ
* /
private static final Pattern ETAG_HEADER_VALUE_PATTERN = Pattern . compile ( "\\*|\\s*((W\\/)?(\"[^\"]*\"))\\s*,?" ) ;
/ * *
* Date formats as specified in the HTTP RFC
* @see < a href = "https://tools.ietf.org/html/rfc7231#section-7.1.1.1" > Section 7 . 1 . 1 . 1 of RFC 7231 < / a >
* /
private static final String [ ] DATE_FORMATS = new String [ ] {
"EEE, dd MMM yyyy HH:mm:ss zzz" ,
"EEE, dd-MMM-yy HH:mm:ss zzz" ,
"EEE MMM dd HH:mm:ss yyyy"
} ;
private static TimeZone GMT = TimeZone . getTimeZone ( "GMT" ) ;
private boolean notModified = false ;
@ -189,141 +198,128 @@ public class ServletWebRequest extends ServletRequestAttributes implements Nativ
@@ -189,141 +198,128 @@ public class ServletWebRequest extends ServletRequestAttributes implements Nativ
@Override
public boolean checkNotModified ( long lastModifiedTimestamp ) {
HttpServletResponse response = getResponse ( ) ;
if ( lastModifiedTimestamp > = 0 & & ! this . notModified ) {
if ( isCompatibleWithConditionalRequests ( response ) ) {
this . notModified = isTimestampNotModified ( lastModifiedTimestamp ) ;
if ( response ! = null ) {
if ( supportsNotModifiedStatus ( ) ) {
if ( this . notModified ) {
response . setStatus ( HttpServletResponse . SC_NOT_MODIFIED ) ;
}
if ( isHeaderAbsent ( response , HEADER_LAST_MODIFIED ) ) {
response . setDateHeader ( HEADER_LAST_MODIFIED , lastModifiedTimestamp ) ;
}
}
else if ( supportsConditionalUpdate ( ) ) {
if ( this . notModified ) {
response . setStatus ( HttpServletResponse . SC_PRECONDITION_FAILED ) ;
}
}
}
}
}
return this . notModified ;
return checkNotModified ( null , lastModifiedTimestamp ) ;
}
@Override
public boolean checkNotModified ( String etag ) {
HttpServletResponse response = getResponse ( ) ;
if ( StringUtils . hasLength ( etag ) & & ! this . notModified ) {
if ( isCompatibleWithConditionalRequests ( response ) ) {
etag = addEtagPadding ( etag ) ;
if ( hasRequestHeader ( HEADER_IF_NONE_MATCH ) ) {
this . notModified = isEtagNotModified ( etag ) ;
}
if ( response ! = null ) {
if ( this . notModified & & supportsNotModifiedStatus ( ) ) {
response . setStatus ( HttpServletResponse . SC_NOT_MODIFIED ) ;
}
if ( isHeaderAbsent ( response , HEADER_ETAG ) ) {
response . setHeader ( HEADER_ETAG , etag ) ;
}
}
}
}
return this . notModified ;
return checkNotModified ( etag , - 1 ) ;
}
@Override
public boolean checkNotModified ( String etag , long lastModifiedTimestamp ) {
HttpServletResponse response = getResponse ( ) ;
if ( StringUtils . hasLength ( etag ) & & ! this . notModified ) {
if ( isCompatibleWithConditionalRequests ( response ) ) {
etag = addEtagPadding ( etag ) ;
if ( hasRequestHeader ( HEADER_IF_NONE_MATCH ) ) {
this . notModified = isEtagNotModified ( etag ) ;
}
else if ( hasRequestHeader ( HEADER_IF_MODIFIED_SINCE ) ) {
this . notModified = isTimestampNotModified ( lastModifiedTimestamp ) ;
}
if ( response ! = null ) {
if ( supportsNotModifiedStatus ( ) ) {
if ( this . notModified ) {
response . setStatus ( HttpServletResponse . SC_NOT_MODIFIED ) ;
}
if ( isHeaderAbsent ( response , HEADER_ETAG ) ) {
response . setHeader ( HEADER_ETAG , etag ) ;
}
if ( isHeaderAbsent ( response , HEADER_LAST_MODIFIED ) ) {
response . setDateHeader ( HEADER_LAST_MODIFIED , lastModifiedTimestamp ) ;
}
}
else if ( supportsConditionalUpdate ( ) ) {
if ( this . notModified ) {
response . setStatus ( HttpServletResponse . SC_PRECONDITION_FAILED ) ;
}
}
}
}
if ( this . notModified | | HttpStatus . OK . value ( ) ! = response . getStatus ( ) ) {
return this . notModified ;
}
return this . notModified ;
}
public boolean isNotModified ( ) {
return this . notModified ;
}
// Evaluate conditions in order of precedence.
// See https://tools.ietf.org/html/rfc7232#section-6
private boolean isCompatibleWithConditionalRequests ( HttpServletResponse response ) {
try {
if ( response = = null ) {
// Can't check response.getStatus() - let's assume we're good
return true ;
if ( validateIfUnmodifiedSince ( lastModifiedTimestamp ) ) {
if ( this . notModified ) {
response . setStatus ( HttpStatus . PRECONDITION_FAILED . value ( ) ) ;
}
return HttpStatus . valueOf ( response . getStatus ( ) ) . is2xxSuccessful ( ) ;
return this . notModified ;
}
catch ( IllegalArgumentException ex ) {
return true ;
boolean validated = validateIfNoneMatch ( etag ) ;
if ( ! validated ) {
validateIfModifiedSince ( lastModifiedTimestamp ) ;
}
}
private boolean isHeaderAbsent ( HttpServletResponse response , String header ) {
if ( response = = null ) {
// Can't check response.getHeader(header) - let's assume it's not set
return true ;
// Update response
boolean isHttpGetOrHead = SAFE_METHODS . contains ( getRequest ( ) . getMethod ( ) ) ;
if ( this . notModified ) {
response . setStatus ( isHttpGetOrHead ?
HttpStatus . NOT_MODIFIED . value ( ) : HttpStatus . PRECONDITION_FAILED . value ( ) ) ;
}
return ( response . getHeader ( header ) = = null ) ;
if ( isHttpGetOrHead ) {
if ( lastModifiedTimestamp > 0 & & parseDateValue ( response . getHeader ( LAST_MODIFIED ) ) = = - 1 ) {
response . setDateHeader ( LAST_MODIFIED , lastModifiedTimestamp ) ;
}
if ( StringUtils . hasLength ( etag ) & & response . getHeader ( ETAG ) = = null ) {
response . setHeader ( ETAG , padEtagIfNecessary ( etag ) ) ;
}
}
return this . notModified ;
}
private boolean hasRequestHeader ( String headerName ) {
return StringUtils . hasLength ( getHeader ( headerName ) ) ;
private boolean validateIfUnmodifiedSince ( long lastModifiedTimestamp ) {
if ( lastModifiedTimestamp < 0 ) {
return false ;
}
long ifUnmodifiedSince = parseDateHeader ( IF_UNMODIFIED_SINCE ) ;
if ( ifUnmodifiedSince = = - 1 ) {
return false ;
}
// We will perform this validation...
this . notModified = ( ifUnmodifiedSince < ( lastModifiedTimestamp / 1000 * 1000 ) ) ;
return true ;
}
private boolean supportsNotModifiedStatus ( ) {
String method = getRequest ( ) . getMethod ( ) ;
return ( METHOD_GET . equals ( method ) | | METHOD_HEAD . equals ( method ) ) ;
private boolean validateIfNoneMatch ( String etag ) {
if ( ! StringUtils . hasLength ( etag ) ) {
return false ;
}
Enumeration < String > ifNoneMatch ;
try {
ifNoneMatch = getRequest ( ) . getHeaders ( IF_NONE_MATCH ) ;
}
catch ( IllegalArgumentException ex ) {
return false ;
}
if ( ! ifNoneMatch . hasMoreElements ( ) ) {
return false ;
}
// We will perform this validation...
etag = padEtagIfNecessary ( etag ) ;
while ( ifNoneMatch . hasMoreElements ( ) ) {
String clientETags = ifNoneMatch . nextElement ( ) ;
Matcher eTagMatcher = ETAG_HEADER_VALUE_PATTERN . matcher ( clientETags ) ;
// Compare weak/strong ETags as per https://tools.ietf.org/html/rfc7232#section-2.3
while ( eTagMatcher . find ( ) ) {
if ( StringUtils . hasLength ( eTagMatcher . group ( ) )
& & etag . replaceFirst ( "^W/" , "" ) . equals ( eTagMatcher . group ( 3 ) ) ) {
this . notModified = true ;
break ;
}
}
}
return true ;
}
private boolean supportsConditionalUpdate ( ) {
String method = getRequest ( ) . getMethod ( ) ;
return ( METHOD_POST . equals ( method ) | | METHOD_PUT . equals ( method ) | | METHOD_DELETE . equals ( method ) )
& & hasRequestHeader ( HEADER_IF_UNMODIFIED_SINCE ) ;
private String padEtagIfNecessary ( String etag ) {
if ( ! StringUtils . hasLength ( etag ) ) {
return etag ;
}
if ( ( etag . startsWith ( "\"" ) | | etag . startsWith ( "W/\"" ) ) & & etag . endsWith ( "\"" ) ) {
return etag ;
}
return "\"" + etag + "\"" ;
}
private boolean isTimestampNotModified ( long lastModifiedTimestamp ) {
long ifModifiedSince = parseDateHeader ( HEADER_IF_MODIFIED_SINCE ) ;
if ( ifModifiedSince ! = - 1 ) {
return ( ifModifiedSince > = ( lastModifiedTimestamp / 1000 * 1000 ) ) ;
private boolean validateIfModifiedSince ( long lastModifiedTimestamp ) {
if ( lastModifiedTimestamp < 0 ) {
return false ;
}
long ifUnmodifiedSince = parseDateHeader ( HEADER_IF_UNMODIFIED_SINCE ) ;
if ( ifUnmodifiedSince ! = - 1 ) {
return ( ifUnmodifiedSince < ( lastModifiedTimestamp / 1000 * 1000 ) ) ;
long ifM odifiedSince = parseDateHeader ( IF_MODIFIED_SINCE ) ;
if ( ifModifiedSince = = - 1 ) {
return false ;
}
return false ;
// We will perform this validation...
this . notModified = ifModifiedSince > = ( lastModifiedTimestamp / 1000 * 1000 ) ;
return true ;
}
public boolean isNotModified ( ) {
return this . notModified ;
}
@SuppressWarnings ( "deprecation" )
private long parseDateHeader ( String headerName ) {
long dateValue = - 1 ;
try {
@ -335,36 +331,32 @@ public class ServletWebRequest extends ServletRequestAttributes implements Nativ
@@ -335,36 +331,32 @@ public class ServletWebRequest extends ServletRequestAttributes implements Nativ
int separatorIndex = headerValue . indexOf ( ';' ) ;
if ( separatorIndex ! = - 1 ) {
String datePart = headerValue . substring ( 0 , separatorIndex ) ;
try {
dateValue = Date . parse ( datePart ) ;
}
catch ( IllegalArgumentException ex2 ) {
// Giving up
}
dateValue = parseDateValue ( datePart ) ;
}
}
return dateValue ;
}
private boolean isEtagNotModified ( String etag ) {
String ifNoneMatch = getHeader ( HEADER_IF_NONE_MATCH ) ;
// compare weak/strong ETag as per https://tools.ietf.org/html/rfc7232#section-2.3
String serverETag = etag . replaceFirst ( "^W/" , "" ) ;
Matcher eTagMatcher = ETAG_HEADER_VALUE_PATTERN . matcher ( ifNoneMatch ) ;
while ( eTagMatcher . find ( ) ) {
if ( "*" . equals ( eTagMatcher . group ( ) )
| | serverETag . equals ( eTagMatcher . group ( 3 ) ) ) {
return true ;
}
private long parseDateValue ( String headerValue ) {
if ( headerValue = = null ) {
// No header value sent at all
return - 1 ;
}
return false ;
}
private String addEtagPadding ( String etag ) {
if ( ! ( etag . startsWith ( "\"" ) | | etag . startsWith ( "W/\"" ) ) | | ! etag . endsWith ( "\"" ) ) {
etag = "\"" + etag + "\"" ;
if ( headerValue . length ( ) > = 3 ) {
// Short "0" or "-1" like values are never valid HTTP date headers...
// Let's only bother with SimpleDateFormat parsing for long enough values.
for ( String dateFormat : DATE_FORMATS ) {
SimpleDateFormat simpleDateFormat = new SimpleDateFormat ( dateFormat , Locale . US ) ;
simpleDateFormat . setTimeZone ( GMT ) ;
try {
return simpleDateFormat . parse ( headerValue ) . getTime ( ) ;
}
catch ( ParseException ex ) {
// ignore
}
}
}
return etag ;
return - 1 ;
}
@Override