/** * 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.config; import java.io.IOException; import java.net.URL; import java.util.ArrayList; import java.util.Collections; import java.util.HashMap; import java.util.HashSet; import java.util.Hashtable; import java.util.Iterator; import java.util.LinkedList; import java.util.List; import java.util.Map; import java.util.NoSuchElementException; import java.util.Set; import java.util.regex.Matcher; import java.util.regex.Pattern; import java.util.stream.Collectors; import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; import org.geotools.data.ows.CRSEnvelope; import org.geotools.data.ows.Layer; import org.geotools.data.ows.SimpleHttpClient; import org.geotools.data.ows.StyleImpl; import org.geotools.data.ows.WMSCapabilities; import org.geotools.data.wms.WebMapServer; import org.geotools.data.wms.xml.Dimension; import org.geotools.data.wms.xml.Extent; import org.geotools.ows.ServiceException; import org.geotools.xml.PreventLocalEntityResolver; import org.geotools.xml.XMLHandlerHints; import org.geowebcache.GeoWebCacheException; import org.geowebcache.config.legends.LegendRawInfo; import org.geowebcache.config.legends.LegendsRawInfo; import org.geowebcache.config.meta.ServiceInformation; import org.geowebcache.filter.parameters.NaiveWMSDimensionFilter; import org.geowebcache.filter.parameters.ParameterFilter; import org.geowebcache.filter.parameters.StringParameterFilter; import org.geowebcache.grid.BoundingBox; import org.geowebcache.grid.GridSet; import org.geowebcache.grid.GridSetBroker; import org.geowebcache.grid.GridSetFactory; import org.geowebcache.grid.GridSubset; import org.geowebcache.grid.GridSubsetFactory; import org.geowebcache.grid.SRS; import org.geowebcache.layer.TileLayer; import org.geowebcache.layer.meta.LayerMetaInformation; import org.geowebcache.layer.meta.MetadataURL; import org.geowebcache.layer.wms.WMSHttpHelper; import org.geowebcache.layer.wms.WMSLayer; public class GetCapabilitiesConfiguration implements Configuration { private static Log log = LogFactory .getLog(org.geowebcache.config.GetCapabilitiesConfiguration.class); // regex patterns used to parse legends urls parameters private static final Pattern LEGEND_WIDTH_PATTERN = Pattern.compile(".*width=(\\d+).*", Pattern.CASE_INSENSITIVE); private static final Pattern LEGEND_HEIGHT_PATTERN = Pattern.compile(".*height=(\\d+).*", Pattern.CASE_INSENSITIVE); private static final Pattern LEGEND_FORMAT_PATTERN = Pattern.compile(".*format=([^&]+).*", Pattern.CASE_INSENSITIVE); private GridSetBroker gridSetBroker; private String url = null; private int backendTimeout = 120; private String mimeTypes = null; private String metaTiling = null; private String vendorParameters = null; private boolean allowCacheBypass = false; private final HashMap<String, TileLayer> layers; private XMLConfiguration primaryConfig; public GetCapabilitiesConfiguration(GridSetBroker gridSetBroker, String url, String mimeTypes, String metaTiling, String allowCacheBypass) { this.gridSetBroker = gridSetBroker; this.url = url; this.mimeTypes = mimeTypes; this.metaTiling = metaTiling; this.layers = new HashMap<String, TileLayer>(); if (Boolean.parseBoolean(allowCacheBypass)) { this.allowCacheBypass = true; } log.info("Constructing from url " + url); } public GetCapabilitiesConfiguration(GridSetBroker gridSetBroker, String url, String mimeTypes, String metaTiling, String vendorParameters, String allowCacheBypass) { this.gridSetBroker = gridSetBroker; this.url = url; this.mimeTypes = mimeTypes; this.metaTiling = metaTiling; this.vendorParameters = vendorParameters; this.layers = new HashMap<String, TileLayer>(); if (Boolean.parseBoolean(allowCacheBypass)) { this.allowCacheBypass = true; } log.info("Constructing from url " + url); } /** * Optionally used by Spring * * @param backendTimeout */ public void setBackendTimeout(int backendTimeout) { this.backendTimeout = backendTimeout; } /** * Identifier for this Configuration instance * * @return the URL given to the constructor */ public String getIdentifier() { return url; } /** * Gets the XML document and parses it, creates WMSLayers for the relevant * * @return the layers described at the given URL */ private synchronized List<TileLayer> getTileLayers(boolean reload) throws GeoWebCacheException { List<TileLayer> layers = null; WebMapServer wms = null; try { wms = getWMS(); } catch (ServiceException | IOException e) { throw new ConfigurationException("Could not retrieve (or parse) GetCapaibilities " + this.url + " :"+e.getMessage(), e); } String wmsUrl = getWMSUrl(wms); log.info("Using GetCapabilities " + wmsUrl + " to generate URLs for WMS requests"); String urlVersion = parseVersion(url); layers = getLayers(wms, wmsUrl, urlVersion); if (layers == null || layers.size() < 1) { log.error("Unable to find any layers based on " + url); } else { log.info("Loaded " + layers.size() + " layers from " + url); } return layers; } public ServiceInformation getServiceInformation() { return null; } /** * Finds URL to WMS service and attempts to slice away the service parameter, since we will add * that anyway. * * @param wms * @return */ private String getWMSUrl(WebMapServer wms) { // // http://sigma.openplans.org:8080/geoserver/wms?SERVICE=WMS& String wmsUrl = wms.getCapabilities().getRequest().getGetCapabilities().getGet().toString(); int queryStart = wmsUrl.lastIndexOf("?"); if (queryStart > 0) { String preQuery = wmsUrl.substring(queryStart); if (preQuery.equalsIgnoreCase("?service=wms&")) { wmsUrl = wmsUrl.substring(0, wmsUrl.lastIndexOf("?")); } } return wmsUrl; } private List<TileLayer> getLayers(WebMapServer wms, String wmsUrl, String urlVersion) throws GeoWebCacheException { List<TileLayer> layers = new LinkedList<TileLayer>(); WMSCapabilities capabilities = wms.getCapabilities(); if (capabilities == null) { throw new ConfigurationException("Unable to get capabitilies from " + wmsUrl); } WMSHttpHelper sourceHelper = new WMSHttpHelper(); List<Layer> layerList = capabilities.getLayerList(); Iterator<Layer> layerIter = layerList.iterator(); while (layerIter.hasNext()) { Layer layer = layerIter.next(); String name = layer.getName(); String stylesStr = ""; String title = layer.getTitle(); String description = layer.get_abstract(); LayerMetaInformation layerMetaInfo = null; if (title != null || description != null) { layerMetaInfo = new LayerMetaInformation(title, description, null, null); } boolean queryable = layer.isQueryable(); if (name != null) { LinkedList<ParameterFilter> paramFilters = new LinkedList<ParameterFilter>(); List<StyleImpl> styles = layer.getStyles(); StringBuffer buf = new StringBuffer(); if (styles != null) { Iterator<StyleImpl> iter = styles.iterator(); boolean hasOne = false; while (iter.hasNext()) { if (hasOne) { buf.append(","); } buf.append(iter.next().getName()); hasOne = true; } stylesStr = buf.toString(); // set styles parameters StringParameterFilter stylesParameterFilter = new StringParameterFilter(); stylesParameterFilter.setKey("STYLES"); stylesParameterFilter.setValues(styles.stream().map(StyleImpl::getName).collect(Collectors.toList())); paramFilters.add(stylesParameterFilter); } double minX = layer.getLatLonBoundingBox().getMinX(); double minY = layer.getLatLonBoundingBox().getMinY(); double maxX = layer.getLatLonBoundingBox().getMaxX(); double maxY = layer.getLatLonBoundingBox().getMaxY(); BoundingBox bounds4326 = new BoundingBox(minX, minY, maxX, maxY); log.info("Found layer: " + layer.getName() + " with LatLon bbox " + bounds4326.toString()); BoundingBox bounds3785 = new BoundingBox(longToSphericalMercatorX(minX), latToSphericalMercatorY(minY), longToSphericalMercatorX(maxX), latToSphericalMercatorY(maxY)); String[] wmsUrls = { wmsUrl }; for (Dimension dimension : layer.getDimensions().values()) { Extent dimExtent = layer.getExtent(dimension.getName()); paramFilters.add(new NaiveWMSDimensionFilter(dimension, dimExtent)); } WMSLayer wmsLayer = null; try { wmsLayer = getLayer(name, wmsUrls, bounds4326, bounds3785, stylesStr, queryable, layer.getBoundingBoxes(), paramFilters); } catch (GeoWebCacheException gwc) { log.error("Error creating " + layer.getName() + ": " + gwc.getMessage()); } if (wmsLayer != null) { // Finalize with some defaults wmsLayer.setCacheBypassAllowed(allowCacheBypass); wmsLayer.setBackendTimeout(backendTimeout); wmsLayer.setMetaInformation(layerMetaInfo); if (urlVersion != null) { wmsLayer.setVersion(urlVersion); } else { String wmsVersion = capabilities.getVersion(); if (wmsVersion != null && wmsVersion.length() > 0) { wmsLayer.setVersion(wmsVersion); } } wmsLayer.setSourceHelper(sourceHelper); List<org.geotools.data.wms.xml.MetadataURL> metadataURLs = layer.getMetadataURL(); if (metadataURLs != null && !metadataURLs.isEmpty()) { List<MetadataURL> convertedMetadataURLs = new ArrayList<MetadataURL>(); for (org.geotools.data.wms.xml.MetadataURL metadataURL : metadataURLs) { convertedMetadataURLs.add(new MetadataURL(metadataURL.getType(), metadataURL.getFormat(), metadataURL.getUrl())); } wmsLayer.setMetadataURLs(convertedMetadataURLs); } // add styles legend information wmsLayer.setLegends(extractLegendsInfo(styles)); layers.add(wmsLayer); } } } return layers; } /** * Helper method that extracts from a legend url the width, height and format parameters. */ private LegendsRawInfo extractLegendsInfo(List<StyleImpl> styles) { LegendsRawInfo legendsRawInfo = new LegendsRawInfo(); // setting some acceptable default values legendsRawInfo.setDefaultWidth(20); legendsRawInfo.setDefaultHeight(20); legendsRawInfo.setDefaultFormat("image/png"); for (StyleImpl style : styles) { // extracting legend information from each style LegendRawInfo legendRawInfo = new LegendRawInfo(); legendRawInfo.setStyle(style.getName()); List legendUrls = style.getLegendURLs(); if (legendUrls != null && !legendUrls.isEmpty()) { String legendUrl = (String) legendUrls.get(0); // let's see if we can extract width, height and format from the style legend url legendRawInfo.setWidth(extractIntegerParameter(legendUrl, LEGEND_WIDTH_PATTERN)); legendRawInfo.setHeight(extractIntegerParameter(legendUrl, LEGEND_HEIGHT_PATTERN)); legendRawInfo.setFormat(extractParameter(legendUrl, LEGEND_FORMAT_PATTERN)); // setting the complete legend url legendRawInfo.setCompleteUrl(legendUrl); } legendsRawInfo.addLegendRawInfo(legendRawInfo); } return legendsRawInfo; } /** * Helper method that simply extracts from the provided url a certain parameter. */ private String extractParameter(String url, Pattern pattern) { Matcher matcher = pattern.matcher(url); if (matcher.matches()) { return matcher.group(1); } return null; } /** * Helper method that simply extracts from the provided url a certain integer parameter. */ private Integer extractIntegerParameter(String url, Pattern pattern) { String value = extractParameter(url, pattern); if (value == null) { return null; } return Integer.valueOf(value); } private WMSLayer getLayer(String name, String[] wmsurl, BoundingBox bounds4326, BoundingBox bounds3785, String stylesStr, boolean queryable, Map<String, CRSEnvelope> additionalBounds, List<ParameterFilter> paramFilters) throws GeoWebCacheException { Hashtable<String, GridSubset> grids = new Hashtable<String, GridSubset>(2); grids.put(gridSetBroker.WORLD_EPSG4326.getName(), GridSubsetFactory.createGridSubSet(gridSetBroker.WORLD_EPSG4326, bounds4326, 0, 30)); grids.put(gridSetBroker.WORLD_EPSG3857.getName(), GridSubsetFactory.createGridSubSet(gridSetBroker.WORLD_EPSG3857, bounds3785, 0, 30)); if (additionalBounds != null && additionalBounds.size() > 0) { Iterator<CRSEnvelope> iter = additionalBounds.values().iterator(); while (iter.hasNext()) { CRSEnvelope env = iter.next(); SRS srs = null; if (env.getEPSGCode() != null) { srs = SRS.getSRS(env.getEPSGCode()); } if (srs == null) { log.error(env.toString() + " has no EPSG code"); } else if (srs.getNumber() == 4326 || srs.getNumber() == 900913 || srs.getNumber() == 3857) { log.debug("Skipping " + srs.toString() + " for " + name); } else { String gridSetName = name + ":" + srs.toString(); BoundingBox extent = new BoundingBox(env.getMinX(), env.getMinY(), env.getMaxX(), env.getMaxY()); GridSet gridSet = GridSetFactory.createGridSet(gridSetName, srs, extent, false, 25, null, GridSetFactory.DEFAULT_PIXEL_SIZE_METER, 256, 256, false); grids.put(gridSetName, GridSubsetFactory.createGridSubSet(gridSet)); } } } List<String> mimeFormats = null; if (this.mimeTypes != null) { String[] mimeFormatArray = this.mimeTypes.split(","); mimeFormats = new ArrayList<String>(mimeFormatArray.length); // This is stupid... but oh well, we're only doing it once for (int i = 0; i < mimeFormatArray.length; i++) { mimeFormats.add(mimeFormatArray[i]); } } else { mimeFormats = new ArrayList<String>(3); mimeFormats.add("image/png"); mimeFormats.add("image/png8"); mimeFormats.add("image/jpeg"); } String[] metaStrings = this.metaTiling.split("x"); int[] metaWidthHeight = { Integer.parseInt(metaStrings[0]), Integer.parseInt(metaStrings[1]) }; return new WMSLayer(name, wmsurl, stylesStr, name, mimeFormats, grids, paramFilters, metaWidthHeight, this.vendorParameters, queryable, null); } WebMapServer getWMS() throws IOException, ServiceException{ Map<String, Object> hints = new HashMap<>(); hints.put(XMLHandlerHints.ENTITY_RESOLVER, PreventLocalEntityResolver.INSTANCE); return new WebMapServer(new URL(url), new SimpleHttpClient(), hints); } private String parseVersion(String url) { String tmp = url.toLowerCase(); int start = tmp.indexOf("version="); if (start == -1) { return null; } start += "version=".length(); int stop = tmp.indexOf("&", start); if (stop > 0) { return tmp.substring(start, stop); } else { return tmp.substring(start); } } private double longToSphericalMercatorX(double x) { return (x / 180.0) * 20037508.34; } private double latToSphericalMercatorY(double y) { if (y > 85.05112) { y = 85.05112; } if (y < -85.05112) { y = -85.05112; } y = (Math.PI / 180.0) * y; double tmp = Math.PI / 4.0 + y / 2.0; return 20037508.34 * Math.log(Math.tan(tmp)) / Math.PI; } /** * @see org.geowebcache.config.Configuration#isRuntimeStatsEnabled() */ public boolean isRuntimeStatsEnabled() { return false; } /** * @see org.geowebcache.config.Configuration#initialize(org.geowebcache.grid.GridSetBroker) */ public int initialize(GridSetBroker gridSetBroker) throws GeoWebCacheException { this.gridSetBroker = gridSetBroker; List<TileLayer> tileLayers = getTileLayers(true); this.layers.clear(); for (TileLayer layer : tileLayers) { layer.initialize(gridSetBroker); if(primaryConfig!=null) { primaryConfig.setDefaultValues(layer); } else if (log.isErrorEnabled()) { log.error("GetCapabilitiesConfiguration could not initialize a layer with default "+ "values as it does not have a global configuration to delegate to."); } layers.put(layer.getName(), layer); } return tileLayers.size(); } /** * @see org.geowebcache.config.Configuration#getTileLayers() * @deprecated */ public List<TileLayer> getTileLayers() { return Collections.unmodifiableList(new ArrayList<TileLayer>(layers.values())); } /** * @see org.geowebcache.config.Configuration#getLayers() */ public Iterable<? extends TileLayer> getLayers() { return Collections.unmodifiableList(new ArrayList<TileLayer>(layers.values())); } /** * @see org.geowebcache.config.Configuration#getTileLayerNames() */ public Set<String> getTileLayerNames() { return new HashSet<String>(layers.keySet()); } /** * @see org.geowebcache.config.Configuration#containsLayer(java.lang.String) */ public boolean containsLayer(String tileLayerId) { return getTileLayerById(tileLayerId) != null; } /** * @see org.geowebcache.config.Configuration#getTileLayerById(java.lang.String) */ public TileLayer getTileLayerById(String layerId) { // this configuration does not differentiate between layer identifier and identity return getTileLayer(layerId); } /** * @see org.geowebcache.config.Configuration#getTileLayer(java.lang.String) */ public TileLayer getTileLayer(String layerName) { return layers.get(layerName); } /** * @see org.geowebcache.config.Configuration#getTileLayerCount() */ public int getTileLayerCount() { return layers.size(); } /** * @see org.geowebcache.config.Configuration#removeLayer(java.lang.String) */ public boolean removeLayer(String layerName) { return layers.remove(layerName) != null; } /** * @see org.geowebcache.config.Configuration#modifyLayer(org.geowebcache.layer.TileLayer) */ public void modifyLayer(TileLayer tl) throws NoSuchElementException { throw new UnsupportedOperationException("modifyLayer is not supported by " + getClass().getSimpleName()); } /** * @see org.geowebcache.config.Configuration#save() */ public void save() throws IOException { // silently do nothing } /** * @return {@code false} * @see org.geowebcache.config.Configuration#canSave(org.geowebcache.layer.TileLayer) */ public boolean canSave(TileLayer tl) { return false; } /** * @see org.geowebcache.config.Configuration#addLayer(org.geowebcache.layer.TileLayer) */ public void addLayer(TileLayer tl) throws IllegalArgumentException { if (tl == null) { throw new NullPointerException(); } throw new IllegalArgumentException( "This is a read only configuration object, can't add tile layer " + tl.getName()); } /** * Get the global configuration delegated to. */ protected XMLConfiguration getPrimaryConfig() { return primaryConfig; } /** * Set the global configuration object to delegate to. */ public void setPrimaryConfig(XMLConfiguration primaryConfig) { this.primaryConfig = primaryConfig; } }