/** * 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, OpenGeo, Copyright 2009 * @author Kevin Smith, Boundless, Copyright 2014 */ package org.geowebcache.service.wms; import java.io.IOException; import java.io.OutputStream; import java.nio.charset.Charset; import java.nio.charset.StandardCharsets; import java.util.ArrayList; import java.util.Arrays; import java.util.Collection; import java.util.Collections; import java.util.HashSet; import java.util.Iterator; import java.util.List; import java.util.Map; import java.util.TreeSet; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; import org.geowebcache.GeoWebCacheException; import org.geowebcache.config.legends.LegendInfo; import org.geowebcache.config.meta.ServiceContact; import org.geowebcache.config.meta.ServiceInformation; import org.geowebcache.config.meta.ServiceProvider; import org.geowebcache.filter.parameters.ParameterFilter; import org.geowebcache.filter.parameters.WMSDimensionProvider; import org.geowebcache.grid.BoundingBox; import org.geowebcache.grid.GridSubset; import org.geowebcache.grid.SRS; import org.geowebcache.io.XMLBuilder; import org.geowebcache.layer.TileLayer; import org.geowebcache.layer.TileLayerDispatcher; import org.geowebcache.layer.meta.LayerMetaInformation; import org.geowebcache.layer.meta.MetadataURL; import org.geowebcache.mime.ImageMime; import org.geowebcache.mime.MimeType; import org.geowebcache.util.ServletUtils; import org.geowebcache.util.URLMangler; import static com.google.common.base.Preconditions.checkNotNull; public class WMSGetCapabilities { private static Log log = LogFactory.getLog(WMSGetCapabilities.class); private TileLayerDispatcher tld; private String urlStr; private boolean includeVendorSpecific = false; protected WMSGetCapabilities(TileLayerDispatcher tld, HttpServletRequest servReq, String baseUrl, String contextPath, URLMangler urlMangler) { this.tld = tld; urlStr = urlMangler.buildURL(baseUrl, contextPath, WMSService.SERVICE_PATH) + "?SERVICE=WMS&"; String[] tiledKey = { "TILED" }; Map<String, String> tiledValue = ServletUtils.selectedStringsFromMap( servReq.getParameterMap(), servReq.getCharacterEncoding(), tiledKey); if (tiledValue != null && tiledValue.size() > 0) { includeVendorSpecific = Boolean.parseBoolean(tiledValue.get("TILED")); } } protected void writeResponse(HttpServletResponse response) { final Charset encoding = StandardCharsets.UTF_8; byte[] data = generateGetCapabilities(encoding).getBytes(encoding); response.setStatus(HttpServletResponse.SC_OK); response.setContentType("application/vnd.ogc.wms_xml"); response.setCharacterEncoding(encoding.name()); response.setContentLength(data.length); response.setHeader("content-disposition", "inline;filename=wms-getcapabilities.xml"); try (OutputStream os = response.getOutputStream();){ os.write(data); os.flush(); } catch (IOException ioe) { log.debug("Caught IOException" + ioe.getMessage()); } } String generateGetCapabilities(Charset encoding) { StringBuilder str = new StringBuilder(); XMLBuilder xml = new XMLBuilder(str); try { xml.header("1.0", encoding); xml.appendUnescaped("<!DOCTYPE WMT_MS_Capabilities SYSTEM \"http://schemas.opengis.net/wms/1.1.1/capabilities_1_1_1.dtd\" "); if (includeVendorSpecific) { xml.appendUnescaped("[\n"); xml.appendUnescaped("<!ELEMENT VendorSpecificCapabilities (TileSet*) >\n"); xml.appendUnescaped("<!ELEMENT TileSet (SRS, BoundingBox?, Resolutions, Width, Height, Format, Layers*, Styles*) >\n"); xml.appendUnescaped("<!ELEMENT Resolutions (#PCDATA) >\n"); xml.appendUnescaped("<!ELEMENT Width (#PCDATA) >\n"); xml.appendUnescaped("<!ELEMENT Height (#PCDATA) >\n"); xml.appendUnescaped("<!ELEMENT Layers (#PCDATA) >\n"); xml.appendUnescaped("<!ELEMENT Styles (#PCDATA) >\n"); xml.appendUnescaped("]"); } xml.appendUnescaped(">\n"); xml.indentElement("WMT_MS_Capabilities").attribute("version", "1.1.1"); // The actual meat service(xml); capability(xml); xml.endElement(); } catch (IOException e) { // Should not happen as StringBuilder doesn't throw IOException throw new IllegalStateException(e); } return str.toString(); } private void service(XMLBuilder xml) throws IOException { ServiceInformation servInfo = tld.getServiceInformation(); xml.indentElement("Service"); xml.indentElement("Name").text("OGC:WMS").endElement(); if (servInfo == null) { xml.indentElement("Title").text("Web Map Service - GeoWebCache").endElement(); } else { xml.indentElement("Title").text(servInfo.getTitle()).endElement(); xml.indentElement("Abstract").text(servInfo.getDescription()).endElement(); if (servInfo.getKeywords() != null) { xml.indentElement("KeywordList"); Iterator<String> keywordIter = servInfo.getKeywords().iterator(); while (keywordIter.hasNext()) { xml.indentElement("Keyword").text(keywordIter.next()).endElement(); } xml.endElement(); } } onlineResource(xml, urlStr); serviceContact(xml); if (servInfo != null) { xml.indentElement("Fees").text(servInfo.getFees()).endElement(); xml.indentElement("AccessConstraints").text(servInfo.getAccessConstraints()).endElement(); } xml.endElement(); } private void serviceContact(XMLBuilder xml) throws IOException { ServiceInformation servInfo = tld.getServiceInformation(); if (servInfo == null) { return; } ServiceProvider servProv = servInfo.getServiceProvider(); if (servProv == null) { return; } ServiceContact servCont = servProv.getServiceContact(); xml.indentElement("ContactInformation"); if (servProv.getProviderName() != null || servCont != null) { xml.indentElement("ContactPersonPrimary"); if (servCont != null) { xml.simpleElement("ContactPerson",servCont.getIndividualName(), true); } xml.simpleElement("ContactOrganization",servProv.getProviderName(), true); xml.endElement(); if (servCont != null) { xml.simpleElement("ContactPosition", servCont.getPositionName(), true); xml.indentElement("ContactAddress"); xml.simpleElement("AddressType", servCont.getAddressType(), true); xml.simpleElement("Address", servCont.getAddressStreet(), true); xml.simpleElement("City", servCont.getAddressCity(), true); xml.simpleElement("StateOrProvince", servCont.getAddressAdministrativeArea(), true); xml.simpleElement("PostCode", servCont.getAddressPostalCode(), true); xml.simpleElement("Country", servCont.getAddressCountry(), true); xml.endElement(); xml.simpleElement("ContactVoiceTelephone", servCont.getPhoneNumber(), true); xml.simpleElement("ContactFacsimileTelephone", servCont.getFaxNumber(), true); xml.simpleElement("ContactElectronicMailAddress", servCont.getAddressEmail(), true); } } xml.endElement(); } private void capability(XMLBuilder xml) throws IOException { xml.indentElement("Capability"); xml.indentElement("Request"); capabilityRequestGetCapabilities(xml); capabilityRequestGetMap(xml); capabilityRequestGetFeatureInfo(xml); capabilityRequestDescribeLayer(xml); capabilityRequestGetLegendGraphic(xml); xml.endElement(); capabilityException(xml); if (this.includeVendorSpecific) { capabilityVendorSpecific(xml); } capabilityLayerOuter(xml); xml.endElement(); } XMLBuilder onlineResource(XMLBuilder xml, String url) throws IOException { return xml.indentElement("OnlineResource") .attribute("xmlns:xlink", "http://www.w3.org/1999/xlink") .attribute("xlink:type", "simple") .attribute("xlink:href", url) .endElement(); } XMLBuilder dcpType(XMLBuilder xml, String url) throws IOException { xml.indentElement("DCPType"); xml.indentElement("HTTP"); xml.indentElement("Get"); onlineResource(xml, url); xml.endElement(); xml.endElement(); xml.endElement(); return xml; } XMLBuilder capability(XMLBuilder xml, String name, Collection<String> formats, String url) throws IOException { xml.indentElement(name); for(String format:formats) { xml.simpleElement("Format", format, true); } dcpType(xml, url); xml.endElement(); return xml; } private void capabilityRequestGetCapabilities(XMLBuilder xml) throws IOException { capability(xml, "GetCapabilities", Collections.singleton("application/vnd.ogc.wms_xml"), urlStr); } private void capabilityRequestGetMap(XMLBuilder xml) throws IOException { // Find all the formats we support Iterable<TileLayer> layerIter = tld.getLayerList(); HashSet<String> formats = new HashSet<String>(); for (TileLayer layer : layerIter) { if (!layer.isEnabled() || !layer.isAdvertised()) { continue; } if (layer.getMimeTypes() != null) { Iterator<MimeType> mimeIter = layer.getMimeTypes().iterator(); while (mimeIter.hasNext()) { MimeType mime = mimeIter.next(); formats.add(mime.getFormat()); } } else { formats.add("image/png"); formats.add("image/jpeg"); } } capability(xml, "GetMap", formats, urlStr); } private void capabilityRequestGetFeatureInfo(XMLBuilder xml) throws IOException { // Find all the info formats we support Iterable<TileLayer> layerIter = tld.getLayerList(); HashSet<String> formats = new HashSet<String>(); for (TileLayer layer : layerIter) { if (!layer.isEnabled() || !layer.isAdvertised()) { continue; } if (layer.getMimeTypes() != null) { Iterator<MimeType> mimeIter = layer.getInfoMimeTypes().iterator(); while (mimeIter.hasNext()) { MimeType mime = mimeIter.next(); formats.add(mime.getFormat()); } } else { formats.add("text/plain"); formats.add("text/html"); formats.add("application/vnd.ogc.gml"); } } capability(xml, "GetFeatureInfo", formats, urlStr); } private void capabilityRequestDescribeLayer(XMLBuilder xml) throws IOException { capability(xml, "DescribeLayer", Collections.singleton("application/vnd.ogc.wms_xml"), urlStr); } private void capabilityRequestGetLegendGraphic(XMLBuilder xml) throws IOException { capability(xml, "GetLegendGraphic", Arrays.asList("image/png", "image/jpeg", "image/gif"), urlStr); } private void capabilityException(XMLBuilder xml) throws IOException { xml.indentElement("Exception"); xml.simpleElement("Format","application/vnd.ogc.se_xml", true); xml.endElement(); } private void capabilityVendorSpecific(XMLBuilder xml) throws IOException { xml.indentElement("VendorSpecificCapabilities"); Iterable<TileLayer> layerIter = tld.getLayerList(); for (TileLayer layer : layerIter) { if (!layer.isEnabled() || !layer.isAdvertised()) { continue; } for (String gridSetId : layer.getGridSubsets()) { GridSubset grid = layer.getGridSubset(gridSetId); List<String> formats = new ArrayList<String>(2); if (layer.getMimeTypes() != null) { for (MimeType mime : layer.getMimeTypes()) { formats.add(mime.getFormat()); } } else { formats.add(ImageMime.png.getFormat()); formats.add(ImageMime.jpeg.getFormat()); } List<String> styles = getStyles(layer.getParameterFilters()); Map<String, LegendInfo> legendsInfo = layer.getLayerLegendsInfo(); for (String format : formats) { for (String style : styles) { try { capabilityVendorSpecificTileset(xml, layer, grid, format, style, legendsInfo.get(style)); } catch (GeoWebCacheException e) { log.error(e.getMessage()); } } } } } xml.endElement(); } /** * @return a list with an empty string for the default style, and any other style name verbatim */ private List<String> getStyles(List<ParameterFilter> parameterFilters) { List<String> styles = new ArrayList<String>(2); styles.add("");// the default style if (parameterFilters != null) { for (ParameterFilter filter : parameterFilters) { if (!"STYLES".equalsIgnoreCase(filter.getKey())) { continue; } final String defaultStyle = filter.getDefaultValue(); for (String style : filter.getLegalValues()) { if (!defaultStyle.equals(style)) { styles.add(style); } } } } return styles; } private void capabilityVendorSpecificTileset(XMLBuilder xml, TileLayer layer, GridSubset grid, String formatStr, String styleName, LegendInfo legendInfo) throws GeoWebCacheException, IOException { String srsStr = grid.getSRS().toString(); StringBuilder resolutionsStr = new StringBuilder(); double[] res = grid.getResolutions(); for (int i = 0; i < res.length; i++) { resolutionsStr.append(Double.toString(res[i]) + " "); } String[] bs = boundsPrep(grid.getCoverageBestFitBounds()); xml.indentElement("TileSet"); xml.simpleElement("SRS", srsStr, true); xml.boundingBox(srsStr, bs[0], bs[1], bs[2], bs[3]); xml.simpleElement("Resolutions", resolutionsStr.toString(), true); xml.simpleElement("Width", Integer.toString(grid.getTileWidth()), true); xml.simpleElement("Height", Integer.toString(grid.getTileHeight()), true); xml.simpleElement("Format", formatStr, true); xml.simpleElement("Layers", layer.getName(), true); xml.indentElement("Styles"); xml.indentElement("Style"); xml.simpleElement("ows:Identifier", ServletUtils.URLEncode(styleName), true); encodeStyleLegendGraphic(xml, legendInfo); xml.endElement(); xml.endElement(); xml.endElement(); } /** * XML encodes the provided legend information. If the provided information legend is NULL * nothing is done. */ private void encodeStyleLegendGraphic(XMLBuilder xml, LegendInfo legendInfo) throws IOException { if (legendInfo == null) { // nothing to do return; } // validate legend info (this attributes are mandatory for WMS 1.1.0, 1.1.1 and 1.3.0) checkNotNull(legendInfo.getWidth(), "Legend with is mandatory in WMS (1.1.0, 1.1.1 and 1.3.0)."); checkNotNull(legendInfo.getHeight(), "Legend height is mandatory in WMS (1.1.0, 1.1.1 and 1.3.0)."); checkNotNull(legendInfo.getFormat(), "Legend format is mandatory in WMS (1.1.0, 1.1.1 and 1.3.0)."); checkNotNull(legendInfo.getLegendUrl(), "Legend URL is mandatory in WMS (1.1.0, 1.1.1 and 1.3.0)."); xml.indentElement("LegendURL"); // add with and height attributes xml.attribute("width", String.valueOf(legendInfo.getWidth())); xml.attribute("height", String.valueOf(legendInfo.getHeight())); // add format element xml.simpleElement("Format", legendInfo.getFormat(), true); // add online resource element xml.indentElement("OnlineResource"); xml.attribute("xlink:type", "simple"); xml.attribute("xlink:href", legendInfo.getLegendUrl()); xml.endElement("OnlineResource"); // close legend URL element xml.endElement("LegendURL"); } private void capabilityLayerOuter(XMLBuilder xml) throws IOException { xml.indentElement("Layer"); xml.simpleElement("Title", "GeoWebCache WMS", true); xml.simpleElement("Abstract", "Note that not all GeoWebCache instances provide a full WMS service.", true); xml.latLonBoundingBox(-180.0, -90.0, 180.0, 90.0); Iterable<TileLayer> layerIter = tld.getLayerList(); for (TileLayer layer : layerIter) { if (!layer.isEnabled() || !layer.isAdvertised()) { continue; } try { capabilityLayerInner(xml, layer); } catch (GeoWebCacheException e) { log.error(e.getMessage()); } } xml.endElement(); } private void capabilityLayerInner(XMLBuilder xml, TileLayer layer) throws GeoWebCacheException, IOException { xml.indentElement("Layer"); if (layer.isQueryable()) { xml.attribute("queryable", "1"); } xml.simpleElement("Name", layer.getName(), true); if (layer.getMetaInformation() != null) { LayerMetaInformation metaInfo = layer.getMetaInformation(); xml.simpleElement("Title", metaInfo.getTitle(), true); xml.simpleElement("Abstract", metaInfo.getDescription(), true); } else { xml.simpleElement("Title", layer.getName(), true); } if (layer.getMetadataURLs() != null) { for (MetadataURL metadataURL : layer.getMetadataURLs()) { xml.indentElement("MetadataURL"); xml.attribute("type", metadataURL.getType()); xml.simpleElement("Format", metadataURL.getFormat(), true); onlineResource(xml, metadataURL.getUrl().toString()); // TODO should this be URLEncoded? xml.endElement(); } } { TreeSet<SRS> srsSet = new TreeSet<>(); HashSet<GridSubset> gridSubsetSet = new HashSet<>(); for (String gridSetId : layer.getGridSubsets()) { GridSubset curGridSubSet = layer.getGridSubset(gridSetId); SRS curSRS = curGridSubSet.getSRS(); if (!srsSet.contains(curSRS)) { srsSet.add(curSRS); gridSubsetSet.add(curGridSubSet); } } for(SRS curSRS: srsSet) { xml.simpleElement("SRS", curSRS.toString(), true); } GridSubset epsg4326GridSubSet = layer.getGridSubsetForSRS(SRS.getEPSG4326()); if (null != epsg4326GridSubSet) { String[] bs = boundsPrep(epsg4326GridSubSet.getCoverageBestFitBounds()); xml.latLonBoundingBox(bs[0], bs[1], bs[2], bs[3]); } for(GridSubset curGridSubSet: gridSubsetSet) { String[] bs = boundsPrep(curGridSubSet.getCoverageBestFitBounds()); xml.boundingBox(curGridSubSet.getSRS().toString(), bs[0], bs[1], bs[2], bs[3]); } } // WMS 1.1 Dimensions // TODO change API to not use string builder. Pass an XML Builder, or ask for a model // object. KS if (layer.getParameterFilters() != null) { StringBuilder dims = new StringBuilder(); StringBuilder extents = new StringBuilder(); for (ParameterFilter parameterFilter : layer.getParameterFilters()) { if (parameterFilter instanceof WMSDimensionProvider) { ((WMSDimensionProvider) parameterFilter).appendDimensionElement(dims, " "); ((WMSDimensionProvider) parameterFilter).appendExtentElement(extents, " "); } } if (dims.length() > 0 && extents.length() > 0) { xml.appendUnescaped(dims.toString()); xml.appendUnescaped(extents.toString()); } } // TODO style? xml.endElement(); } String[] boundsPrep(BoundingBox bbox) { String[] bs = { Double.toString(bbox.getMinX()), Double.toString(bbox.getMinY()), Double.toString(bbox.getMaxX()), Double.toString(bbox.getMaxY()) }; return bs; } }