/* (c) 2014 Open Source Geospatial Foundation - all rights reserved * (c) 2001 - 2013 OpenPlans * This code is licensed under the GPL 2.0 license, available at the root * application directory. */ package org.geoserver.wms.capabilities; import static org.geoserver.ows.util.ResponseUtils.buildSchemaURL; import java.io.ByteArrayInputStream; import java.io.ByteArrayOutputStream; import java.io.IOException; import java.io.OutputStream; import java.util.Arrays; import java.util.List; import java.util.logging.Logger; import javax.xml.parsers.SAXParser; import javax.xml.parsers.SAXParserFactory; import javax.xml.transform.Result; import javax.xml.transform.Source; import javax.xml.transform.Transformer; import javax.xml.transform.TransformerConfigurationException; import javax.xml.transform.TransformerException; import javax.xml.transform.TransformerFactory; import javax.xml.transform.sax.SAXSource; import javax.xml.transform.stream.StreamResult; import javax.xml.transform.stream.StreamSource; import org.geoserver.ows.Response; import org.geoserver.platform.Operation; import org.geoserver.platform.ServiceException; import org.geoserver.wfs.CapabilitiesTransformer; import org.geoserver.wms.ExtendedCapabilitiesProvider; import org.geoserver.wms.GetCapabilities; import org.geoserver.wms.GetCapabilitiesRequest; import org.geoserver.wms.GetMapRequest; import org.geoserver.wms.WMS; import org.xml.sax.EntityResolver; import org.xml.sax.InputSource; import org.xml.sax.SAXException; import org.xml.sax.XMLReader; /** * OWS {@link Response} bean to handle WMS {@link GetCapabilities} results. * * <p> * Note since the XSLT API does not support declaring internal DTDs, and we may need to in order for * {@link ExtendedCapabilitiesProvider}s to contribute to the document type definition, if there's * any {@code ExtendedCapabilitiesProvider} that contributes to this capabilities document, the * plain document as created by {@link CapabilitiesTransformer} is gonna be run through an XSLT * transformation that will insert the proper internal DTD declaration. * </p> * <p> * Each {@link ExtendedCapabilitiesProvider#getVendorSpecificCapabilitiesRoots()} is added to the * list of direct children of the {@code VendorSpecificCapabilities} element, and each * {@link ExtendedCapabilitiesProvider#getVendorSpecificCapabilitiesChildDecls()} is added to the * list of internal DTD elements, like in the following example: * * <pre> * <code> * <!DOCTYPE WMT_MS_Capabilities SYSTEM "BASE_URL/schemas/wms/1.1.1/WMS_MS_Capabilities.dtd"[ * <!ELEMENT VendorSpecificCapabilities (TileSet*, Test?) > * <!ELEMENT Resolutions (#PCDATA) > * <!ELEMENT TestChild (#PCDATA) > * ]> * </code> * </pre> * * Where BASE_URL is the {@link GetMapRequest#getBaseUrl()}, {@code TileSet*} and {@code Test?} are * contributed through {@link ExtendedCapabilitiesProvider#getVendorSpecificCapabilitiesRoots()}, * and {@code <!ELEMENT Resolutions (#PCDATA) >} and {@code <!ELEMENT TestChild (#PCDATA) >} through * {@link ExtendedCapabilitiesProvider#getVendorSpecificCapabilitiesChildDecls()} * </p> * * @author groldan * */ public class GetCapabilitiesResponse extends BaseCapabilitiesResponse { private static final Logger LOGGER = org.geotools.util.logging.Logging.getLogger(GetCapabilitiesResponse.class); private WMS wms; /** * @param wms * needed for {@link WMS#getAvailableExtendedCapabilitiesProviders()} in order to * check of internal DTD elements shall be added to the output document */ public GetCapabilitiesResponse(final WMS wms) { super(GetCapabilitiesTransformer.class,GetCapabilitiesTransformer.WMS_CAPS_DEFAULT_MIME); this.wms = wms; } /** * @param value * {@link GetCapabilitiesTransformer} * @param output * destination * @param operation * The operation identifier which resulted in <code>value</code> * @see org.geoserver.ows.Response#write(java.lang.Object, java.io.OutputStream, * org.geoserver.platform.Operation) */ @Override public void write(final Object value, final OutputStream output, final Operation operation) throws IOException, ServiceException { final GetCapabilitiesTransformer transformer = (GetCapabilitiesTransformer) value; final GetCapabilitiesRequest request = (GetCapabilitiesRequest) operation.getParameters()[0]; final String internalDTDDeclaration = getInternalDTDDeclaration(request); if (internalDTDDeclaration == null) { // transform directly to output try { transformer.transform(request, output); } catch (TransformerException e) { throw new ServiceException(e); } } else { // we need to add internal DTD elements, and need to use an XSL to do that, // since the XSLT API does not support it out of the box byte[] rawCapabilities; Transformer dtdIncludeTransformer; { ByteArrayOutputStream target = new ByteArrayOutputStream(); try { transformer.transform(request, target); } catch (TransformerException e) { throw new ServiceException(e); } rawCapabilities = target.toByteArray(); } { // Explicitly use SAXON's transformer factory. For some reason xalan's does not // work TransformerFactory tFactory = TransformerFactory.newInstance(); String xsltSystemId = getClass().getResource("getcaps_111_internalDTD.xsl") .toExternalForm(); Source tsource = new StreamSource(xsltSystemId); try { dtdIncludeTransformer = tFactory.newTransformer(tsource); } catch (TransformerConfigurationException e) { throw new ServiceException(e); } } // Set the full DTD declaration, including internal elements provided by // ExtendedCapabilitiesProviders, as an stylesheet parameter dtdIncludeTransformer.setParameter("DTDDeclaration", internalDTDDeclaration); Source source; try { /* * As per GEOS-4945, we need to provide the XSL transformer a namespace aware input * source that doesn't complain if the resulting DTD location is unreachable. To do * so, a SAX XMLReader with an EntityResolver that resolves to the local copy of the * DTD will be used. */ SAXParserFactory spf = SAXParserFactory.newInstance(); spf.setNamespaceAware(true); // xslt _needs_ namespace aware input source SAXParser sp = spf.newSAXParser(); XMLReader rawCapsReader = sp.getXMLReader(); rawCapsReader.setEntityResolver(new EntityResolver() { @Override public InputSource resolveEntity(String publicId, String systemId) throws SAXException { final String dtdLocation = "/schemas/wms/1.1.1/WMS_MS_Capabilities.dtd"; String dtdSystemId = getClass().getResource(dtdLocation).toExternalForm(); return new InputSource(dtdSystemId); } }); source = new SAXSource(rawCapsReader, new InputSource(new ByteArrayInputStream( rawCapabilities))); } catch (Exception e) { throw new ServiceException(e); } Result result = new StreamResult(output); try { dtdIncludeTransformer.transform(source, result); } catch (TransformerException e) { throw new ServiceException(e); } } } /** * Builds a full WMS 1.1.1 GetCapabilities internal DTD declaration by asking the configured * {@link ExtendedCapabilitiesProvider}s for the elements to contribute to the DTD. * <p> * Each {@link ExtendedCapabilitiesProvider#getVendorSpecificCapabilitiesRoots()} is added to * the list of direct children of the {@code VendorSpecificCapabilities} element, and each * {@link ExtendedCapabilitiesProvider#getVendorSpecificCapabilitiesChildDecls()} is added to * the list of internal DTD elements, like in the following example: * * <pre> * <code> * <!DOCTYPE WMT_MS_Capabilities SYSTEM "BASE_URL/schemas/wms/1.1.1/WMS_MS_Capabilities.dtd"[ * <!ELEMENT VendorSpecificCapabilities (TileSet*, Test?) > * <!ELEMENT Resolutions (#PCDATA) > * <!ELEMENT TestChild (#PCDATA) > * ]> * </code> * </pre> * * Where BASE_URL is the {@link GetMapRequest#getBaseUrl()}, {@code TileSet*} and {@code Test?} * are contributed through * {@link ExtendedCapabilitiesProvider#getVendorSpecificCapabilitiesRoots()}, and * {@code <!ELEMENT Resolutions (#PCDATA) >} and {@code <!ELEMENT TestChild (#PCDATA) >} through * {@link ExtendedCapabilitiesProvider#getVendorSpecificCapabilitiesChildDecls()} * </p> * * @param request * */ private String getInternalDTDDeclaration(final GetCapabilitiesRequest request) { // do we need to add internal DTD declarations? List<ExtendedCapabilitiesProvider> providers; providers = wms.getAvailableExtendedCapabilitiesProviders(); StringBuilder vendorSpecificCapsElements = new StringBuilder( "<!ELEMENT VendorSpecificCapabilities ("); StringBuilder internalDTDElements = new StringBuilder(); int numRoots = 0; for (ExtendedCapabilitiesProvider provider : providers) { List<String> roots = provider.getVendorSpecificCapabilitiesRoots(request); if (roots != null && roots.size() > 0) { for (String vendorRoot : roots) { numRoots++; if (numRoots > 1) { vendorSpecificCapsElements.append(", "); } vendorSpecificCapsElements.append(vendorRoot); } List<String> childDecls = provider.getVendorSpecificCapabilitiesChildDecls(request); for (String internalElement : childDecls) { internalDTDElements.append(internalElement); internalDTDElements.append('\n'); } } } vendorSpecificCapsElements.append(") >\n"); String fullDTDDeclaration = null; if (numRoots > 0) { final String baseURL = request.getBaseUrl(); String dtdUrl = buildSchemaURL(baseURL, "wms/1.1.1/WMS_MS_Capabilities.dtd"); StringBuilder builder = new StringBuilder("<!DOCTYPE WMT_MS_Capabilities SYSTEM \""); builder.append(dtdUrl).append("\"[\n"); builder.append(vendorSpecificCapsElements); builder.append(internalDTDElements); builder.append("]>\n"); fullDTDDeclaration = builder.toString(); } return fullDTDDeclaration; } @Override public String getMimeType(Object value, Operation operation) throws ServiceException { // check that we have a valid value (same check as the super method) if (value == null || !value.getClass().isAssignableFrom(super.getBinding())) { // this is not good (same error message as the super method) String message = String.format("%s/%s", value == null ? "null" : value.getClass().getName(), operation.getId()); throw new IllegalArgumentException(message); } // search for the get capabilities object GetCapabilitiesRequest request = null; for (Object parameter : operation.getParameters()) { if (parameter instanceof GetCapabilitiesRequest) { // we found our request request = (GetCapabilitiesRequest) parameter; } } if (request == null) { // unlikely but no get capabilities request was found, fall back to the default behavior return super.getMimeType(value, operation); } // let's see if we have the format parameter String format = request.getRawKvp().get("FORMAT"); if (format == null || format.isEmpty()) { // no format parameter, fall back to default behavior return super.getMimeType(value, operation); } // if the requested format is a valid mime type return it for (String mimeType : GetCapabilitiesTransformer.WMS_CAPS_AVAIL_MIME) { if (format.equalsIgnoreCase(mimeType)) { // the format parameter value maps to a valid mime type, returning the associate mime type return mimeType; } } // the requested format is not supported, throw an exception throw new RuntimeException(String.format("The request format '%s' is not supported.", format)); } }