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