/* (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.wcs2_0.response; import static org.apache.commons.lang.StringUtils.isBlank; import static org.apache.commons.lang.StringUtils.isNotBlank; import static org.geoserver.ows.util.ResponseUtils.appendQueryString; import static org.geoserver.ows.util.ResponseUtils.buildURL; import java.net.URI; import java.net.URISyntaxException; import java.util.ArrayList; import java.util.Collection; import java.util.Collections; import java.util.Enumeration; import java.util.HashSet; import java.util.Iterator; import java.util.List; import java.util.Map; import java.util.Set; import java.util.TreeSet; import java.util.logging.Level; import java.util.logging.Logger; import net.opengis.wcs20.GetCapabilitiesType; import org.geoserver.ExtendedCapabilitiesProvider; import org.geoserver.catalog.CoverageInfo; import org.geoserver.catalog.KeywordInfo; import org.geoserver.config.ContactInfo; import org.geoserver.config.GeoServer; import org.geoserver.config.ResourceErrorHandling; import org.geoserver.config.SettingsInfo; import org.geoserver.ows.URLMangler; import org.geoserver.platform.GeoServerExtensions; import org.geoserver.platform.ServiceException; import org.geoserver.wcs.WCSInfo; import org.geoserver.wcs.responses.CoverageResponseDelegate; import org.geoserver.wcs.responses.CoverageResponseDelegateFinder; import org.geoserver.wcs2_0.GetCoverage; import org.geoserver.wcs2_0.WCS20Const; import org.geoserver.wcs2_0.util.NCNameResourceCodec; import org.geotools.referencing.CRS; import org.geotools.util.logging.Logging; import org.geotools.wcs.v2_0.WCS; import org.geotools.xml.transform.TransformerBase; import org.geotools.xml.transform.Translator; import org.opengis.geometry.BoundingBox; import org.vfny.geoserver.global.CoverageInfoLabelComparator; import org.vfny.geoserver.wcs.WcsException; import org.xml.sax.Attributes; import org.xml.sax.ContentHandler; import org.xml.sax.SAXException; import org.xml.sax.helpers.AttributesImpl; import org.xml.sax.helpers.NamespaceSupport; /** * Transformer for GetCapabilities * * @author Emanuele Tajariol (etj) - GeoSolutions * @author Simone Giannecchini, GeoSolutions */ public class WCS20GetCapabilitiesTransformer extends TransformerBase { private static final Logger LOGGER = Logging.getLogger(WCS20GetCapabilitiesTransformer.class); protected static final String CUR_VERSION = WCS20Const.V201; private WCSInfo wcs; private final boolean skipMisconfigured; private CoverageResponseDelegateFinder responseFactory; /** {@link Enum} that identifies the various sections.*/ enum SECTIONS { ServiceIdentification, ServiceProvider, OperationsMetadata, ServiceMetadata, Contents, Languages, All; public static final Set<String> names; static { Set<String> tmp = new HashSet<String>(); for (SECTIONS section : SECTIONS.values()) { tmp.add(section.name()); } names = Collections.unmodifiableSet(tmp); } }; public WCS20GetCapabilitiesTransformer(GeoServer gs, CoverageResponseDelegateFinder responseFactory) { this.wcs = gs.getService(WCSInfo.class); this.skipMisconfigured = ResourceErrorHandling.SKIP_MISCONFIGURED_LAYERS.equals( gs.getGlobal().getResourceErrorHandling()); this.responseFactory = responseFactory; setNamespaceDeclarationEnabled(false); } @Override public Translator createTranslator(ContentHandler handler) { return new WCS20GetCapabilitiesTranslator(handler); } private class WCS20GetCapabilitiesTranslator extends TranslatorSupport { /** * DOCUMENT ME! * * @uml.property name="request" * @uml.associationEnd multiplicity="(0 1)" */ private GetCapabilitiesType request; private List<WCSExtendedCapabilitiesProvider> extensions; private org.geoserver.ExtendedCapabilitiesProvider.Translator translator; private TranslatorHelper helper; /** * Creates a new WFSCapsTranslator object. * * @param handler * DOCUMENT ME! */ public WCS20GetCapabilitiesTranslator(ContentHandler handler) { super(handler, null, null); this.helper = new TranslatorHelper(); this.extensions = GeoServerExtensions.extensions(WCSExtendedCapabilitiesProvider.class); // register namespaces provided by extended capabilities NamespaceSupport namespaces = getNamespaceSupport(); namespaces.declarePrefix("wcscrs", "http://www.opengis.net/wcs/service-extension/crs/1.0"); namespaces.declarePrefix("int", "http://www.opengis.net/WCS_service-extension_interpolation/1.0"); for (WCSExtendedCapabilitiesProvider cp : extensions) { cp.registerNamespaces(namespaces); } this.translator = new ExtendedCapabilitiesProvider.Translator() { @Override public void start(String element, Attributes attributes) { WCS20GetCapabilitiesTranslator.this.start(element, attributes); } @Override public void start(String element) { WCS20GetCapabilitiesTranslator.this.start(element); } @Override public void end(String element) { WCS20GetCapabilitiesTranslator.this.end(element); } @Override public void chars(String text) { WCS20GetCapabilitiesTranslator.this.chars(text); } }; } /** * Encode the object. * * @param o * The Object to encode. * * @throws IllegalArgumentException * if the Object is not encodeable. */ public void encode(Object o) throws IllegalArgumentException { if (!(o instanceof GetCapabilitiesType)) { throw new IllegalArgumentException("Not a GetCapabilitiesType: "+o!=null?o.toString():"null"); } this.request = (GetCapabilitiesType) o; // check the update sequence final long updateSequence = wcs.getGeoServer().getGlobal().getUpdateSequence(); long requestedUpdateSequence = -1; if (request.getUpdateSequence() != null) { try { requestedUpdateSequence = Long.parseLong(request.getUpdateSequence()); } catch (NumberFormatException e) { throw new WcsException("Invalid update sequence number format, " + "should be an integer", WcsException.WcsExceptionCode.InvalidUpdateSequence, "updateSequence"); } if (requestedUpdateSequence > updateSequence) { throw new WcsException("Invalid update sequence value, it's higher " + "than the current value, " + updateSequence, WcsException.WcsExceptionCode.InvalidUpdateSequence, "updateSequence"); } } // check and init sections // handle the sections directive boolean allSections; List<String> sections; if (request.getSections() == null) { sections = Collections.emptyList(); allSections = true; } else { sections = request.getSections().getSection(); allSections = sections.contains(SECTIONS.All.name()); for (String section : sections) { if(! SECTIONS.names.contains(section)) throw new WcsException("Unknown section " + section, WcsException.WcsExceptionCode.InvalidParameterValue, "Sections"); } } // Build the document final AttributesImpl attributes = WCS20Const.getDefaultNamespaces(); attributes.addAttribute("", "version", "version", "", CUR_VERSION); attributes.addAttribute("", "updateSequence", "updateSequence", "", String.valueOf(updateSequence)); helper.registerNamespaces(getNamespaceSupport(), attributes); // TODO: add a config to choose the canonical or local schema String location = buildSchemaLocation(request.getBaseUrl(), WCS.NAMESPACE, "http://schemas.opengis.net/wcs/2.0/wcsGetCapabilities.xsd"); // final String locationDef = WCS.NAMESPACE + " " + buildSchemaURL(request.getBaseUrl(), "wcs/2.0/wcsGetCapabilities.xsd");// attributes.addAttribute("", "xsi:schemaLocation", "xsi:schemaLocation", "", location); start("wcs:Capabilities", attributes); // encode the actual capabilities contents taking into consideration // the sections if (requestedUpdateSequence < updateSequence) { if (allSections || sections.contains(SECTIONS.ServiceIdentification.name())) handleServiceIdentification(); if (allSections || sections.contains(SECTIONS.ServiceProvider.name())) handleServiceProvider(); if (allSections || sections.contains(SECTIONS.OperationsMetadata.name())) handleOperationsMetadata(); if (allSections || sections.contains(SECTIONS.ServiceMetadata.name())) handleServiceMetadata(request); if (allSections || sections.contains(SECTIONS.Contents.name())) handleContents(); if (allSections || sections.contains(SECTIONS.Languages.name())) handleLanguages(); } end("wcs:Capabilities"); } String buildSchemaLocation(String schemaBaseURL, String... locations) { for (WCSExtendedCapabilitiesProvider cp : extensions) { locations = helper.append(locations, cp.getSchemaLocations(schemaBaseURL)); } return helper.buildSchemaLocation(locations); } private void handleServiceMetadata(GetCapabilitiesType ct) { start("wcs:ServiceMetadata"); // formats are part of the document only starting version 2.0.1 if(ct.getAcceptVersions() == null || ct.getAcceptVersions().getVersion() == null || ct.getAcceptVersions().getVersion().isEmpty() || ct.getAcceptVersions().getVersion().contains("2.0.1")) { Set<String> formats = new TreeSet<String>(); for (String format : responseFactory.getOutputFormats()) { CoverageResponseDelegate delegate = responseFactory.encoderFor(format); String mime = delegate.getMimeType(format); try { new URI(mime); formats.add(mime); } catch(URISyntaxException e) { // skip it } } for (String format : formats) { element("wcs:formatSupported", format); } } // the CRS extension requires us to declare the full list of supported CRS start("wcs:Extension"); // add the supported CRS Collection<String> codes; if(wcs.getSRS() == null || wcs.getSRS().isEmpty()) { codes = CRS.getSupportedCodes("EPSG"); } else { codes = wcs.getSRS(); } for (String code : codes) { if(!code.equals("WGS84(DD)")) { element("wcscrs:crsSupported", "http://www.opengis.net/def/crs/EPSG/0/" + code); } } // add the supported interpolation methods element("int:interpolationSupported", "http://www.opengis.net/def/interpolation/OGC/1/nearest-neighbor"); element("int:interpolationSupported", "http://www.opengis.net/def/interpolation/OGC/1/linear"); element("int:interpolationSupported", "http://www.opengis.net/def/interpolation/OGC/1/cubic"); end("wcs:Extension"); end("wcs:ServiceMetadata"); } /** * Handles the service identification of the capabilities document. * * @param config * The OGC service to transform. * * @throws SAXException * For any errors. */ private void handleServiceIdentification() { start("ows:ServiceIdentification"); element("ows:Title", wcs.getTitle()); element("ows:Abstract", wcs.getAbstract()); handleKeywords(wcs.getKeywords()); element("ows:ServiceType", "urn:ogc:service:wcs"); // TODO: check this: some docs specify a "OGC WCS" string element("ows:ServiceTypeVersion", WCS20Const.V201); element("ows:ServiceTypeVersion", WCS20Const.V111); element("ows:ServiceTypeVersion", WCS20Const.V110); element("ows:Profile", "http://www.opengis.net/spec/WCS/2.0/conf/core"); element("ows:Profile", "http://www.opengis.net/spec/WCS_protocol-binding_get-kvp/1.0.1"); // requirement #1 in OGC 09-147r3 element("ows:Profile", "http://www.opengis.net/spec/WCS_protocol-binding_post-xml/1.0"); // don't believe we support this one // element("ows:Profile", "http://www.opengis.net/spec/WCS_service-extension_crs/1.0/conf/crs-discrete-coverage"); element("ows:Profile", "http://www.opengis.net/spec/WCS_service-extension_crs/1.0/conf/crs-gridded-coverage"); // element("ows:Profile","http://www.opengis.net/spec/WCS_coverage-encoding/1.0/conf/coverage-encoding"); // TODO: check specs and URL // === GeoTiff encoding extension element("ows:Profile"," http://www.opengis.net/spec/WCS_geotiff-coverages/1.0/conf/geotiff-coverage");// TODO: check specs and URL // === GML encoding element("ows:Profile","http://www.opengis.net/spec/GMLCOV/1.0/conf/gml-coverage"); element("ows:Profile","http://www.opengis.net/spec/GMLCOV/1.0/conf/special-format"); element("ows:Profile","http://www.opengis.net/spec/GMLCOV/1.0/conf/multipart"); // === Scaling Extension element("ows:Profile","http://www.opengis.net/spec/WCS_service-extension_scaling/1.0/conf/scaling"); // === CRS Extension element("ows:Profile", "http://www.opengis.net/spec/WCS_service-extension_crs/1.0/conf/crs"); // === Interpolation element("ows:Profile", "http://www.opengis.net/spec/WCS_service-extension_interpolation/1.0/conf/interpolation"); element("ows:Profile", "http://www.opengis.net/spec/WCS_service-extension_interpolation/1.0/conf/interpolation-per-axis"); // TODO for time axis element("ows:Profile","http://www.opengis.net/spec/WCS_service-extension_interpolation/1.0/conf/nearest-neighbor"); element("ows:Profile","http://www.opengis.net/spec/WCS_service-extension_interpolation/1.0/conf/linear"); element("ows:Profile","http://www.opengis.net/spec/WCS_service-extension_interpolation/1.0/conf/cubic"); // === Range Subsetting element("ows:Profile","http://www.opengis.net/spec/WCS_service-extension_range-subsetting/1.0/conf/record-subsetting"); // TODO don't believe we support these // element("ows:Profile","http://www.opengis.net/spec/WCS_service-extension_array-subsetting/1.0/conf/array-subsetting"); // element("ows:Profile","http://www.opengis.net/spec/WCS_service-extension_range-subsetting/1.0/conf/nested-subsetting"); String fees = wcs.getFees(); if ( isBlank(fees)) { fees = "NONE"; } element("ows:Fees", fees); String accessConstraints = wcs.getAccessConstraints(); if ( isBlank(accessConstraints)) { accessConstraints = "NONE"; } element("ows:AccessConstraints", accessConstraints); end("ows:ServiceIdentification"); } /** * Handles the service provider of the capabilities document. * * @param config * The OGC service to transform. * * @throws SAXException * For any errors. */ private void handleServiceProvider() { start("ows:ServiceProvider"); SettingsInfo settings = wcs.getGeoServer().getSettings(); element("ows:ProviderName", settings.getContact().getContactOrganization()); AttributesImpl attributes = new AttributesImpl(); attributes.addAttribute("", "xlink:href", "xlink:href", "", settings.getOnlineResource() != null ? settings.getOnlineResource() : ""); element("ows:ProviderSite", null, attributes); handleContact(); end("ows:ServiceProvider"); } /** * Handles the OperationMetadata portion of the document, printing out * the operations and where to bind to them. * * @param config * The global wms. * * @throws SAXException * For any problems. */ private void handleOperationsMetadata() { start("ows:OperationsMetadata"); handleOperation("GetCapabilities", null); handleOperation("DescribeCoverage", null); handleOperation("GetCoverage", null); // specify that we do support xml post encoding, clause 8.3.2.2 of // the WCS 1.1.1 spec AttributesImpl attributes = new AttributesImpl(); attributes.addAttribute(null, "name", "name", null, "PostEncoding"); start("ows:Constraint", attributes); start("ows:AllowedValues"); element("ows:Value", "XML"); // element("ows:Value", "text/xml"); // element("ows:Value", "application/xml"); end("ows:AllowedValues"); end("ows:Constraint"); if(extensions != null && extensions.size() > 0) { try { for (WCSExtendedCapabilitiesProvider provider : extensions) { provider.encodeExtendedOperations(translator, wcs, request); } } catch (Exception e) { throw new ServiceException("Extended capabilities provider threw error", e); } } end("ows:OperationsMetadata"); } private void handleOperation(String capabilityName, Map<String, List<String>> parameters) { AttributesImpl attributes = new AttributesImpl(); attributes.addAttribute(null, "name", "name", null, capabilityName); start("ows:Operation", attributes); final String url = appendQueryString(buildURL(request.getBaseUrl(), "wcs", null, URLMangler.URLType.SERVICE), ""); start("ows:DCP"); start("ows:HTTP"); attributes = new AttributesImpl(); attributes.addAttribute("", "xlink:href", "xlink:href", "", url); element("ows:Get", null, attributes); end("ows:HTTP"); end("ows:DCP"); attributes = new AttributesImpl(); attributes.addAttribute("", "xlink:href", "xlink:href", "", url); start("ows:DCP"); start("ows:HTTP"); element("ows:Post", null, attributes); end("ows:HTTP"); end("ows:DCP"); if (parameters != null && !parameters.isEmpty()) { for (Map.Entry<String, List<String>> param : parameters.entrySet()) { attributes = new AttributesImpl(); attributes.addAttribute("", "name", "name", "", param.getKey()); start("ows:Parameter", attributes); start("ows:AllowedValues"); for (String value : param.getValue()) { element("ows:Value", value); } end("ows:AllowedValues"); end("ows:Parameter"); } } end("ows:Operation"); } /** * DOCUMENT ME! * * @param kwords * DOCUMENT ME! * * @throws SAXException * DOCUMENT ME! */ private void handleKeywords(List<KeywordInfo> kwords) { if( kwords != null && ! kwords.isEmpty()) { start("ows:Keywords"); for (KeywordInfo kword : kwords) { element("ows:Keyword", kword.getValue()); } end("ows:Keywords"); } } /** * Handles contacts. * * @param wcs * the service. */ private void handleContact() { final GeoServer gs = wcs.getGeoServer(); start("ows:ServiceContact"); ContactInfo contact = gs.getSettings().getContact(); elementIfNotEmpty("ows:IndividualName", contact.getContactPerson()); elementIfNotEmpty("ows:PositionName", contact.getContactPosition()); start("ows:ContactInfo"); start("ows:Phone"); elementIfNotEmpty("ows:Voice", contact.getContactVoice()); elementIfNotEmpty("ows:Facsimile", contact.getContactFacsimile()); end("ows:Phone"); start("ows:Address"); elementIfNotEmpty("ows:DeliveryPoint", contact.getAddress()); elementIfNotEmpty("ows:City", contact.getAddressCity()); elementIfNotEmpty("ows:AdministrativeArea", contact.getAddressState()); elementIfNotEmpty("ows:PostalCode", contact.getAddressPostalCode()); elementIfNotEmpty("ows:Country", contact.getAddressCountry()); elementIfNotEmpty("ows:ElectronicMailAddress", contact.getContactEmail()); end("ows:Address"); String or = gs.getSettings().getOnlineResource(); if ( isNotBlank(or)) { AttributesImpl attributes = new AttributesImpl(); attributes.addAttribute("", "xlink:href", "xlink:href", "", or); start("ows:OnlineResource", attributes); end("OnlineResource"); } end("ows:ContactInfo"); end("ows:ServiceContact"); } private void handleWGS84BoundingBox(BoundingBox envelope) { start("ows:WGS84BoundingBox"); element("ows:LowerCorner", new StringBuilder(Double.toString(envelope.getLowerCorner() .getOrdinate(0))).append(" ").append(envelope.getLowerCorner().getOrdinate(1)) .toString()); element("ows:UpperCorner", new StringBuilder(Double.toString(envelope.getUpperCorner() .getOrdinate(0))).append(" ").append(envelope.getUpperCorner().getOrdinate(1)) .toString()); end("ows:WGS84BoundingBox"); } private void handleContents() { start("wcs:Contents"); @SuppressWarnings("unchecked") final Set<CoverageInfo> coverages = new TreeSet<CoverageInfo>(new CoverageInfoLabelComparator()); coverages.addAll(wcs.getGeoServer().getCatalog().getCoverages()); // filter out disabled coverages for (Iterator<CoverageInfo> it = coverages.iterator(); it.hasNext();) { CoverageInfo cv = (CoverageInfo) it.next(); if (!cv.enabled()) { it.remove(); } } for (CoverageInfo cv : coverages) { try { mark(); handleCoverageSummary(cv); commit(); } catch (Exception e) { if (skipMisconfigured) { reset(); LOGGER.log(Level.SEVERE, "Skipping coverage " + cv.prefixedName() + " as its capabilities generation failed", e); } else { throw new RuntimeException("Capabilities document generation failed on coverage " + cv.prefixedName(), e); } } } if(extensions != null && extensions.size() > 0) { start("wcs:Extension"); try { for (WCSExtendedCapabilitiesProvider provider : extensions) { provider.encodeExtendedContents(translator, wcs, new ArrayList<CoverageInfo>(coverages), request); } } catch (Exception e) { throw new ServiceException("Extended capabilities provider threw error", e); } end("wcs:Extension"); } end("wcs:Contents"); } private void handleCoverageSummary(CoverageInfo cv) throws Exception { start("wcs:CoverageSummary"); String covId = NCNameResourceCodec.encode(cv); element("wcs:CoverageId", covId); element("wcs:CoverageSubtype", "RectifiedGridCoverage"); // TODO make this parametric handleWGS84BoundingBox(cv.getLatLonBoundingBox()); handleBoundingBox(cv.boundingBox()); end("wcs:CoverageSummary"); } /** * Spits out the boundingbox for the current coverage taking into account the reprojection policy. * * @param boundingBox an instance of reference * @throws Exception in case we don't manage to retrieve the CRS EPSG code for this bbox (It should not happen!) */ private void handleBoundingBox(BoundingBox boundingBox) throws Exception { // CRS for this bbox final AttributesImpl attributes = new AttributesImpl(); attributes.addAttribute( "", "crs", "crs", "", GetCoverage.SRS_STARTER+CRS.lookupIdentifier(boundingBox.getCoordinateReferenceSystem(), false)); start("ows:BoundingBox",attributes); // LowerCorner element("ows:LowerCorner", new StringBuilder(Double.toString(boundingBox.getLowerCorner() .getOrdinate(0))).append(" ").append(boundingBox.getLowerCorner().getOrdinate(1)) .toString()); // UpperCorner element("ows:UpperCorner", new StringBuilder(Double.toString(boundingBox.getUpperCorner() .getOrdinate(0))).append(" ").append(boundingBox.getUpperCorner().getOrdinate(1)) .toString()); end("ows:BoundingBox"); } private void handleLanguages() { // start("ows:Languages"); // // TODO // end("ows:Languages"); } /** * Writes the element if and only if the content is not null and not * empty * * @param elementName * @param content */ private void elementIfNotEmpty(String elementName, String content) { if ( isNotBlank(content) ) element(elementName, content); } } }