View Javadoc
1   /*
2    * ====================================================================
3    * Licensed to the Apache Software Foundation (ASF) under one
4    * or more contributor license agreements.  See the NOTICE file
5    * distributed with this work for additional information
6    * regarding copyright ownership.  The ASF licenses this file
7    * to you under the Apache License, Version 2.0 (the
8    * "License"); you may not use this file except in compliance
9    * with the License.  You may obtain a copy of the License at
10   *
11   *   http://www.apache.org/licenses/LICENSE-2.0
12   *
13   * Unless required by applicable law or agreed to in writing,
14   * software distributed under the License is distributed on an
15   * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
16   * KIND, either express or implied.  See the License for the
17   * specific language governing permissions and limitations
18   * under the License.
19   * ====================================================================
20   *
21   * This software consists of voluntary contributions made by many
22   * individuals on behalf of the Apache Software Foundation.  For more
23   * information on the Apache Software Foundation, please see
24   * <http://www.apache.org/>.
25   *
26   */
27  package org.apache.hc.client5.http.impl.cache;
28  
29  import java.util.HashSet;
30  import java.util.Iterator;
31  import java.util.Set;
32  import java.util.function.BiConsumer;
33  
34  import org.apache.hc.client5.http.cache.HeaderConstants;
35  import org.apache.hc.client5.http.cache.HttpCacheEntry;
36  import org.apache.hc.client5.http.cache.RequestCacheControl;
37  import org.apache.hc.client5.http.cache.ResponseCacheControl;
38  import org.apache.hc.core5.annotation.Contract;
39  import org.apache.hc.core5.annotation.Internal;
40  import org.apache.hc.core5.annotation.ThreadingBehavior;
41  import org.apache.hc.core5.http.FormattedHeader;
42  import org.apache.hc.core5.http.Header;
43  import org.apache.hc.core5.http.HttpHeaders;
44  import org.apache.hc.core5.http.HttpRequest;
45  import org.apache.hc.core5.http.HttpResponse;
46  import org.apache.hc.core5.http.message.ParserCursor;
47  import org.apache.hc.core5.util.Args;
48  import org.apache.hc.core5.util.CharArrayBuffer;
49  import org.apache.hc.core5.util.TextUtils;
50  import org.apache.hc.core5.util.Tokenizer;
51  import org.slf4j.Logger;
52  import org.slf4j.LoggerFactory;
53  
54  /**
55   * A parser for the HTTP Cache-Control header that can be used to extract information about caching directives.
56   * <p>
57   * This class is thread-safe and has a singleton instance ({@link #INSTANCE}).
58   * </p>
59   * <p>
60   * The {@link #parseResponse(Iterator)} method takes an HTTP header and returns a {@link ResponseCacheControl} object containing
61   * the relevant caching directives. The header can be either a {@link FormattedHeader} object, which contains a
62   * pre-parsed {@link CharArrayBuffer}, or a plain {@link Header} object, in which case the value will be parsed and
63   * stored in a new {@link CharArrayBuffer}.
64   * </p>
65   * <p>
66   * This parser only supports two directives: "max-age" and "s-maxage". If either of these directives are present in the
67   * header, their values will be parsed and stored in the {@link ResponseCacheControl} object. If both directives are
68   * present, the value of "s-maxage" takes precedence.
69   * </p>
70   */
71  @Internal
72  @Contract(threading = ThreadingBehavior.IMMUTABLE)
73  class CacheControlHeaderParser {
74  
75      /**
76       * The singleton instance of this parser.
77       */
78      public static final CacheControlHeaderParser INSTANCE = new CacheControlHeaderParser();
79  
80      /**
81       * The logger for this class.
82       */
83      private static final Logger LOG = LoggerFactory.getLogger(CacheControlHeaderParser.class);
84  
85  
86      private final static char EQUAL_CHAR = '=';
87  
88      /**
89       * The set of characters that can delimit a token in the header.
90       */
91      private static final Tokenizer.Delimiter TOKEN_DELIMS = Tokenizer.delimiters(EQUAL_CHAR, ',');
92  
93      /**
94       * The set of characters that can delimit a value in the header.
95       */
96      private static final Tokenizer.Delimiter VALUE_DELIMS = Tokenizer.delimiters(EQUAL_CHAR, ',');
97  
98      /**
99       * The token parser used to extract values from the header.
100      */
101     private final Tokenizer tokenParser;
102 
103     /**
104      * Constructs a new instance of this parser.
105      */
106     protected CacheControlHeaderParser() {
107         super();
108         this.tokenParser = Tokenizer.INSTANCE;
109     }
110 
111     public void parse(final Iterator<Header> headerIterator, final BiConsumer<String, String> consumer) {
112         while (headerIterator.hasNext()) {
113             final Header header = headerIterator.next();
114             final CharArrayBuffer buffer;
115             final Tokenizer.Cursor cursor;
116             if (header instanceof FormattedHeader) {
117                 buffer = ((FormattedHeader) header).getBuffer();
118                 cursor = new Tokenizer.Cursor(((FormattedHeader) header).getValuePos(), buffer.length());
119             } else {
120                 final String s = header.getValue();
121                 if (s == null) {
122                     continue;
123                 }
124                 buffer = new CharArrayBuffer(s.length());
125                 buffer.append(s);
126                 cursor = new Tokenizer.Cursor(0, buffer.length());
127             }
128 
129             // Parse the header
130             while (!cursor.atEnd()) {
131                 final String name = tokenParser.parseToken(buffer, cursor, TOKEN_DELIMS);
132                 String value = null;
133                 if (!cursor.atEnd()) {
134                     final int valueDelim = buffer.charAt(cursor.getPos());
135                     cursor.updatePos(cursor.getPos() + 1);
136                     if (valueDelim == EQUAL_CHAR) {
137                         value = tokenParser.parseValue(buffer, cursor, VALUE_DELIMS);
138                         if (!cursor.atEnd()) {
139                             cursor.updatePos(cursor.getPos() + 1);
140                         }
141                     }
142                 }
143                 consumer.accept(name, value);
144             }
145         }
146     }
147 
148     /**
149      * Parses the specified response header and returns a new {@link ResponseCacheControl} instance containing
150      * the relevant caching directives.
151      *
152      * <p>The returned {@link ResponseCacheControl} instance will contain the values for "max-age" and "s-maxage"
153      * caching directives parsed from the input header. If the input header does not contain any caching directives
154      * or if the directives are malformed, the returned {@link ResponseCacheControl} instance will have default values
155      * for "max-age" and "s-maxage" (-1).</p>
156      *
157      * @param headerIterator the header to parse, cannot be {@code null}
158      * @return a new {@link ResponseCacheControl} instance containing the relevant caching directives parsed
159      * from the response header
160      * @throws IllegalArgumentException if the input header is {@code null}
161      */
162     public final ResponseCacheControl parseResponse(final Iterator<Header> headerIterator) {
163         Args.notNull(headerIterator, "headerIterator");
164         final ResponseCacheControl.Builder builder = ResponseCacheControl.builder();
165         parse(headerIterator, (name, value) -> {
166             if (name.equalsIgnoreCase(HeaderConstants.CACHE_CONTROL_S_MAX_AGE)) {
167                 builder.setSharedMaxAge(parseSeconds(name, value));
168             } else if (name.equalsIgnoreCase(HeaderConstants.CACHE_CONTROL_MAX_AGE)) {
169                 builder.setMaxAge(parseSeconds(name, value));
170             } else if (name.equalsIgnoreCase(HeaderConstants.CACHE_CONTROL_MUST_REVALIDATE)) {
171                 builder.setMustRevalidate(true);
172             } else if (name.equalsIgnoreCase(HeaderConstants.CACHE_CONTROL_NO_CACHE)) {
173                 builder.setNoCache(true);
174                 if (value != null) {
175                     final Tokenizer.Cursor valCursor = new ParserCursor(0, value.length());
176                     final Set<String> noCacheFields = new HashSet<>();
177                     while (!valCursor.atEnd()) {
178                         final String token = tokenParser.parseToken(value, valCursor, VALUE_DELIMS);
179                         if (!TextUtils.isBlank(token)) {
180                             noCacheFields.add(token);
181                         }
182                         if (!valCursor.atEnd()) {
183                             valCursor.updatePos(valCursor.getPos() + 1);
184                         }
185                     }
186                     builder.setNoCacheFields(noCacheFields);
187                 }
188             } else if (name.equalsIgnoreCase(HeaderConstants.CACHE_CONTROL_NO_STORE)) {
189                 builder.setNoStore(true);
190             } else if (name.equalsIgnoreCase(HeaderConstants.CACHE_CONTROL_PRIVATE)) {
191                 builder.setCachePrivate(true);
192             } else if (name.equalsIgnoreCase(HeaderConstants.CACHE_CONTROL_PROXY_REVALIDATE)) {
193                 builder.setProxyRevalidate(true);
194             } else if (name.equalsIgnoreCase(HeaderConstants.CACHE_CONTROL_PUBLIC)) {
195                 builder.setCachePublic(true);
196             } else if (name.equalsIgnoreCase(HeaderConstants.CACHE_CONTROL_STALE_WHILE_REVALIDATE)) {
197                 builder.setStaleWhileRevalidate(parseSeconds(name, value));
198             } else if (name.equalsIgnoreCase(HeaderConstants.CACHE_CONTROL_STALE_IF_ERROR)) {
199                 builder.setStaleIfError(parseSeconds(name, value));
200             } else if (name.equalsIgnoreCase(HeaderConstants.CACHE_CONTROL_MUST_UNDERSTAND)) {
201                 builder.setMustUnderstand(true);
202             } else if (name.equalsIgnoreCase(HeaderConstants.CACHE_CONTROL_IMMUTABLE)) {
203                 builder.setImmutable(true);
204             }
205         });
206         return builder.build();
207     }
208 
209     public final ResponseCacheControl parse(final HttpResponse response) {
210         return parseResponse(response.headerIterator(HttpHeaders.CACHE_CONTROL));
211     }
212 
213     public final ResponseCacheControl parse(final HttpCacheEntry cacheEntry) {
214         return parseResponse(cacheEntry.headerIterator(HttpHeaders.CACHE_CONTROL));
215     }
216 
217     /**
218      * Parses the specified request header and returns a new {@link RequestCacheControl} instance containing
219      * the relevant caching directives.
220      *
221      * @param headerIterator the header to parse, cannot be {@code null}
222      * @return a new {@link RequestCacheControl} instance containing the relevant caching directives parsed
223      * from the request header
224      * @throws IllegalArgumentException if the input header is {@code null}
225      */
226     public final RequestCacheControl parseRequest(final Iterator<Header> headerIterator) {
227         Args.notNull(headerIterator, "headerIterator");
228         final RequestCacheControl.Builder builder = RequestCacheControl.builder();
229         parse(headerIterator, (name, value) -> {
230             if (name.equalsIgnoreCase(HeaderConstants.CACHE_CONTROL_MAX_AGE)) {
231                 builder.setMaxAge(parseSeconds(name, value));
232             } else if (name.equalsIgnoreCase(HeaderConstants.CACHE_CONTROL_MAX_STALE)) {
233                 builder.setMaxStale(parseSeconds(name, value));
234             } else if (name.equalsIgnoreCase(HeaderConstants.CACHE_CONTROL_MIN_FRESH)) {
235                 builder.setMinFresh(parseSeconds(name, value));
236             } else if (name.equalsIgnoreCase(HeaderConstants.CACHE_CONTROL_NO_STORE)) {
237                 builder.setNoStore(true);
238             } else if (name.equalsIgnoreCase(HeaderConstants.CACHE_CONTROL_NO_CACHE)) {
239                 builder.setNoCache(true);
240             } else if (name.equalsIgnoreCase(HeaderConstants.CACHE_CONTROL_ONLY_IF_CACHED)) {
241                 builder.setOnlyIfCached(true);
242             } else if (name.equalsIgnoreCase(HeaderConstants.CACHE_CONTROL_STALE_IF_ERROR)) {
243                 builder.setStaleIfError(parseSeconds(name, value));
244             }
245         });
246         return builder.build();
247     }
248 
249     public final RequestCacheControl parse(final HttpRequest request) {
250         return parseRequest(request.headerIterator(HttpHeaders.CACHE_CONTROL));
251     }
252 
253     private static long parseSeconds(final String name, final String value) {
254         final long delta = CacheSupport.deltaSeconds(value);
255         if (delta == -1 && LOG.isDebugEnabled()) {
256             LOG.debug("Directive {} is malformed: {}", name, value);
257         }
258         return delta;
259     }
260 
261 }
262 
263