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.io.Closeable;
30  import java.lang.ref.ReferenceQueue;
31  import java.util.Collection;
32  import java.util.HashMap;
33  import java.util.HashSet;
34  import java.util.Map;
35  import java.util.Set;
36  import java.util.concurrent.atomic.AtomicBoolean;
37  
38  import org.apache.hc.client5.http.cache.HttpCacheCASOperation;
39  import org.apache.hc.client5.http.cache.HttpCacheEntry;
40  import org.apache.hc.client5.http.cache.HttpCacheStorage;
41  import org.apache.hc.client5.http.cache.Resource;
42  import org.apache.hc.client5.http.cache.ResourceIOException;
43  import org.apache.hc.core5.annotation.Contract;
44  import org.apache.hc.core5.annotation.ThreadingBehavior;
45  import org.apache.hc.core5.util.Args;
46  
47  /**
48   * <p>
49   * {@link HttpCacheStorage} implementation capable of deallocating resources associated with
50   * the cache entries.
51   * <p>
52   * This cache keeps track of cache entries using
53   * {@link java.lang.ref.PhantomReference} and maintains a collection of all resources that
54   * are no longer in use. The cache, however, does not automatically deallocates associated
55   * resources by invoking {@link Resource#dispose()} method. The consumer MUST periodically
56   * call {@link #cleanResources()} method to trigger resource deallocation. The cache can be
57   * permanently shut down using {@link #shutdown()} method. All resources associated with
58   * the entries used by the cache will be deallocated.
59   * </p>
60   * <p>
61   * This {@link HttpCacheStorage} implementation is intended for use with {@link FileResource}
62   * and similar.
63   * </p>
64   * <p>
65   * Compatibility note. Prior to version 4.4 this storage implementation used to dispose of
66   * all resource entries upon {@link #close()}. As of version 4.4 the {@link #close()} method
67   * disposes only of those resources that have been explicitly removed from the cache with
68   * {@link #removeEntry(String)} method.
69   * </p>
70   * <p>
71   * The {@link #shutdown()} ()} method can still be used to shut down the storage and dispose of
72   * all resources currently managed by it.
73   * </p>
74   *
75   * @since 4.1
76   */
77  @Contract(threading = ThreadingBehavior.SAFE)
78  public class ManagedHttpCacheStorage implements HttpCacheStorage, Closeable {
79  
80      private final CacheMap entries;
81      private final ReferenceQueue<HttpCacheEntry> morque;
82      private final Set<ResourceReference> resources;
83      private final AtomicBoolean active;
84  
85      public ManagedHttpCacheStorage(final CacheConfig config) {
86          super();
87          this.entries = new CacheMap(config.getMaxCacheEntries());
88          this.morque = new ReferenceQueue<>();
89          this.resources = new HashSet<>();
90          this.active = new AtomicBoolean(true);
91      }
92  
93      private void ensureValidState() {
94          if (!isActive()) {
95              throw new IllegalStateException("Cache has been shut down");
96          }
97      }
98  
99      private void keepResourceReference(final HttpCacheEntry entry) {
100         final Resource resource = entry.getResource();
101         if (resource != null) {
102             // Must deallocate the resource when the entry is no longer in used
103             final ResourceReference ref = new ResourceReference(entry, this.morque);
104             this.resources.add(ref);
105         }
106     }
107 
108     @Override
109     public void putEntry(final String url, final HttpCacheEntry entry) throws ResourceIOException {
110         Args.notNull(url, "URL");
111         Args.notNull(entry, "Cache entry");
112         ensureValidState();
113         synchronized (this) {
114             this.entries.put(url, entry);
115             keepResourceReference(entry);
116         }
117     }
118 
119     @Override
120     public HttpCacheEntry getEntry(final String url) throws ResourceIOException {
121         Args.notNull(url, "URL");
122         ensureValidState();
123         synchronized (this) {
124             return this.entries.get(url);
125         }
126     }
127 
128     @Override
129     public void removeEntry(final String url) throws ResourceIOException {
130         Args.notNull(url, "URL");
131         ensureValidState();
132         synchronized (this) {
133             // Cannot deallocate the associated resources immediately as the
134             // cache entry may still be in use
135             this.entries.remove(url);
136         }
137     }
138 
139     @Override
140     public void updateEntry(
141             final String url,
142             final HttpCacheCASOperation casOperation) throws ResourceIOException {
143         Args.notNull(url, "URL");
144         Args.notNull(casOperation, "CAS operation");
145         ensureValidState();
146         synchronized (this) {
147             final HttpCacheEntry existing = this.entries.get(url);
148             final HttpCacheEntry updated = casOperation.execute(existing);
149             this.entries.put(url, updated);
150             if (existing != updated) {
151                 keepResourceReference(updated);
152             }
153         }
154     }
155 
156     @Override
157     public Map<String, HttpCacheEntry> getEntries(final Collection<String> keys) throws ResourceIOException {
158         Args.notNull(keys, "Key");
159         final Map<String, HttpCacheEntry> resultMap = new HashMap<>(keys.size());
160         for (final String key: keys) {
161             final HttpCacheEntry entry = getEntry(key);
162             if (entry != null) {
163                 resultMap.put(key, entry);
164             }
165         }
166         return resultMap;
167     }
168 
169     public void cleanResources() {
170         if (isActive()) {
171             ResourceReference ref;
172             while ((ref = (ResourceReference) this.morque.poll()) != null) {
173                 synchronized (this) {
174                     this.resources.remove(ref);
175                 }
176                 ref.getResource().dispose();
177             }
178         }
179     }
180 
181     public void shutdown() {
182         if (compareAndSet()) {
183             synchronized (this) {
184                 this.entries.clear();
185                 for (final ResourceReference ref: this.resources) {
186                     ref.getResource().dispose();
187                 }
188                 this.resources.clear();
189                 while (this.morque.poll() != null) {
190                 }
191             }
192         }
193     }
194 
195     @Override
196     public void close() {
197         if (compareAndSet()) {
198             synchronized (this) {
199                 ResourceReference ref;
200                 while ((ref = (ResourceReference) this.morque.poll()) != null) {
201                     this.resources.remove(ref);
202                     ref.getResource().dispose();
203                 }
204             }
205         }
206     }
207 
208     /**
209      * Check if the cache is still active and has not shut down.
210      *
211      * @return {@code true} if the cache is active, otherwise return {@code false}.
212      * @since 5.2
213      */
214     public boolean isActive() {
215         return active.get();
216     }
217 
218     private boolean compareAndSet(){
219         return this.active.compareAndSet(true, false);
220     }
221 }