/* (c) 2014 - 2016 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.layer; import static com.google.common.base.Preconditions.checkNotNull; import static com.google.common.base.Preconditions.checkState; import static com.google.common.base.Throwables.propagate; import static org.geoserver.gwc.GWC.tileLayerName; import static org.geoserver.ows.util.ResponseUtils.buildURL; import static org.geoserver.ows.util.ResponseUtils.params; import java.awt.*; import java.io.IOException; import java.net.MalformedURLException; import java.net.URL; import java.util.ArrayList; import java.util.Arrays; import java.util.Collections; import java.util.HashMap; import java.util.HashSet; import java.util.Iterator; import java.util.List; import java.util.Map; import java.util.Set; import java.util.logging.Level; import java.util.logging.Logger; import javax.servlet.http.Cookie; import javax.servlet.http.HttpServletRequest; import com.google.common.collect.Iterables; import org.geoserver.catalog.Catalog; import org.geoserver.catalog.KeywordInfo; import org.geoserver.catalog.LayerGroupInfo; import org.geoserver.catalog.LayerInfo; import org.geoserver.catalog.MetadataLinkInfo; import org.geoserver.catalog.MetadataMap; import org.geoserver.catalog.PublishedInfo; import org.geoserver.catalog.ResourceInfo; import org.geoserver.catalog.StyleInfo; import org.geoserver.catalog.WorkspaceInfo; import org.geoserver.gwc.GWC; import org.geoserver.gwc.config.GWCConfig; import org.geoserver.gwc.dispatch.GwcServiceDispatcherCallback; import org.geoserver.ows.LocalWorkspace; import org.geoserver.ows.Dispatcher; import org.geoserver.ows.URLMangler; import org.geoserver.ows.util.RequestUtils; import org.geoserver.platform.GeoServerExtensions; import org.geoserver.wms.GetLegendGraphicRequest; import org.geoserver.wms.GetMapRequest; import org.geoserver.wms.WMS; import org.geoserver.wms.WebMap; import org.geoserver.wms.capabilities.LegendSample; import org.geotools.geometry.jts.ReferencedEnvelope; import org.geotools.referencing.CRS; import org.geotools.util.logging.Logging; import org.geowebcache.GeoWebCacheException; import org.geowebcache.config.ConfigurationException; import org.geowebcache.config.XMLGridSubset; import org.geowebcache.config.legends.LegendInfo; import org.geowebcache.config.legends.LegendInfoBuilder; import org.geowebcache.conveyor.ConveyorTile; import org.geowebcache.filter.parameters.ParameterException; import org.geowebcache.filter.parameters.ParameterFilter; import org.geowebcache.filter.request.RequestFilter; import org.geowebcache.grid.BoundingBox; import org.geowebcache.grid.GridSet; import org.geowebcache.grid.GridSetBroker; import org.geowebcache.grid.GridSubset; import org.geowebcache.grid.OutsideCoverageException; import org.geowebcache.grid.SRS; import org.geowebcache.io.Resource; import org.geowebcache.layer.ExpirationRule; import org.geowebcache.layer.LayerListenerList; import org.geowebcache.layer.MetaTile; import org.geowebcache.layer.ProxyLayer; import org.geowebcache.layer.TileLayer; import org.geowebcache.layer.TileLayerListener; import org.geowebcache.layer.meta.ContactInformation; import org.geowebcache.layer.meta.LayerMetaInformation; import org.geowebcache.layer.meta.MetadataURL; import org.geowebcache.layer.updatesource.UpdateSourceDefinition; import org.geowebcache.locks.LockProvider.Lock; import org.geowebcache.mime.FormatModifier; import org.geowebcache.mime.MimeException; import org.geowebcache.mime.MimeType; import org.geowebcache.util.GWCVars; import org.geowebcache.util.ServletUtils; import org.opengis.referencing.crs.CoordinateReferenceSystem; import com.google.common.base.Throwables; import com.vividsolutions.jts.geom.Envelope; import com.vividsolutions.jts.geom.Geometry; import org.vfny.geoserver.util.ResponseUtils; public class GeoServerTileLayer extends TileLayer implements ProxyLayer { private static final Logger LOGGER = Logging.getLogger(GeoServerTileLayer.class); private final GeoServerTileLayerInfo info; public static final String GWC_SEED_INTERCEPT_TOKEN = "GWC_SEED_INTERCEPT"; public static final ThreadLocal<WebMap> WEB_MAP = new ThreadLocal<WebMap>(); private String configErrorMessage; private Map<String, GridSubset> subSets; private static LayerListenerList listeners = new LayerListenerList(); private final GridSetBroker gridSetBroker; private Catalog catalog; private String publishedId; volatile private PublishedInfo publishedInfo; private LegendSample legendSample; private WMS wms; public GeoServerTileLayer(final PublishedInfo publishedInfo, final GWCConfig configDefaults, final GridSetBroker gridsets) { checkNotNull(publishedInfo, "publishedInfo"); checkNotNull(gridsets, "gridsets"); checkNotNull(configDefaults, "configDefaults"); this.gridSetBroker = gridsets; this.publishedInfo = publishedInfo; this.info = TileLayerInfoUtil.loadOrCreate(getPublishedInfo(), configDefaults); } public GeoServerTileLayer(final PublishedInfo publishedInfo, final GridSetBroker gridsets, final GeoServerTileLayerInfo state) { checkNotNull(publishedInfo, "publishedInfo"); checkNotNull(gridsets, "gridsets"); checkNotNull(state, "state"); this.gridSetBroker = gridsets; this.publishedInfo = publishedInfo; this.info = state; TileLayerInfoUtil.checkAutomaticStyles(publishedInfo, state); } public GeoServerTileLayer(final Catalog catalog, final String publishedId, final GWCConfig configDefaults, final GridSetBroker gridsets) { checkNotNull(catalog, "catalog"); checkNotNull(publishedId, "publishedId"); checkNotNull(gridsets, "gridsets"); checkNotNull(configDefaults, "configDefaults"); this.gridSetBroker = gridsets; this.catalog = catalog; this.publishedId = publishedId; this.info = TileLayerInfoUtil.loadOrCreate(getPublishedInfo(), configDefaults); } public GeoServerTileLayer(final Catalog catalog, final String publishedId, final GridSetBroker gridsets, final GeoServerTileLayerInfo state) { checkNotNull(catalog, "catalog"); checkNotNull(publishedId, "publishedId"); checkNotNull(gridsets, "gridsets"); checkNotNull(state, "state"); this.gridSetBroker = gridsets; this.catalog = catalog; this.publishedId = publishedId; this.info = state; } @Override public String getId() { return info.getId(); } @Override public String getBlobStoreId(){ return info.getBlobStoreId(); } @Override public String getName() { // getting the current gwc operation String gwcOperation = GwcServiceDispatcherCallback.GWC_OPERATION.get(); // checking if we are in the context of a get capabilities request if (gwcOperation != null && gwcOperation.equalsIgnoreCase("GetCapabilities")) { // this is a get capabilities request, we need to check if we are in the context of virtual service return getNoPrefixedNameIfVirtualService(); } return info.getName(); } private String getNoPrefixedNameIfVirtualService() { // let's see if this a virtual service request WorkspaceInfo localWorkspace = LocalWorkspace.get(); if (localWorkspace != null) { // yes this is a virtual service request so removing the workspace prefix return CatalogConfiguration.removeWorkspacePrefix(info.getName(), catalog); } // this a normal request so just returning the prefixed layer name return info.getName(); } void setConfigErrorMessage(String configErrorMessage) { this.configErrorMessage = configErrorMessage; } public String getConfigErrorMessage() { return configErrorMessage; } @Override public List<ParameterFilter> getParameterFilters() { return new ArrayList<ParameterFilter>(info.getParameterFilters()); } public void resetParameterFilters() { super.defaultParameterFilterValues = null;// reset default values } /** * Returns whether this tile layer is enabled. * <p> * The layer is enabled if the following conditions apply: * <ul> * <li>Caching for this layer is enabled by configuration * <li>Its backing {@link LayerInfo} or {@link LayerGroupInfo} is enabled and not errored (as * per {@link LayerInfo#enabled()} {@link LayerGroupInfo} * <li>The layer is not errored ({@link #getConfigErrorMessage() == null} * </ul> * The layer is enabled by configuration if: the {@code GWC.enabled} metadata property is set to * {@code true} in it's corresponding {@link LayerInfo} or {@link LayerGroupInfo} * {@link MetadataMap}, or there's no {@code GWC.enabled} property set at all but the global * {@link GWCConfig#isCacheLayersByDefault()} is {@code true}. * </p> * * @see org.geowebcache.layer.TileLayer#isEnabled() */ @Override public boolean isEnabled() { final boolean tileLayerInfoEnabled = info.isEnabled(); if (!tileLayerInfoEnabled) { return false; } if (getConfigErrorMessage() != null) { if (LOGGER.isLoggable(Level.FINEST)) { LOGGER.finest("Layer " + getName() + "is not enabled due to config error: " + getConfigErrorMessage()); } return false; } boolean geoserverLayerEnabled; LayerInfo layerInfo = getLayerInfo(); if (layerInfo != null) { geoserverLayerEnabled = layerInfo.enabled(); } else { // LayerGroupInfo has no enabled property, so assume true geoserverLayerEnabled = true; } return tileLayerInfoEnabled && geoserverLayerEnabled; } @Override public void setEnabled(final boolean enabled) { info.setEnabled(enabled); } /** * * @see org.geowebcache.layer.TileLayer#isQueryable() * @see WMS#isQueryable(LayerGroupInfo) * @see WMS#isQueryable(LayerInfo) */ @Override public boolean isQueryable() { boolean queryable = GWC.get().isQueryable(this); return queryable; } private ReferencedEnvelope getLatLonBbox() throws IllegalStateException { final CoordinateReferenceSystem wgs84LonFirst; try { final boolean longitudeFirst = true; wgs84LonFirst = CRS.decode("EPSG:4326", longitudeFirst); } catch (Exception e) { throw new RuntimeException(e); } ReferencedEnvelope latLongBbox; if (getLayerInfo() == null) { LayerGroupInfo groupInfo = getLayerGroupInfo(); try { ReferencedEnvelope bounds = groupInfo.getBounds(); boolean lenient = true; latLongBbox = bounds.transform(wgs84LonFirst, lenient); } catch (Exception e) { String msg = "Can't get lat long bounds for layer group " + tileLayerName(groupInfo); LOGGER.log(Level.WARNING, msg, e); throw new IllegalStateException(msg, e); } } else { ResourceInfo resourceInfo = getResourceInfo(); latLongBbox = resourceInfo.getLatLonBoundingBox(); if (null == latLongBbox) { latLongBbox = new ReferencedEnvelope(wgs84LonFirst); } if (null == latLongBbox.getCoordinateReferenceSystem()) { ReferencedEnvelope tmp = new ReferencedEnvelope(wgs84LonFirst); tmp.init(latLongBbox.getMinX(), latLongBbox.getMaxX(), latLongBbox.getMinY(), latLongBbox.getMaxY()); latLongBbox = tmp; } } return latLongBbox; } /** * @return the {@link LayerInfo} for this layer, or {@code null} if it's backed by a * {@link LayerGroupInfo} instead * * @deprecated use getPublishedInfo instead */ @Deprecated public LayerInfo getLayerInfo() { PublishedInfo info = getPublishedInfo(); if (info instanceof LayerInfo) { return (LayerInfo) info; } return null; } public PublishedInfo getPublishedInfo() { if (publishedInfo == null) { synchronized (this) { if(publishedInfo == null) { // see if it's a layer or a layer group PublishedInfo work = catalog.getLayer(publishedId); if (work == null) { work = catalog.getLayerGroup(publishedId); } if (work != null) { TileLayerInfoUtil.checkAutomaticStyles(work, info); } else { throw new IllegalStateException( "Could not locate a layer or layer group with id " + publishedId + " within GeoServer configuration, the GWC configuration seems to be out of synch"); } this.publishedInfo = work; } } } return publishedInfo; } /** * @return the {@link LayerGroupInfo} for this layer, or {@code null} if it's backed by a * {@link LayerInfo} instead * * @deprecated use getPublishedInfo instead */ @Deprecated public LayerGroupInfo getLayerGroupInfo() { PublishedInfo info = getPublishedInfo(); if (info instanceof LayerGroupInfo) { return (LayerGroupInfo) info; } return null; } private ResourceInfo getResourceInfo() { LayerInfo layerInfo = getLayerInfo(); return layerInfo == null ? null : layerInfo.getResource(); } /** * Overrides to return a dynamic view of the backing {@link LayerInfo} or {@link LayerGroupInfo} * metadata adapted to GWC * * @see org.geowebcache.layer.TileLayer#getMetaInformation() */ @Override public LayerMetaInformation getMetaInformation() { LayerMetaInformation meta = null; String title = getName(); String description = ""; List<String> keywords = Collections.emptyList(); List<ContactInformation> contacts = Collections.emptyList(); ResourceInfo resourceInfo = getResourceInfo(); if (resourceInfo != null) { title = resourceInfo.getTitle(); description = resourceInfo.getAbstract(); keywords = new ArrayList<>(); for (KeywordInfo kw : resourceInfo.getKeywords()) { keywords.add(kw.getValue()); } } else { LayerGroupInfo lg = getLayerGroupInfo(); if(lg != null) { if (lg != null) { if(lg.getTitle() != null) { title = lg.getTitle(); } if(lg.getAbstract() != null) { description = lg.getAbstract(); } } } } meta = new LayerMetaInformation(title, description, keywords, contacts); return meta; } /** * The default style name for the layer, as advertised by its backing * {@link LayerInfo#getDefaultStyle()}, or {@code null} if this tile layer is backed by a * {@link LayerGroupInfo}. * <p> * As the default style is always cached, its name is not stored as part of this tile layer's * {@link GeoServerTileLayerInfo}. Instead it's 'live' and retrieved from the current * {@link LayerInfo} every time this method is invoked. * </p> * * @see org.geowebcache.layer.TileLayer#getStyles() * @see GeoServerTileLayerInfo#getDefaultStyle() */ @Override public String getStyles() { LayerGroupInfo layerGroupInfo = getLayerGroupInfo(); if (layerGroupInfo != null) { // there's no such thing as default style for a layer group return null; } LayerInfo layerInfo = getLayerInfo(); StyleInfo defaultStyle = layerInfo.getDefaultStyle(); if (defaultStyle == null) { setConfigErrorMessage("Underlying GeoSever Layer has no default style"); return null; } return defaultStyle.prefixedName(); } /** * @see org.geowebcache.layer.TileLayer#getFeatureInfo * @see GWC#dispatchOwsRequest */ @Override public Resource getFeatureInfo(ConveyorTile convTile, BoundingBox bbox, int height, int width, int x, int y) throws GeoWebCacheException { Map<String, String> params = buildGetFeatureInfo(convTile, bbox, height, width, x, y); Resource response; try { response = GWC.get().dispatchOwsRequest(params, (Cookie[]) null); } catch (Exception e) { throw new GeoWebCacheException(e); } return response; } private Map<String, String> buildGetFeatureInfo(ConveyorTile convTile, BoundingBox bbox, int height, int width, int x, int y) { Map<String, String> wmsParams = new HashMap<String, String>(); wmsParams.put("SERVICE", "WMS"); wmsParams.put("VERSION", "1.1.1"); wmsParams.put("REQUEST", "GetFeatureInfo"); wmsParams.put("LAYERS", getName()); wmsParams.put("STYLES", ""); wmsParams.put("QUERY_LAYERS", getName()); MimeType mimeType = convTile.getMimeType(); if (mimeType == null) { mimeType = getMimeTypes().get(0); } wmsParams.put("FORMAT", mimeType.getFormat()); wmsParams.put("EXCEPTIONS", GetMapRequest.SE_XML); wmsParams.put("INFO_FORMAT", convTile.getMimeType().getFormat()); GridSubset gridSubset = convTile.getGridSubset(); wmsParams.put("SRS", gridSubset.getSRS().toString()); wmsParams.put("HEIGHT", String.valueOf(height)); wmsParams.put("WIDTH", String.valueOf(width)); wmsParams.put("BBOX", bbox.toString()); wmsParams.put("X", String.valueOf(x)); wmsParams.put("Y", String.valueOf(y)); String featureCount; { Map<String, String> values = ServletUtils.selectedStringsFromMap( convTile.servletReq.getParameterMap(), convTile.servletReq.getCharacterEncoding(), "feature_count"); featureCount = values.get("feature_count"); } if (featureCount != null) { wmsParams.put("FEATURE_COUNT", featureCount); } Map<String, String> fullParameters = convTile.getFullParameters(); if (fullParameters.isEmpty()) { fullParameters = getDefaultParameterFilters(); } wmsParams.putAll(fullParameters); return wmsParams; } @Override public ConveyorTile getTile(ConveyorTile tile) throws GeoWebCacheException, IOException, OutsideCoverageException { MimeType mime = tile.getMimeType(); final List<MimeType> formats = getMimeTypes(); if (mime == null) { mime = formats.get(0); } else { if (!formats.contains(mime)) { throw new IllegalArgumentException(mime.getFormat() + " is not a supported format for " + getName()); } } final String tileGridSetId = tile.getGridSetId(); final GridSubset gridSubset = getGridSubset(tileGridSetId); if (gridSubset == null) { throw new IllegalArgumentException("Requested gridset not found: " + tileGridSetId); } final long[] gridLoc = tile.getTileIndex(); checkNotNull(gridLoc); // Final preflight check, throws OutsideCoverageException if necessary gridSubset.checkCoverage(gridLoc); ConveyorTile returnTile; int metaX; int metaY; if (mime.supportsTiling()) { metaX = info.getMetaTilingX(); metaY = info.getMetaTilingY(); } else { metaX = metaY = 1; } returnTile = getMetatilingReponse(tile, true, metaX, metaY); sendTileRequestedEvent(returnTile); return returnTile; } @Override public void addLayerListener(final TileLayerListener listener) { listeners.addListener(listener); } @Override public boolean removeLayerListener(final TileLayerListener listener) { listeners.removeListener(listener); return true; } protected final void sendTileRequestedEvent(ConveyorTile tile) { if (listeners != null) { listeners.sendTileRequested(this, tile); } } private ConveyorTile getMetatilingReponse(ConveyorTile tile, final boolean tryCache, final int metaX, final int metaY) throws GeoWebCacheException, IOException { final GridSubset gridSubset = getGridSubset(tile.getGridSetId()); final int zLevel = (int) tile.getTileIndex()[2]; tile.setMetaTileCacheOnly(!gridSubset.shouldCacheAtZoom(zLevel)); if (tryCache && tryCacheFetch(tile)) { return finalizeTile(tile); } final GeoServerMetaTile metaTile = createMetaTile(tile, metaX, metaY); Lock lock = null; try { /** ****************** Acquire lock ******************* */ lock = GWC.get().getLockProvider().getLock(buildLockKey(tile, metaTile)); // got the lock on the meta tile, try again if (tryCache && tryCacheFetch(tile)) { LOGGER.finest("--> " + Thread.currentThread().getName() + " returns cache hit for " + Arrays.toString(metaTile.getMetaGridPos())); } else { LOGGER.finer("--> " + Thread.currentThread().getName() + " submitting getMap request for meta grid location " + Arrays.toString(metaTile.getMetaGridPos()) + " on " + metaTile); WebMap map; try { long requestTime = System.currentTimeMillis(); map = dispatchGetMap(tile, metaTile); checkNotNull(map, "Did not obtain a WebMap from GeoServer's Dispatcher"); metaTile.setWebMap(map); saveTiles(metaTile, tile, requestTime); } catch (Exception e) { Throwables.propagateIfInstanceOf(e, GeoWebCacheException.class); throw new GeoWebCacheException("Problem communicating with GeoServer", e); } } /** ****************** Return lock and response ****** */ } finally { if(lock != null) { lock.release(); } metaTile.dispose(); } return finalizeTile(tile); } private String buildLockKey(ConveyorTile tile, GeoServerMetaTile metaTile) { StringBuilder metaKey = new StringBuilder(); final long[] tileIndex; if(metaTile != null) { tileIndex = metaTile.getMetaGridPos(); metaKey.append("gsmeta_"); } else { tileIndex = tile.getTileIndex(); metaKey.append("tile_"); } long x = tileIndex[0]; long y = tileIndex[1]; long z = tileIndex[2]; metaKey.append(tile.getLayerId()); metaKey.append("_").append(tile.getGridSetId()); metaKey.append("_").append(x).append("_").append(y).append("_").append(z); if(tile.getParametersId() != null) { metaKey.append("_").append(tile.getParametersId()); } metaKey.append(".").append(tile.getMimeType().getFileExtension()); return metaKey.toString(); } private WebMap dispatchGetMap(final ConveyorTile tile, final MetaTile metaTile) throws Exception { Map<String, String> params = buildGetMap(tile, metaTile); WebMap map; try { HttpServletRequest actualRequest = tile.servletReq; Cookie[] cookies = actualRequest == null ? null : actualRequest.getCookies(); GWC.get().dispatchOwsRequest(params, cookies); map = WEB_MAP.get(); if (!(map instanceof WebMap)) { throw new IllegalStateException("Expected: RenderedImageMap, got " + map); } } finally { WEB_MAP.remove(); } return map; } private GeoServerMetaTile createMetaTile(ConveyorTile tile, final int metaX, final int metaY) { GeoServerMetaTile metaTile; String tileGridSetId = tile.getGridSetId(); GridSubset gridSubset = getGridSubset(tileGridSetId); MimeType responseFormat = tile.getMimeType(); FormatModifier formatModifier = null; long[] tileGridPosition = tile.getTileIndex(); int gutter = responseFormat.isVector() ? 0 : info.getGutter(); metaTile = new GeoServerMetaTile(gridSubset, responseFormat, formatModifier, tileGridPosition, metaX, metaY, gutter); return metaTile; } private Map<String, String> buildGetMap(final ConveyorTile tile, final MetaTile metaTile) throws ParameterException { Map<String, String> params = new HashMap<String, String>(); final MimeType mimeType = tile.getMimeType(); final String gridSetId = tile.getGridSetId(); final GridSubset gridSubset = getGridSubset(gridSetId); int width = metaTile.getMetaTileWidth(); int height = metaTile.getMetaTileHeight(); String srs = gridSubset.getSRS().toString(); String format = mimeType.getFormat(); BoundingBox bbox = metaTile.getMetaTileBounds(); params.put("SERVICE", "WMS"); params.put("VERSION", "1.1.1"); params.put("REQUEST", "GetMap"); params.put("LAYERS", getName()); params.put("SRS", srs); params.put("FORMAT", format); params.put("WIDTH", String.valueOf(width)); params.put("HEIGHT", String.valueOf(height)); params.put("BBOX", bbox.toString()); params.put("EXCEPTIONS", GetMapRequest.SE_XML); params.put("STYLES", ""); params.put("TRANSPARENT", "true"); params.put(GWC_SEED_INTERCEPT_TOKEN, "true"); Map<String, String> filteredParams = tile.getFullParameters(); if (filteredParams.isEmpty()) { filteredParams = getDefaultParameterFilters(); } params.putAll(filteredParams); return params; } private 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) { LOGGER.info(gwce.getMessage()); tile.setErrorMsg(gwce.getMessage()); return false; } } return false; } 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); } tile.setTileLayer(this); return tile; } /** * @param tile */ private void setTileIndexHeader(ConveyorTile tile) { tile.servletResp.addHeader("geowebcache-tile-index", Arrays.toString(tile.getTileIndex())); } @Override public ConveyorTile getNoncachedTile(ConveyorTile tile) throws GeoWebCacheException { try { return getMetatilingReponse(tile, false, 1, 1); } catch (IOException e) { throw new GeoWebCacheException(e); } } @Override public ConveyorTile doNonMetatilingRequest(ConveyorTile tile) throws GeoWebCacheException { try { return getMetatilingReponse(tile, true, 1, 1); } catch (IOException e) { throw new GeoWebCacheException(e); } } @Override public void seedTile(ConveyorTile tile, boolean tryCache) throws GeoWebCacheException, IOException { // Ignore a seed call on a tile that's outside the cached grid levels range final GridSubset gridSubset = getGridSubset(tile.getGridSetId()); final int zLevel = (int) tile.getTileIndex()[2]; if (!gridSubset.shouldCacheAtZoom(zLevel)) { if (LOGGER.isLoggable(Level.FINEST)) { LOGGER.finest("Ignoring seed call on tile " + tile + " as it's outside the cacheable zoom level range"); } return; } int metaX = info.getMetaTilingX(); int metaY = info.getMetaTilingY(); if (!tile.getMimeType().supportsTiling()) { metaX = metaY = 1; } getMetatilingReponse(tile, tryCache, metaX, metaY); } /** * @see org.geowebcache.layer.TileLayer#getGridSubsets() */ @Override public synchronized Set<String> getGridSubsets() { checkGridSubsets(); return new HashSet<String>(subSets.keySet()); } @Override public GridSubset getGridSubset(final String gridSetId) { checkGridSubsets(); return subSets.get(gridSetId); } private synchronized void checkGridSubsets() { if (this.subSets == null) { ReferencedEnvelope latLongBbox = getLatLonBbox(); try { this.subSets = getGrids(latLongBbox, gridSetBroker); } catch (ConfigurationException e) { String msg = "Can't create grids for '" + getName() + "': " + e.getMessage(); LOGGER.log(Level.WARNING, msg, e); setConfigErrorMessage(msg); } } } @Override public synchronized GridSubset removeGridSubset(String gridSetId) { checkGridSubsets(); final GridSubset oldValue = this.subSets.remove(gridSetId); Set<XMLGridSubset> gridSubsets = new HashSet<XMLGridSubset>(info.getGridSubsets()); for (Iterator<XMLGridSubset> it = gridSubsets.iterator(); it.hasNext();) { if (it.next().getGridSetName().equals(gridSetId)) { it.remove(); break; } } info.setGridSubsets(gridSubsets); return oldValue; } @Override public void addGridSubset(GridSubset gridSubset) { XMLGridSubset gridSubsetInfo = new XMLGridSubset(gridSubset); Set<XMLGridSubset> gridSubsets = new HashSet<XMLGridSubset>(info.getGridSubsets()); gridSubsets.add(gridSubsetInfo); info.setGridSubsets(gridSubsets); this.subSets = null; } private Map<String, GridSubset> getGrids(final ReferencedEnvelope latLonBbox, final GridSetBroker gridSetBroker) throws ConfigurationException { Set<XMLGridSubset> cachedGridSets = info.getGridSubsets(); if (cachedGridSets.size() == 0) { return Collections.emptyMap(); } Map<String, GridSubset> grids = new HashMap<String, GridSubset>(2); for (XMLGridSubset gridSubset : cachedGridSets) { final String gridSetId = gridSubset.getGridSetName(); final GridSet gridSet = gridSetBroker.get(gridSetId); if (gridSet == null) { LOGGER.info("No GWC GridSet named '" + gridSetId + "' exists."); continue; } BoundingBox extent = gridSubset.getExtent(); if (null == extent) { try { SRS srs = gridSet.getSrs(); try { extent = getBounds(srs); } catch (RuntimeException cantComputeBounds) { final String msg = "Can't compute bounds for tile layer " + getName() + " in CRS " + srs + ". Assuming full GridSet bounds. (" + cantComputeBounds.getMessage() + ")"; if (LOGGER.isLoggable(Level.FINE)) { LOGGER.log(Level.FINE, msg, cantComputeBounds); } else { LOGGER.warning(msg); } extent = gridSet.getBounds(); } BoundingBox maxBounds = gridSet.getBounds(); BoundingBox intersection = maxBounds.intersection(extent); extent = intersection; } catch (RuntimeException e) { LOGGER.log(Level.WARNING, "Error computing layer bounds, assuming whole GridSet bounds", e); extent = gridSet.getOriginalExtent(); } } gridSubset.setExtent(extent); GridSubset gridSubSet = gridSubset.getGridSubSet(gridSetBroker); grids.put(gridSetId, gridSubSet); } return grids; } private BoundingBox getBounds(final SRS srs) { CoordinateReferenceSystem targetCrs; try { final String epsgCode = srs.toString(); final boolean longitudeFirst = true; targetCrs = CRS.decode(epsgCode, longitudeFirst); checkNotNull(targetCrs); } catch (Exception e) { throw propagate(e); } ReferencedEnvelope nativeBounds; if (getLayerInfo() != null) { // projection policy for these bounds are already taken care of by the geoserver // configuration nativeBounds = getLayerInfo().getResource().getNativeBoundingBox(); } else { nativeBounds = getLayerGroupInfo().getBounds(); } checkState(nativeBounds != null, getName(), " has no native bounds set"); Envelope transformedBounds; // try reprojecting directly try { transformedBounds = nativeBounds.transform(targetCrs, true, 10000); } catch (Exception e) { // no luck, try the expensive way final Geometry targetAov = GWC.getAreaOfValidityAsGeometry(targetCrs, gridSetBroker); if (null == targetAov) { String msg = "Can't compute tile layer bouds out of resource native bounds for CRS " + srs; LOGGER.log(Level.WARNING, msg, e); throw new IllegalArgumentException(msg, e); } LOGGER.log(Level.FINE, "Can't compute tile layer bouds out of resource " + "native bounds for CRS " + srs, e); final CoordinateReferenceSystem nativeCrs = nativeBounds.getCoordinateReferenceSystem(); try { ReferencedEnvelope targetAovBounds = new ReferencedEnvelope( targetAov.getEnvelopeInternal(), targetCrs); // transform target AOV in target CRS to native CRS ReferencedEnvelope targetAovInNativeCrs = targetAovBounds.transform(nativeCrs, true, 10000); // get the intersection between the target aov in native crs and native layer bounds Envelope intersection = targetAovInNativeCrs.intersection(nativeBounds); ReferencedEnvelope clipped = new ReferencedEnvelope(intersection, nativeCrs); // transform covered area in native crs to target crs transformedBounds = clipped.transform(targetCrs, true, 10000); } catch (Exception e1) { throw propagate(e1); } } BoundingBox targetBbox = new BoundingBox(transformedBounds.getMinX(), transformedBounds.getMinY(), transformedBounds.getMaxX(), transformedBounds.getMaxY()); return targetBbox; } public GeoServerTileLayerInfo getInfo() { return info; } /** * @see org.geowebcache.layer.TileLayer#getUpdateSources() */ @Override public List<UpdateSourceDefinition> getUpdateSources() { return Collections.emptyList(); } /** * @see org.geowebcache.layer.TileLayer#useETags() */ @Override public boolean useETags() { return false; } /** * @see org.geowebcache.layer.TileLayer#getFormatModifiers() */ @Override public List<FormatModifier> getFormatModifiers() { return Collections.emptyList(); } /** * @see org.geowebcache.layer.TileLayer#setFormatModifiers(java.util.List) */ @Override public void setFormatModifiers(List<FormatModifier> formatModifiers) { throw new UnsupportedOperationException(); } /** * @see org.geowebcache.layer.TileLayer#getMetaTilingFactors() */ @Override public int[] getMetaTilingFactors() { return new int[] { info.getMetaTilingX(), info.getMetaTilingY() }; } /** * @return {@code true} * @see #getNoncachedTile(ConveyorTile) * @see org.geowebcache.layer.TileLayer#isCacheBypassAllowed() */ @Override public Boolean isCacheBypassAllowed() { return true; } /** * @throws UnsupportedOperationException * @see org.geowebcache.layer.TileLayer#setCacheBypassAllowed(boolean) */ @Override public void setCacheBypassAllowed(boolean allowed) { throw new UnsupportedOperationException(); } /** * @return {@code 0} * @see org.geowebcache.layer.TileLayer#getBackendTimeout() */ @Override public Integer getBackendTimeout() { return Integer.valueOf(0); } /** * @throws UnsupportedOperationException * @see org.geowebcache.layer.TileLayer#setBackendTimeout(int) */ @Override public void setBackendTimeout(int seconds) { throw new UnsupportedOperationException(); } /** * @see org.geowebcache.layer.TileLayer#getMimeTypes() */ @Override public List<MimeType> getMimeTypes() { Set<String> mimeFormats = info.getMimeFormats(); List<MimeType> mimeTypes = new ArrayList<MimeType>(mimeFormats.size()); for (String format : mimeFormats) { try { mimeTypes.add(MimeType.createFromFormat(format)); } catch (MimeException e) { LOGGER.log(Level.WARNING, "Can't create MimeType from format " + format, e); } } return mimeTypes; } /** * @see org.geowebcache.layer.TileLayer#getExpireClients(int) */ @Override public int getExpireClients(int zoomLevel) { if (info.getExpireClients()>0) { return info.getExpireClients(); } LayerInfo layerInfo = getLayerInfo(); if(layerInfo != null) { return getLayerMaxAge(layerInfo); } LayerGroupInfo layerGroupInfo = getLayerGroupInfo(); if (layerGroupInfo != null) { return getGroupMaxAge(layerGroupInfo); } else { if(LOGGER.isLoggable(Level.FINE)) { LOGGER.log(Level.FINE, "Found a GeoServerTileLayer that is not base on either" + "LayerInfo or LayerGroupInfo, setting its max age to 0"); } return 0; } } /** * Returns the max age of a layer group by looking for the minimum max age of its components * * @param lg * */ private int getGroupMaxAge(LayerGroupInfo lg) { int maxAge = Integer.MAX_VALUE; for (PublishedInfo pi : lg.getLayers()) { int piAge; if(pi instanceof LayerInfo) { piAge = getLayerMaxAge((LayerInfo) pi); } else if(pi instanceof LayerGroupInfo) { piAge = getGroupMaxAge((LayerGroupInfo) pi); } else { if(LOGGER.isLoggable(Level.FINE)) { LOGGER.log(Level.FINE, "Found a PublishedInfo that is nor LayerInfo nor " + "LayerGroupInfo, setting its max age to 0: " + pi); } piAge = 0; } maxAge = Math.min(piAge, maxAge); } return maxAge; } /** * Returns the max age for the specified layer * */ private int getLayerMaxAge(LayerInfo li) { MetadataMap metadata = li.getResource().getMetadata(); Object enabled = metadata.get(ResourceInfo.CACHING_ENABLED); if (enabled != null && enabled.toString().equalsIgnoreCase("true")) { Integer maxAge = metadata.get(ResourceInfo.CACHE_AGE_MAX, Integer.class); if(maxAge != null) { return maxAge; } else { return 0; } } return 0; } /** * @see org.geowebcache.layer.TileLayer#getExpireCache(int) */ @Override public int getExpireCache(int zoomLevel) { if (info.getExpireCacheList() != null) { ExpirationRule matchedRule = null; for (ExpirationRule rule : info.getExpireCacheList()) { if (zoomLevel >= rule.getMinZoom()) { matchedRule = rule; } else { //ExpirationRules should be zoomlevel ascending break; } } if (matchedRule!=null) { return matchedRule.getExpiration(); } } return info.getExpireCache(); } /** * @return {@code null}, no request filters supported so far * @see org.geowebcache.layer.TileLayer#getRequestFilters() */ @Override public List<RequestFilter> getRequestFilters() { return null; } /** * Empty method, returns {@code true}, initialization is dynamic for this class. * * @see org.geowebcache.layer.TileLayer#initialize(org.geowebcache.grid.GridSetBroker) */ @Override public boolean initialize(final GridSetBroker gridSetBroker) { return true; } @Override public String toString() { return new StringBuilder(getClass().getSimpleName()).append("[").append(info).append("]") .toString(); } @Override public List<MimeType> getInfoMimeTypes() { // Get the formats WMS supports for GetFeatureInfo List<String> typeStrings = ((WMS) GeoServerExtensions.bean("wms")).getAvailableFeatureInfoFormats(); List<MimeType> types = new ArrayList<MimeType>(typeStrings.size()); for(String typeString: typeStrings) { try { types.add(MimeType.createFromFormat(typeString)); } catch (MimeException e) { if (LOGGER.isLoggable(Level.WARNING)){ LOGGER.log(Level.WARNING, e.getMessage(), e); } } } return types; } @Override public void proxyRequest(ConveyorTile tile) throws GeoWebCacheException { try { GWC.get().proxyOwsRequest(tile); } catch (Exception e) { throw new GeoWebCacheException("Failed to cascade request", e); } } @Override public List<MetadataURL> getMetadataURLs() { List<MetadataLinkInfo> gsMetadataLinks; List<MetadataURL> gwcMetadataLinks = new ArrayList<>(); LayerInfo layerInfo = getLayerInfo(); if (layerInfo != null) { // this is a normal layer gsMetadataLinks = layerInfo.getResource().getMetadataLinks(); } else { // this is a layer group gsMetadataLinks = new ArrayList<>(); for (LayerInfo layer : Iterables.filter(getLayerGroupInfo().getLayers(), LayerInfo.class)) { // getting metadata of all layers of the layer group List<MetadataLinkInfo> metadataLinksLayer = layer.getResource().getMetadataLinks(); if (metadataLinksLayer != null) { gsMetadataLinks.addAll(metadataLinksLayer); } } } String baseUrl = RequestUtils.baseURL(Dispatcher.REQUEST.get().getHttpRequest()); for(MetadataLinkInfo gsMetadata : gsMetadataLinks) { String url = ResponseUtils.proxifyMetadataLink(gsMetadata, baseUrl); try { gwcMetadataLinks.add(new MetadataURL(gsMetadata.getMetadataType(), gsMetadata.getType(), new URL(url))); } catch (MalformedURLException exception) { if (LOGGER.isLoggable(Level.WARNING)) { LOGGER.warning("Error adding layer metadata URL."); } } } return gwcMetadataLinks; } @Override public boolean isAdvertised() { return true; } @Override public void setAdvertised(boolean advertised) { return; } @Override public boolean isTransientLayer(){ return false; } @Override public void setTransientLayer(boolean transientLayer){ return; } @Override public void setBlobStoreId(String blobStoreId) { info.setBlobStoreId(blobStoreId); } @Override public Map<String, org.geowebcache.config.legends.LegendInfo> getLayerLegendsInfo() { LayerInfo layerInfo = getLayerInfo(); if (layerInfo == null) { return Collections.emptyMap(); } Map<String, org.geowebcache.config.legends.LegendInfo> legends = new HashMap<>(); Set<StyleInfo> styles = new HashSet<>(layerInfo.getStyles()); styles.add(layerInfo.getDefaultStyle()); for (StyleInfo styleInfo : styles) { org.geoserver.catalog.LegendInfo legendInfo = styleInfo.getLegend(); LegendInfoBuilder gwcLegendInfo = new LegendInfoBuilder(); if (legendInfo != null) { gwcLegendInfo.withStyleName(styleInfo.getName()) .withWidth(legendInfo.getWidth()) .withHeight(legendInfo.getHeight()) .withFormat(legendInfo.getFormat()) .withCompleteUrl(buildURL(RequestUtils.baseURL(Dispatcher.REQUEST.get().getHttpRequest()), legendInfo.getOnlineResource(), null, URLMangler.URLType.RESOURCE)); legends.put(styleInfo.prefixedName(), gwcLegendInfo.build()); } else { int finalWidth = GetLegendGraphicRequest.DEFAULT_WIDTH; int finalHeight = GetLegendGraphicRequest.DEFAULT_HEIGHT; String finalFormat = GetLegendGraphicRequest.DEFAULT_FORMAT; try { Dimension dimension = getLegendSample().getLegendURLSize(styleInfo); if (dimension != null) { finalWidth = (int) dimension.getWidth(); finalHeight = (int) dimension.getHeight(); } if (null == getWms().getLegendGraphicOutputFormat(finalFormat)) { if (LOGGER.isLoggable(Level.WARNING)) { LOGGER.warning("Default legend format (" + finalFormat + ")is not supported (jai not available?), can't add LegendURL element"); } continue; } } catch (Exception exception) { LOGGER.log(Level.WARNING, "Error getting LegendURL dimensions from sample", exception); } String layerName = layerInfo.prefixedName(); Map<String, String> params = params("service", "WMS", "request", "GetLegendGraphic", "format", finalFormat, "width", String.valueOf(finalWidth), "height", String.valueOf(finalHeight), "layer", layerName); if (!styleInfo.getName().equals(layerInfo.getDefaultStyle().getName())) { params.put("style", styleInfo.getName()); } gwcLegendInfo.withStyleName(styleInfo.getName()) .withWidth(finalWidth) .withHeight(finalHeight) .withFormat(finalFormat) .withCompleteUrl(buildURL(RequestUtils.baseURL( Dispatcher.REQUEST.get().getHttpRequest()), "ows", params, URLMangler.URLType.SERVICE)); legends.put(styleInfo.prefixedName(), gwcLegendInfo.build()); } } return legends; } /** * Helper that gets the LegendSample bean from Spring context when needed. */ private LegendSample getLegendSample() { if (legendSample == null) { // no need for synchronization the bean is always the same legendSample = GeoServerExtensions.bean(LegendSample.class); } return legendSample; } /** * Helper that gets the WMS bean from Spring context when needed. */ private WMS getWms() { if (wms == null) { // no need for synchronization the bean is always the same wms = GeoServerExtensions.bean(WMS.class); } return wms; } void setLegendSample(LegendSample legendSample) { this.legendSample = legendSample; } void setWms(WMS wms) { this.wms = wms; } }