/* (c) 2014 Open Source Geospatial Foundation - all rights reserved
* (c) 2001 - 2013 OpenPlans
* This code is licensed under the GPL 2.0 license, available at the root
* application directory.
*/
package org.geoserver.gwc.wms;
import static com.google.common.base.Preconditions.checkArgument;
import static com.google.common.base.Preconditions.checkState;
import static org.geowebcache.conveyor.Conveyor.CacheResult.MISS;
import java.io.ByteArrayOutputStream;
import java.lang.reflect.Method;
import java.nio.channels.Channels;
import java.security.MessageDigest;
import java.util.Arrays;
import java.util.Date;
import java.util.logging.Level;
import java.util.logging.Logger;
import javax.servlet.http.HttpServletResponse;
import org.aopalliance.intercept.MethodInterceptor;
import org.aopalliance.intercept.MethodInvocation;
import org.apache.commons.httpclient.util.DateParseException;
import org.apache.commons.httpclient.util.DateUtil;
import org.geoserver.catalog.LayerInfo;
import org.geoserver.catalog.MetadataMap;
import org.geoserver.catalog.ResourceInfo;
import org.geoserver.gwc.GWC;
import org.geoserver.gwc.config.GWCConfig;
import org.geoserver.gwc.layer.GeoServerTileLayer;
import org.geoserver.ows.Dispatcher;
import org.geoserver.ows.HttpErrorCodeException;
import org.geoserver.wms.GetMapRequest;
import org.geoserver.wms.WebMap;
import org.geoserver.wms.WebMapService;
import org.geoserver.wms.map.RawMap;
import org.geotools.util.logging.Logging;
import org.geowebcache.conveyor.Conveyor.CacheResult;
import org.geowebcache.conveyor.ConveyorTile;
import org.geowebcache.grid.BoundingBox;
import org.geowebcache.grid.GridSubset;
import org.geowebcache.io.ByteArrayResource;
import org.geowebcache.io.Resource;
import org.geowebcache.layer.TileLayer;
/**
* {@link WebMapService#getMap(GetMapRequest)} Spring's AOP method interceptor to serve cached tiles
* whenever the request matches a GeoWebCache tile.
*
* @author Gabriel Roldan
*
*/
public class CachingWebMapService implements MethodInterceptor {
private static final Logger LOGGER = Logging.getLogger(CachingWebMapService.class);
private GWC gwc;
public CachingWebMapService(GWC gwc) {
this.gwc = gwc;
}
/**
* Wraps {@link WebMapService#getMap(GetMapRequest)}, called by the {@link Dispatcher}
*
* @see WebMapService#getMap(GetMapRequest)
* @see org.aopalliance.intercept.MethodInterceptor#invoke(org.aopalliance.intercept.MethodInvocation)
*/
public WebMap invoke(MethodInvocation invocation) throws Throwable {
GWCConfig config = gwc.getConfig();
if (!config.isDirectWMSIntegrationEnabled()) {
return (WebMap) invocation.proceed();
}
final GetMapRequest request = getRequest(invocation);
boolean tiled = request.isTiled();
if (!tiled) {
return (WebMap) invocation.proceed();
}
final StringBuilder requestMistmatchTarget = new StringBuilder();
ConveyorTile cachedTile = gwc.dispatch(request, requestMistmatchTarget);
if (cachedTile == null) {
WebMap dynamicResult = (WebMap) invocation.proceed();
dynamicResult.setResponseHeader("geowebcache-cache-result", MISS.toString());
dynamicResult.setResponseHeader("geowebcache-miss-reason",
requestMistmatchTarget.toString());
return dynamicResult;
}
checkState(cachedTile.getTileLayer() != null);
final TileLayer layer = cachedTile.getTileLayer();
if (LOGGER.isLoggable(Level.FINEST)) {
LOGGER.finest("GetMap request intercepted, serving cached content: " + request);
}
final byte[] tileBytes;
{
final Resource mapContents = cachedTile.getBlob();
if (mapContents instanceof ByteArrayResource) {
tileBytes = ((ByteArrayResource) mapContents).getContents();
} else {
ByteArrayOutputStream out = new ByteArrayOutputStream();
mapContents.transferTo(Channels.newChannel(out));
tileBytes = out.toByteArray();
}
}
// Handle Etags
final String ifNoneMatch = request.getHttpRequestHeader("If-None-Match");
final byte[] hash = MessageDigest.getInstance("MD5").digest(tileBytes);
final String etag = toHexString(hash);
if (etag.equals(ifNoneMatch)) {
// Client already has the current version
LOGGER.finer("ETag matches, returning 304");
throw new HttpErrorCodeException(HttpServletResponse.SC_NOT_MODIFIED);
}
LOGGER.finer("No matching ETag, returning cached tile");
final String mimeType = cachedTile.getMimeType().getMimeType();
RawMap map = new RawMap(null, tileBytes, mimeType);
map.setContentDispositionHeader(null, "." + cachedTile.getMimeType().getFileExtension(), false);
Integer cacheAgeMax = getCacheAge(layer);
LOGGER.log(Level.FINE, "Using cacheAgeMax {0}", cacheAgeMax);
if (cacheAgeMax != null) {
map.setResponseHeader("Cache-Control", "max-age=" + cacheAgeMax);
} else {
map.setResponseHeader("Cache-Control", "no-cache");
}
setConditionalGetHeaders(map, cachedTile, request, etag);
setCacheMetadataHeaders(map, cachedTile, layer);
return map;
}
private void setConditionalGetHeaders(RawMap map, ConveyorTile cachedTile, GetMapRequest request, String etag) {
map.setResponseHeader("ETag", etag);
final long tileTimeStamp = cachedTile.getTSCreated();
final String ifModSinceHeader = request.getHttpRequestHeader("If-Modified-Since");
// commons-httpclient's DateUtil can encode and decode timestamps formatted as per RFC-1123,
// which is one of the three formats allowed for Last-Modified and If-Modified-Since headers
// (e.g. 'Sun, 06 Nov 1994 08:49:37 GMT'). See
// http://www.w3.org/Protocols/rfc2616/rfc2616-sec3.html#sec3.3.1
final String lastModified = org.apache.commons.httpclient.util.DateUtil
.formatDate(new Date(tileTimeStamp));
map.setResponseHeader("Last-Modified", lastModified);
final Date ifModifiedSince;
if (ifModSinceHeader != null && ifModSinceHeader.length() > 0) {
try {
ifModifiedSince = DateUtil.parseDate(ifModSinceHeader);
// the HTTP header has second precision
long ifModSinceSeconds = 1000 * (ifModifiedSince.getTime() / 1000);
long tileTimeStampSeconds = 1000 * (tileTimeStamp / 1000);
if (ifModSinceSeconds >= tileTimeStampSeconds) {
throw new HttpErrorCodeException(HttpServletResponse.SC_NOT_MODIFIED);
}
} catch (DateParseException e) {
if (LOGGER.isLoggable(Level.FINER)) {
LOGGER.finer("Can't parse client's If-Modified-Since header: '"
+ ifModSinceHeader + "'");
}
}
}
}
private void setCacheMetadataHeaders(RawMap map, ConveyorTile cachedTile, TileLayer layer) {
long[] tileIndex = cachedTile.getTileIndex();
CacheResult cacheResult = cachedTile.getCacheResult();
GridSubset gridSubset = layer.getGridSubset(cachedTile.getGridSetId());
BoundingBox tileBounds = gridSubset.boundsFromIndex(tileIndex);
String cacheResultHeader = cacheResult == null ? "UNKNOWN" : cacheResult.toString();
map.setResponseHeader("geowebcache-layer", layer.getName());
map.setResponseHeader("geowebcache-cache-result", cacheResultHeader);
map.setResponseHeader("geowebcache-tile-index", Arrays.toString(tileIndex));
map.setResponseHeader("geowebcache-tile-bounds", tileBounds.toString());
map.setResponseHeader("geowebcache-gridset", gridSubset.getName());
map.setResponseHeader("geowebcache-crs", gridSubset.getSRS().toString());
}
private Integer getCacheAge(TileLayer layer) {
Integer cacheAge = null;
if (layer instanceof GeoServerTileLayer) {
LayerInfo layerInfo = ((GeoServerTileLayer) layer).getLayerInfo();
// configuring caching does not appear possible for layergroup
if (layerInfo != null) {
MetadataMap metadata = layerInfo.getResource().getMetadata();
Boolean enabled = metadata.get(ResourceInfo.CACHING_ENABLED, Boolean.class);
if (enabled != null && enabled) {
cacheAge = layerInfo.getResource().getMetadata().get(ResourceInfo.CACHE_AGE_MAX, Integer.class);
}
}
}
return cacheAge;
}
private GetMapRequest getRequest(MethodInvocation invocation) {
final Method method = invocation.getMethod();
checkArgument(method.getDeclaringClass().equals(WebMapService.class));
checkArgument("getMap".equals(method.getName()));
final Object[] arguments = invocation.getArguments();
checkArgument(arguments.length == 1);
checkArgument(arguments[0] instanceof GetMapRequest);
final GetMapRequest request = (GetMapRequest) arguments[0];
return request;
}
private String toHexString(byte[] hash) {
StringBuilder sb = new StringBuilder();
for (int i = 0; i < hash.length; i += 4) {
int c1 = 0xFF & hash[i];
int c2 = 0xFF & hash[i + 1];
int c3 = 0xFF & hash[i + 2];
int c4 = 0xFF & hash[i + 3];
int integer = ((c1 << 24) + (c2 << 16) + (c3 << 8) + (c4 << 0));
sb.append(Integer.toHexString(integer));
}
return sb.toString();
}
}