/**
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Lesser General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*
* @author Arne Kepp, The Open Planning Project, Copyright 2008
*/
package org.geowebcache.layer.wms;
import java.io.IOException;
import java.util.Arrays;
import java.util.HashMap;
import java.util.Hashtable;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.concurrent.locks.Condition;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.geowebcache.GeoWebCacheException;
import org.geowebcache.conveyor.ConveyorTile;
import org.geowebcache.filter.parameters.ParameterFilter;
import org.geowebcache.filter.request.RequestFilter;
import org.geowebcache.grid.BoundingBox;
import org.geowebcache.grid.GridSetBroker;
import org.geowebcache.grid.GridSubset;
import org.geowebcache.grid.OutsideCoverageException;
import org.geowebcache.grid.SRS;
import org.geowebcache.io.ByteArrayResource;
import org.geowebcache.io.Resource;
import org.geowebcache.layer.AbstractTileLayer;
import org.geowebcache.layer.ExpirationRule;
import org.geowebcache.layer.GridLocObj;
import org.geowebcache.layer.meta.LayerMetaInformation;
import org.geowebcache.mime.FormatModifier;
import org.geowebcache.mime.MimeType;
import org.geowebcache.mime.XMLMime;
import org.geowebcache.util.GWCVars;
/**
* A tile layer backed by a WMS server
*/
public class WMSLayer extends AbstractTileLayer {
public enum RequestType {
MAP, FEATUREINFO
};
private String[] wmsUrl = null;
private Integer concurrency = null;
private String wmsLayers = null;
protected Integer gutter;
private String errorMime;
private String wmsVersion;
private String httpUsername;
private String httpPassword;
private String proxyUrl;
// Not used, should be removed through XSL
@SuppressWarnings("unused")
private Boolean tiled;
private Boolean transparent;
private String bgColor;
private String palette;
private String vendorParameters;
// Not used, should be removed through XSL
@SuppressWarnings("unused")
private String cachePrefix;
protected String sphericalMercatorOverride;
// private transient int expireCacheInt = -1;
// private transient int expireClientsInt = -1;
private transient int curWmsURL;
private transient Lock layerLock;
private transient boolean layerLocked;
private transient Condition layerLockedCond;
private transient Condition[] gridLocConds;
private transient HashMap<GridLocObj, Boolean> procQueue;
private transient WMSSourceHelper sourceHelper = null;
private static transient Log log = LogFactory.getLog(org.geowebcache.layer.wms.WMSLayer.class);
/**
* Note XStream uses reflection, this is only used for testing and loading from getCapabilities
*
* @param layerName
* @param cacheFactory
* @param wmsURL
* @param wmsStyles
* @param wmsLayers
* @param mimeFormats
* @param grids
* @param metaWidthHeight
* @param vendorParams
*/
public WMSLayer(String layerName, String[] wmsURL, String wmsStyles, String wmsLayers,
List<String> mimeFormats, Hashtable<String, GridSubset> subSets,
List<ParameterFilter> parameterFilters, int[] metaWidthHeight, String vendorParams,
boolean queryable) {
this.name = layerName;
this.wmsUrl = wmsURL;
this.wmsLayers = wmsLayers;
this.wmsStyles = wmsStyles;
this.mimeFormats = mimeFormats;
this.subSets = subSets;
this.parameterFilters = parameterFilters;
this.metaWidthHeight = metaWidthHeight;
this.vendorParameters = vendorParams;
this.transparent = true;
// this.bgColor = "0x000000";
// this.palette = "test.png";
this.queryable = queryable;
}
/**
* @see org.geowebcache.layer.TileLayer#initializeInternal(org.geowebcache.grid.GridSetBroker)
*/
@Override
protected boolean initializeInternal(GridSetBroker gridSetBroker) {
if (null == this.enabled) {
this.enabled = Boolean.TRUE;
}
if (null == this.sourceHelper) {
log.warn(this.name
+ " is configured without a source, which is a bug unless you're running tests that don't care.");
}
curWmsURL = 0;
if (backendTimeout == null) {
backendTimeout = 120;
}
layerLock = new ReentrantLock();
layerLockedCond = layerLock.newCondition();
procQueue = new HashMap<GridLocObj, Boolean>();
if (this.metaWidthHeight == null || this.metaWidthHeight.length != 2) {
this.metaWidthHeight = new int[2];
this.metaWidthHeight[0] = 3;
this.metaWidthHeight[1] = 3;
}
// Create conditions for tile locking
if (concurrency == null) {
concurrency = 32;
}
// TODO There should be a WMSServer object and it should be on that
this.gridLocConds = new Condition[concurrency];
for (int i = 0; i < gridLocConds.length; i++) {
gridLocConds[i] = layerLock.newCondition();
}
if (this.sourceHelper instanceof WMSHttpHelper) {
for (int i = 0; i < wmsUrl.length; i++) {
String url = wmsUrl[i];
if (!url.contains("?")) {
wmsUrl[i] = url + "?";
}
}
}
if (gutter == null) {
gutter = Integer.valueOf(0);
}
if (this.requestFilters != null) {
Iterator<RequestFilter> iter = requestFilters.iterator();
while (iter.hasNext()) {
try {
iter.next().initialize(this);
} catch (GeoWebCacheException e) {
log.error(e.getMessage());
}
}
}
return true;
}
@Override
public Resource getFeatureInfo(ConveyorTile convTile, BoundingBox bbox, int height, int width,
int x, int y) throws GeoWebCacheException {
return sourceHelper.makeFeatureInfoRequest(convTile, bbox, height, width, x, y);
}
/**
* The main function
*
* 1) Create cache key, test whether we can retrieve without locking 2) Get lock for metatile,
* monitor condition variable if not (Recheck cache after signal) 3) Create metatile request,
* execute 4) Get tiles and save them to cache 5) Unlock metatile, signal other threads 6) Set
* Cache-Control, return tile
*
* @param wmsparams
* @return
* @throws OutsideCoverageException
*/
public ConveyorTile getTile(ConveyorTile tile) throws GeoWebCacheException, IOException,
OutsideCoverageException {
MimeType mime = tile.getMimeType();
if (mime == null) {
mime = this.formats.get(0);
}
if (!formats.contains(mime)) {
throw new GeoWebCacheException(mime.getFormat() + " is not a supported format for "
+ name);
}
String tileGridSetId = tile.getGridSetId();
long[] gridLoc = tile.getTileIndex();
// Final preflight check, throws exception if necessary
getGridSubset(tileGridSetId).checkCoverage(gridLoc);
ConveyorTile returnTile;
try {
if (tryCacheFetch(tile)) {
returnTile = finalizeTile(tile);
} else if (mime.supportsTiling()) { // Okay, so we need to go to the backend
returnTile = getMetatilingReponse(tile, true);
} else {
returnTile = getNonMetatilingReponse(tile, true);
}
} finally {
cleanUpThreadLocals();
}
sendTileRequestedEvent(returnTile);
return returnTile;
}
/**
* Used for seeding
*/
public void seedTile(ConveyorTile tile, boolean tryCache) throws GeoWebCacheException,
IOException {
if (tile.getMimeType().supportsTiling()
&& (metaWidthHeight[0] > 1 || metaWidthHeight[1] > 1)) {
getMetatilingReponse(tile, tryCache);
} else {
getNonMetatilingReponse(tile, tryCache);
}
}
/**
* Metatiling request forwarding
*
* @param tile
* the Tile with all the information
* @param tryCache
* whether to try the cache, or seed
* @throws GeoWebCacheException
*/
private ConveyorTile getMetatilingReponse(ConveyorTile tile, boolean tryCache)
throws GeoWebCacheException {
// int idx = this.getSRSIndex(tile.getSRS());
long[] gridLoc = tile.getTileIndex();
GridSubset gridSubset = subSets.get(tile.getGridSetId());
// GridCalculator gridCalc = getGrid(tile.getSRS()).getGridCalculator();
MimeType mimeType = tile.getMimeType();
Map<String, String> fullParameters = tile.getFullParameters();
if (fullParameters.isEmpty()) {
fullParameters = getDefaultParameterFilters();
}
WMSMetaTile metaTile = new WMSMetaTile(this, gridSubset, mimeType,
this.getFormatModifier(tile.getMimeType()), gridLoc, metaWidthHeight[0],
metaWidthHeight[1], fullParameters);
// Leave a hint to save expiration, if necessary
if (saveExpirationHeaders) {
metaTile.setExpiresHeader(GWCVars.CACHE_USE_WMS_BACKEND_VALUE);
}
long[] metaGridLoc = metaTile.getMetaGridPos();
GridLocObj metaGlo = new GridLocObj(metaGridLoc, this.gridLocConds.length);
/** ****************** Acquire lock ******************* */
waitForQueue(metaGlo);
/** ****************** Check cache again ************** */
if (tryCache && tryCacheFetch(tile)) {
// Someone got it already, return lock and we're done
removeFromQueue(metaGlo);
return finalizeTile(tile);
}
/*
* This thread's byte buffer
*/
ByteArrayResource buffer = getImageBuffer(WMS_BUFFER);
try {
/** ****************** No luck, Request metatile ****** */
// Leave a hint to save expiration, if necessary
if (saveExpirationHeaders) {
metaTile.setExpiresHeader(GWCVars.CACHE_USE_WMS_BACKEND_VALUE);
}
sourceHelper.makeRequest(metaTile, buffer);
if (metaTile.getError()) {
throw new GeoWebCacheException("Empty metatile, error message: "
+ metaTile.getErrorMessage());
}
if (saveExpirationHeaders) {
// Converting to seconds
saveExpirationInformation((int) (tile.getExpiresHeader() / 1000));
}
metaTile.setImageBytes(buffer);
saveTiles(metaTile, tile);
/** ****************** Return lock and response ****** */
} finally {
removeFromQueue(metaGlo);
}
return finalizeTile(tile);
}
/**
* Non-metatiling forward to backend
*
* @param tile
* the Tile with all the information
* @param tryCache
* whether to try the cache, or seed
* @throws GeoWebCacheException
*/
private ConveyorTile getNonMetatilingReponse(ConveyorTile tile, boolean tryCache)
throws GeoWebCacheException {
// String debugHeadersStr = null;
long[] gridLoc = tile.getTileIndex();
GridLocObj glo = new GridLocObj(gridLoc, this.gridLocConds.length);
/** ****************** Acquire lock ******************* */
waitForQueue(glo);
try {
/** ****************** Check cache again ************** */
if (tryCache && tryCacheFetch(tile)) {
// Someone got it already, return lock and we're done
removeFromQueue(glo);
return tile;
// return this.createTileResponse(tile.getData(), -1, mime,
// response);
}
/** ****************** Tile ******************* */
// String requestURL = null;
// Leave a hint to save expiration, if necessary
if (saveExpirationHeaders) {
tile.setExpiresHeader(GWCVars.CACHE_USE_WMS_BACKEND_VALUE);
}
tile = doNonMetatilingRequest(tile);
if (tile.getStatus() > 299
|| this.getExpireCache((int) gridLoc[2]) != GWCVars.CACHE_DISABLE_CACHE) {
tile.persist();
}
if (saveExpirationHeaders) {
// Converting to seconds in the process
saveExpirationInformation((int) (tile.getExpiresHeader() / 1000));
}
/** ****************** Return lock and response ****** */
} finally {
removeFromQueue(glo);
}
return finalizeTile(tile);
}
public boolean tryCacheFetch(ConveyorTile tile) {
int expireCache = this.getExpireCache((int) tile.getTileIndex()[2]);
if (expireCache != GWCVars.CACHE_DISABLE_CACHE) {
try {
return tile.retrieve(expireCache * 1000L);
} catch (GeoWebCacheException gwce) {
log.error(gwce.getMessage());
tile.setErrorMsg(gwce.getMessage());
return false;
}
}
return false;
}
public void setTileIndexHeader(ConveyorTile tile) {
tile.servletResp.addHeader("geowebcache-tile-index", Arrays.toString(tile.getTileIndex()));
}
public ConveyorTile doNonMetatilingRequest(ConveyorTile tile) throws GeoWebCacheException {
tile.setTileLayer(this);
ByteArrayResource buffer = getImageBuffer(WMS_BUFFER);
sourceHelper.makeRequest(tile, buffer);
if (tile.getError() || buffer.getSize() == 0) {
throw new GeoWebCacheException("Empty tile, error message: " + tile.getErrorMessage());
}
tile.setBlob(buffer);
return tile;
}
private ConveyorTile finalizeTile(ConveyorTile tile) {
if (tile.getStatus() == 0 && !tile.getError()) {
tile.setStatus(200);
}
if (tile.servletResp != null) {
setExpirationHeader(tile.servletResp, (int) tile.getTileIndex()[2]);
setTileIndexHeader(tile);
}
return tile;
}
protected void saveExpirationInformation(int backendExpire) {
this.saveExpirationHeaders = false;
try {
if (getExpireCache(0) == GWCVars.CACHE_USE_WMS_BACKEND_VALUE) {
if (backendExpire == -1) {
this.expireCacheList.set(0, new ExpirationRule(0, 7200));
log.error("Layer profile wants MaxAge from backend,"
+ " but backend does not provide this. Setting to 7200 seconds.");
} else {
this.expireCacheList.set(backendExpire, new ExpirationRule(0, 7200));
}
log.trace("Setting expireCache to: " + expireCache);
}
if (getExpireCache(0) == GWCVars.CACHE_USE_WMS_BACKEND_VALUE) {
if (backendExpire == -1) {
this.expireClientsList.set(0, new ExpirationRule(0, 7200));
log.error("Layer profile wants MaxAge from backend,"
+ " but backend does not provide this. Setting to 7200 seconds.");
} else {
this.expireClientsList.set(0, new ExpirationRule(0, backendExpire));
log.trace("Setting expireClients to: " + expireClients);
}
}
} catch (Exception e) {
// Sometimes this doesn't work (network conditions?),
// and it's really not worth getting caught up on it.
e.printStackTrace();
}
}
public Map<String, String> getWMSRequestTemplate(MimeType responseFormat, RequestType reqType) {
Map<String, String> params = new HashMap<String, String>();
FormatModifier mod = getFormatModifier(responseFormat);
params.put("SERVICE", "WMS");
String request;
if (reqType == RequestType.MAP) {
request = "GetMap";
} else { // if(reqType == RequestType.FEATUREINFO) {
request = "GetFeatureInfo";
}
params.put("REQUEST", request);
String version = wmsVersion;
if (wmsVersion == null) {
version = "1.1.1";
}
params.put("VERSION", version);
String layers;
if (this.wmsLayers != null && this.wmsLayers.length() != 0) {
layers = wmsLayers;
} else {
layers = getName();
}
params.put("LAYERS", layers);
if (reqType == RequestType.FEATUREINFO) {
params.put("QUERY_LAYERS", layers);
}
String exceptions;
if (errorMime != null) {
exceptions = errorMime;
} else {
exceptions = XMLMime.ogcxml.getMimeType();
}
params.put("EXCEPTIONS", exceptions);
String styles = "";
if (wmsStyles != null && wmsStyles.length() != 0) {
styles = wmsStyles;
}
params.put("STYLES", styles);
if (reqType == RequestType.MAP) {
Boolean tmpTransparent = transparent;
if (mod != null && mod.getTransparent() != null) {
tmpTransparent = mod.getTransparent();
}
if (tmpTransparent == null || tmpTransparent) {
params.put("TRANSPARENT", "TRUE");
} else {
params.put("TRANSPARENT", "FALSE");
}
String tmpBgColor = bgColor;
if (mod != null && mod.getBgColor() != null) {
tmpBgColor = mod.getBgColor();
}
if (tmpBgColor != null && tmpBgColor.length() != 0) {
params.put("BGCOLOR", tmpBgColor);
}
String tmpPalette = palette;
if (mod != null && mod.getPalette() != null) {
tmpPalette = mod.getPalette();
}
if (tmpPalette != null && tmpPalette.length() != 0) {
params.put("PALETTE", tmpPalette);
}
}
if (vendorParameters != null && vendorParameters.length() != 0) {
String[] vparams = vendorParameters.split("&");
for (String vp : vparams) {
if (vp.length() > 0) {
String[] split = vp.split("=");
String key = split[0];
String val = split[1];
if (key.length() > 0) {
params.put(key, val);
}
}
}
}
return params;
}
/**
* Get the WMS backend URL that should be used next according to the round robin.
*
* @return the next URL
*/
protected String nextWmsURL() {
curWmsURL = (curWmsURL + 1) % wmsUrl.length;
return wmsUrl[curWmsURL];
}
public long[][] getZoomedInGridLoc(String gridSetId, long[] gridLoc)
throws GeoWebCacheException {
return null;
}
/**
* Acquires lock for the entire layer, returns only after all other requests that could write to
* the queue have finished
*/
public void acquireLayerLock() {
if (layerLock == null) {
layerLocked = true;
return;
}
boolean wait = true;
// Wait until the queue is free
while (wait) {
try {
layerLock.lock();
this.layerLocked = true;
if (this.procQueue == null || this.procQueue.size() == 0) {
wait = false;
}
} finally {
layerLock.unlock();
}
}
}
/**
* Releases lock for the entire layer, signals threads that have been kept waiting
*/
public void releaseLayerLock() {
if (layerLock == null) {
layerLocked = false;
return;
}
layerLock.lock();
try {
layerLocked = false;
// Wake everyone up
layerLockedCond.signalAll();
} finally {
layerLock.unlock();
}
}
/**
*
* @param metaGridLoc
* @return
*/
protected boolean waitForQueue(GridLocObj glo) {
boolean retry = true;
boolean hasWaited = false;
// int condIdx = getLocCondIdx(gridLoc);
while (retry) {
layerLock.lock();
try {
// Check for global lock
if (layerLocked) {
this.layerLockedCond.await();
} else if (this.procQueue.containsKey(glo)) {
// System.out.println(Thread.currentThread().getId()
// + " WAITING FOR "+glo.toString()+ " convar " + condIdx);
hasWaited = true;
this.gridLocConds[glo.hashCode()].await();
// System.out.println(Thread.currentThread().getId()
// + " WAKING UP "+glo.toString()+ " convar " + condIdx);
} else {
this.procQueue.put(glo, true);
retry = false;
// System.out.println(Thread.currentThread().getId()
// + " CONTINUES "+glo.toString()+ " convar " + condIdx);
}
} catch (InterruptedException ie) {
// Do we care? Maybe if the program is about to shut down
} finally {
layerLock.unlock();
}
}
return hasWaited;
}
/**
* Synchronization function, ensures that the same metatile is not requested simultaneously by
* two threads.
*
* @param gridLoc
* the grid positions of the tile (bottom left of metatile)
* @return
*/
protected void removeFromQueue(GridLocObj glo) {
layerLock.lock();
try {
// System.out.println(Thread.currentThread().getId()
// + " DONE, SIGNALLING "+glo.toString() + " convar " + condIdx);
this.procQueue.remove(glo);
this.gridLocConds[glo.hashCode()].signalAll();
} finally {
layerLock.unlock();
}
}
public void setErrorMime(String errormime) {
this.errorMime = errormime;
}
public void addMetaWidthHeight(int w, int h) {
this.metaWidthHeight[0] = w;
this.metaWidthHeight[1] = h;
}
public void setWMSurl(String[] wmsurl) {
this.wmsUrl = wmsurl;
}
public String[] getWMSurl() {
return this.wmsUrl;
}
public String getHttpPassword() {
return httpPassword;
}
public String getHttpUsername() {
return httpUsername;
}
public String getProxyUrl() {
return proxyUrl;
}
/**
* Mandatory
*/
public void setSourceHelper(WMSSourceHelper source) {
log.debug("Setting sourceHelper on " + this.name);
this.sourceHelper = source;
}
public WMSSourceHelper getSourceHelper() {
return sourceHelper;
}
public void setVersion(String version) {
this.wmsVersion = version;
}
public void setTiled(boolean tiled) {
this.tiled = tiled;
}
public boolean getTransparent() {
if (transparent == null || transparent) {
return true;
} else {
return false;
}
}
public void setTransparent(boolean transparent) {
this.transparent = transparent;
}
public int[] getBackgroundColor() {
if (bgColor == null || transparent != null && transparent) {
return null;
}
int[] ret = new int[3];
// 0xRRGGBB
ret[0] = Integer.parseInt(bgColor.substring(2, 4), 16);
ret[1] = Integer.parseInt(bgColor.substring(4, 6), 16);
ret[2] = Integer.parseInt(bgColor.substring(6, 8), 16);
return ret;
}
public ConveyorTile getNoncachedTile(ConveyorTile tile) throws GeoWebCacheException {
// Should we do mime type checks?
// note: not using getImageBuffer() here cause this method is not called during seeding, so
// there's no gain
Resource buffer = new ByteArrayResource(2048);
sourceHelper.makeRequest(tile, buffer);
tile.setBlob(buffer);
return tile;
}
public String backendSRSOverride(SRS srs) {
if (sphericalMercatorOverride != null && srs.equals(SRS.getEPSG3857())) {
return sphericalMercatorOverride;
} else {
return srs.toString();
}
}
private Object readResolve() {
// Not really needed at this point
return this;
}
public void cleanUpThreadLocals() {
WMS_BUFFER.remove();
WMS_BUFFER2.remove();
}
public void setMetaInformation(LayerMetaInformation layerMetaInfo) {
this.metaInformation = layerMetaInfo;
}
}