/* 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;
}
}