/* Copyright (c) 2010 TOPP - www.openplans.org. All rights reserved. * This code is licensed under the GPL 2.0 license, availible 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.List; 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.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; /** * 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 Response { 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); this.wms = wms; } /** * @return {@code "text/xml"} * @see org.geoserver.ows.Response#getMimeType(java.lang.Object, * org.geoserver.platform.Operation) */ @Override public String getMimeType(final Object value, final Operation operation) throws ServiceException { if (value instanceof GetCapabilitiesTransformer) { return GetCapabilitiesTransformer.WMS_CAPS_MIME; } throw new IllegalArgumentException(value == null ? "null" : value.getClass().getName() + "/" + operation.getId()); } /** * @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.toString()); Source source = new StreamSource(new ByteArrayInputStream(rawCapabilities)); 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 * @return */ 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; } }