The Java URLConnection
mechanism may be configured to use
a ResponseCache
. This article describes a
ResponseCache
implementation.
Implementation Outline
The implementation requires subclassing ResponseCache
, providing
implementations of get(URI,String,Map<String,List<String>>)
and
put(URI,URLConnection)
. Non-trivial implementations of each
method require providing concrete implementations of
CacheResponse
and CacheRequest
. The
outline of the implementation is:
import java.net.CacheRequest;
import java.net.CacheResponse;
import java.net.ResponseCache;
import java.net.URI;
import java.net.URLConnection;
public class ResponseCacheImpl extends ResponseCache {
/**
* Default {@link ResponseCacheImpl}.
*/
public static final ResponseCacheImpl DEFAULT = new ResponseCacheImpl();
private ResponseCacheImpl() {
super();
/*
* ...
*/
}
@Override
public CacheResponse get(URI uri, String method,
Map<String,List<String>> headers) {
CacheResponseImpl response = null;
/*
* ...
*/
return response;
}
@Override
public CacheRequest put(URI uri, URLConnection connection) {
CacheRequestImpl request = null;
/*
* ...
*/
return request;
}
public class CacheRequestImpl extends CacheRequest {
/*
* ...
*/
}
public class CacheResponseImpl extends CacheResponse {
/*
* ...
*/
}
}
Note: The get()
and put()
methods may return null
indicating that no
caching facility is available for that URI
.
Cache Design
The cache will be a simple file system hierarchy residing under
${user.home}/.config/java/cache/
. A cached URI
will map to a
directory which will exist and contain two files if the object is cached:
BODY
and HEADERS
.
private Path cache(URI uri) {
Path path = cache.resolve(uri.getScheme().toLowerCase());
String host = uri.getHost().toLowerCase();
int port = uri.getPort();
if (port > 0) {
host += ":" + String.valueOf(port);
}
path = path.resolve(host);
String string = uri.getPath();
if (string != null) {
for (String substring : string.split("[/]+")) {
if (isNotEmpty(substring)) {
path = path.resolve(substring);
}
}
}
return path.normalize();
}
No attempt will be made to cache “complex” URI
s. isCacheable
is
defined as:
private boolean isCacheable(URI uri) {
return (uri.isAbsolute()
&& (! uri.isOpaque())
&& uri.getUserInfo() == null
&& uri.getQuery() == null
&& uri.getFragment() == null);
}
A URI
is cached if it isCacheable()
and its body exists in the
cache:
private boolean isCached(URI uri) {
return isCacheable(uri) && Files.exists(cache(uri).resolve(BODY));
}
ResponseCache.put(URI,URLConnection) Implementation
This method allows the caller to put an object in the cache.
URLConnection.getHeaderFields()
are saved
along with the “body” using an XMLEncoder
.
@Override
public CacheRequest put(URI uri, URLConnection connection) {
CacheRequestImpl request = null;
if (isCacheable(uri)) {
if (! connection.getAllowUserInteraction()) {
request =
new CacheRequestImpl(cache(uri),
connection.getHeaderFields());
}
}
return request;
}
public class CacheRequestImpl extends CacheRequest {
private final Path path;
private final Map<String,List<String>> headers;
private CacheRequestImpl(Path path, Map<String,List<String>> headers) {
super();
this.path = Objects.requireNonNull(path);
this.headers = Objects.requireNonNull(headers);
}
@Override
public OutputStream getBody() throws IOException {
Files.createDirectories(path);
XMLEncoder encoder =
new XMLEncoder(Files.newOutputStream(path.resolve(HEADERS)));
encoder.writeObject(headers);
encoder.close();
return Files.newOutputStream(path.resolve(BODY));
}
ResponseCache.get(URI,String,Map<String,List>) Implementation
This method attempts to retrieve the object from cache. If it is cached,
CacheResponseImpl
provides the previously saved headers and
InputStream
from the cached file. Headers are deserialized
with an XMLDecoder
.
@Override
public CacheResponse get(URI uri, String method,
Map<String,List<String>> headers) {
CacheResponseImpl response = null;
if (isCached(uri)) {
response = new CacheResponseImpl(cache(uri));
}
return response;
}
public class CacheResponseImpl extends CacheResponse {
private final Path path;
private CacheResponseImpl(Path path) {
super();
this.path = Objects.requireNonNull(path);
}
@Override
public Map<String,List<String>> getHeaders() throws IOException {
XMLDecoder decoder =
new XMLDecoder(Files.newInputStream(path.resolve(HEADERS)));
@SuppressWarnings("unchecked")
Map<String,List<String>> headers =
(Map<String,List<String>>) decoder.readObject();
decoder.close();
return headers;
}
@Override
public InputStream getBody() throws IOException {
return Files.newInputStream(path.resolve(BODY));
}
}
Installation
The ResponseCache
implementation must be configured with
the ResponseCache.setDefault(ResponseCache)
static method. The following code fragment checks to see no other
ResponseCache
is installed before installing the target.
if (ResponseCache.getDefault() == null) {
ResponseCache.setDefault(ResponseCacheImpl.DEFAULT);
}
Ant Task
The critical portions of an Ant Task that uses
URLConnection
configured with ResponseCacheImpl
for
download is shown below.
@AntTask("download")
@NoArgsConstructor @ToString
public class DownloadTask extends Task {
static {
if (ResponseCache.getDefault() == null) {
ResponseCache.setDefault(ResponseCacheImpl.DEFAULT);
}
}
@NotNull @Getter @Setter
private URI uri = null;
@NotNull @Getter @Setter
private File toFile = null;
@Override
public void execute() throws BuildException {
try {
URLConnection connection = getUri().toURL().openConnection();
IOUtils.copy(connection.getInputStream(),
new FileOutputStream(getToFile()));
log(getUri() + " --> " + getToFile());
} catch (BuildException exception) {
throw exception;
} catch (RuntimeException exception) {
throw exception;
} catch (Exception exception) {
throw new BuildException(exception);
}
}
}
ResponseCacheImpl.java
The complete implementation:
/*
* $Id: README.md 6485 2020-07-18 03:45:11Z ball $
*
* Copyright 2019 Allen D. Ball. All rights reserved.
*/
package ball.net;
import java.beans.XMLDecoder;
import java.beans.XMLEncoder;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.net.CacheRequest;
import java.net.CacheResponse;
import java.net.ResponseCache;
import java.net.URI;
import java.net.URLConnection;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.nio.file.attribute.PosixFilePermission;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.Set;
import static java.nio.file.attribute.PosixFilePermissions.asFileAttribute;
import static java.nio.file.attribute.PosixFilePermissions.fromString;
import static org.apache.commons.lang3.StringUtils.isNotEmpty;
/**
* {@link ResponseCache} implementation.
*
* @author {@link.uri mailto:ball@iprotium.com Allen D. Ball}
* @version $Revision: 6485 $
*/
public class ResponseCacheImpl extends ResponseCache {
/**
* Default {@link ResponseCacheImpl}.
*/
public static final ResponseCacheImpl DEFAULT = new ResponseCacheImpl();
private static final String BODY = "BODY";
private static final String HEADERS = "HEADERS";
private final Path cache;
private ResponseCacheImpl() {
super();
try {
cache =
Paths.get(System.getProperty("user.home"),
".config", "java", "cache");
Files.createDirectories(cache,
asFileAttribute(fromString("rwx------")));
} catch (Exception exception) {
throw new ExceptionInInitializerError(exception);
}
}
@Override
public CacheResponse get(URI uri, String method,
Map<String,List<String>> headers) {
CacheResponseImpl response = null;
if (isCached(uri)) {
response = new CacheResponseImpl(cache(uri));
}
return response;
}
@Override
public CacheRequest put(URI uri, URLConnection connection) {
CacheRequestImpl request = null;
if (isCacheable(uri)) {
if (! connection.getAllowUserInteraction()) {
request =
new CacheRequestImpl(cache(uri),
connection.getHeaderFields());
}
}
return request;
}
private Path cache(URI uri) {
Path path = cache.resolve(uri.getScheme().toLowerCase());
String host = uri.getHost().toLowerCase();
int port = uri.getPort();
if (port > 0) {
host += ":" + String.valueOf(port);
}
path = path.resolve(host);
String string = uri.getPath();
if (string != null) {
for (String substring : string.split("[/]+")) {
if (isNotEmpty(substring)) {
path = path.resolve(substring);
}
}
}
return path.normalize();
}
private boolean isCached(URI uri) {
return isCacheable(uri) && Files.exists(cache(uri).resolve(BODY));
}
private boolean isCacheable(URI uri) {
return (uri.isAbsolute()
&& (! uri.isOpaque())
&& uri.getUserInfo() == null
&& uri.getQuery() == null
&& uri.getFragment() == null);
}
private void delete(Path path) throws IOException {
Files.deleteIfExists(path.resolve(HEADERS));
Files.deleteIfExists(path.resolve(BODY));
Files.deleteIfExists(path);
}
public class CacheRequestImpl extends CacheRequest {
private final Path path;
private final Map<String,List<String>> headers;
private CacheRequestImpl(Path path, Map<String,List<String>> headers) {
super();
this.path = Objects.requireNonNull(path);
this.headers = Objects.requireNonNull(headers);
}
@Override
public OutputStream getBody() throws IOException {
Files.createDirectories(path);
XMLEncoder encoder =
new XMLEncoder(Files.newOutputStream(path.resolve(HEADERS)));
encoder.writeObject(headers);
encoder.close();
return Files.newOutputStream(path.resolve(BODY));
}
@Override
public void abort() {
try {
delete(path);
} catch (Exception exception) {
throw new IllegalStateException(exception);
}
}
}
public class CacheResponseImpl extends CacheResponse {
private final Path path;
private CacheResponseImpl(Path path) {
super();
this.path = Objects.requireNonNull(path);
}
@Override
public Map<String,List<String>> getHeaders() throws IOException {
XMLDecoder decoder =
new XMLDecoder(Files.newInputStream(path.resolve(HEADERS)));
@SuppressWarnings("unchecked")
Map<String,List<String>> headers =
(Map<String,List<String>>) decoder.readObject();
decoder.close();
return headers;
}
@Override
public InputStream getBody() throws IOException {
return Files.newInputStream(path.resolve(BODY));
}
}
}