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