/**
* Copyright (c) Codice Foundation
*
* This 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 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
* Lesser General Public License for more details. A copy of the GNU Lesser General Public License
* is distributed along with this program and can be found at
* <http://www.gnu.org/licenses/lgpl.html>.
*
**/
package org.codice.ddf.spatial.ogc.csw.catalog.endpoint;
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.StringWriter;
import java.io.Writer;
import java.net.URL;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.HashMap;
import java.util.HashSet;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import java.util.Set;
import javax.ws.rs.Consumes;
import javax.ws.rs.GET;
import javax.ws.rs.POST;
import javax.ws.rs.Produces;
import javax.ws.rs.QueryParam;
import javax.ws.rs.core.Context;
import javax.ws.rs.core.MediaType;
import javax.ws.rs.core.UriInfo;
import javax.xml.bind.JAXBContext;
import javax.xml.bind.JAXBElement;
import javax.xml.bind.JAXBException;
import javax.xml.bind.Marshaller;
import javax.xml.namespace.QName;
import javax.xml.parsers.DocumentBuilder;
import javax.xml.parsers.DocumentBuilderFactory;
import javax.xml.parsers.ParserConfigurationException;
import org.apache.commons.io.IOUtils;
import org.apache.commons.lang.ObjectUtils;
import org.apache.commons.lang.StringUtils;
import org.apache.cxf.common.util.CollectionUtils;
import org.codice.ddf.spatial.ogc.csw.catalog.common.Csw;
import org.codice.ddf.spatial.ogc.csw.catalog.common.CswConstants;
import org.codice.ddf.spatial.ogc.csw.catalog.common.CswException;
import org.codice.ddf.spatial.ogc.csw.catalog.common.CswRecordCollection;
import org.codice.ddf.spatial.ogc.csw.catalog.common.CswRequest;
import org.codice.ddf.spatial.ogc.csw.catalog.common.DescribeRecordRequest;
import org.codice.ddf.spatial.ogc.csw.catalog.common.GetCapabilitiesRequest;
import org.codice.ddf.spatial.ogc.csw.catalog.common.GetRecordByIdRequest;
import org.codice.ddf.spatial.ogc.csw.catalog.common.GetRecordsRequest;
import org.codice.ddf.spatial.ogc.csw.catalog.converter.DefaultCswRecordMap;
import org.codice.ddf.spatial.ogc.csw.catalog.endpoint.mappings.CswRecordMapperFilterVisitor;
import org.codice.ddf.spatial.ogc.csw.catalog.transformer.TransformerManager;
import org.geotools.feature.NameImpl;
import org.geotools.filter.AttributeExpressionImpl;
import org.geotools.filter.text.cql2.CQL;
import org.geotools.filter.text.cql2.CQLException;
import org.geotools.xml.Configuration;
import org.geotools.xml.Parser;
import org.opengis.filter.Filter;
import org.opengis.filter.expression.PropertyName;
import org.opengis.filter.sort.SortBy;
import org.osgi.framework.BundleContext;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.w3c.dom.Document;
import org.w3c.dom.Element;
import org.xml.sax.SAXException;
import ddf.catalog.CatalogFramework;
import ddf.catalog.data.Metacard;
import ddf.catalog.data.Result;
import ddf.catalog.federation.FederationException;
import ddf.catalog.filter.FilterBuilder;
import ddf.catalog.filter.FilterDelegate;
import ddf.catalog.filter.impl.SortByImpl;
import ddf.catalog.operation.QueryRequest;
import ddf.catalog.operation.QueryResponse;
import ddf.catalog.operation.impl.QueryImpl;
import ddf.catalog.operation.impl.QueryRequestImpl;
import ddf.catalog.source.SourceUnavailableException;
import ddf.catalog.source.UnsupportedQueryException;
import net.opengis.cat.csw.v_2_0_2.CapabilitiesType;
import net.opengis.cat.csw.v_2_0_2.DescribeRecordResponseType;
import net.opengis.cat.csw.v_2_0_2.DescribeRecordType;
import net.opengis.cat.csw.v_2_0_2.ElementSetType;
import net.opengis.cat.csw.v_2_0_2.GetCapabilitiesType;
import net.opengis.cat.csw.v_2_0_2.GetRecordByIdType;
import net.opengis.cat.csw.v_2_0_2.GetRecordsType;
import net.opengis.cat.csw.v_2_0_2.ObjectFactory;
import net.opengis.cat.csw.v_2_0_2.QueryType;
import net.opengis.cat.csw.v_2_0_2.ResultType;
import net.opengis.cat.csw.v_2_0_2.SchemaComponentType;
import net.opengis.cat.csw.v_2_0_2.TransactionResponseType;
import net.opengis.cat.csw.v_2_0_2.TransactionType;
import net.opengis.filter.v_1_1_0.ComparisonOperatorType;
import net.opengis.filter.v_1_1_0.ComparisonOperatorsType;
import net.opengis.filter.v_1_1_0.EID;
import net.opengis.filter.v_1_1_0.FilterCapabilities;
import net.opengis.filter.v_1_1_0.FilterType;
import net.opengis.filter.v_1_1_0.GeometryOperandsType;
import net.opengis.filter.v_1_1_0.IdCapabilitiesType;
import net.opengis.filter.v_1_1_0.LogicalOperators;
import net.opengis.filter.v_1_1_0.ScalarCapabilitiesType;
import net.opengis.filter.v_1_1_0.SortByType;
import net.opengis.filter.v_1_1_0.SpatialCapabilitiesType;
import net.opengis.filter.v_1_1_0.SpatialOperatorNameType;
import net.opengis.filter.v_1_1_0.SpatialOperatorType;
import net.opengis.filter.v_1_1_0.SpatialOperatorsType;
import net.opengis.ows.v_1_0_0.CodeType;
import net.opengis.ows.v_1_0_0.DCP;
import net.opengis.ows.v_1_0_0.DomainType;
import net.opengis.ows.v_1_0_0.HTTP;
import net.opengis.ows.v_1_0_0.OnlineResourceType;
import net.opengis.ows.v_1_0_0.Operation;
import net.opengis.ows.v_1_0_0.OperationsMetadata;
import net.opengis.ows.v_1_0_0.RequestMethodType;
import net.opengis.ows.v_1_0_0.ResponsiblePartySubsetType;
import net.opengis.ows.v_1_0_0.ServiceIdentification;
import net.opengis.ows.v_1_0_0.ServiceProvider;
/**
* CswEndpoint provides a server implementation of the Catalogue Service for Web (CSW) 2.0.2.
*/
public class CswEndpoint implements Csw {
protected static final String SERVICE_TITLE = "Catalog Service for the Web";
protected static final String SERVICE_ABSTRACT = "DDF CSW Endpoint";
protected static final List<String> SERVICE_TYPE_VERSION = Collections
.unmodifiableList(Arrays.asList(CswConstants.VERSION_2_0_2));
protected static final List<SpatialOperatorNameType> SPATIAL_OPERATORS = Collections
.unmodifiableList(
Arrays.asList(SpatialOperatorNameType.BBOX, SpatialOperatorNameType.BEYOND,
SpatialOperatorNameType.CONTAINS, SpatialOperatorNameType.CROSSES,
SpatialOperatorNameType.DISJOINT, SpatialOperatorNameType.D_WITHIN,
SpatialOperatorNameType.INTERSECTS, SpatialOperatorNameType.OVERLAPS,
SpatialOperatorNameType.TOUCHES, SpatialOperatorNameType.WITHIN));
protected static final List<ComparisonOperatorType> COMPARISON_OPERATORS = Collections
.unmodifiableList(
Arrays.asList(ComparisonOperatorType.BETWEEN, ComparisonOperatorType.NULL_CHECK,
ComparisonOperatorType.LIKE, ComparisonOperatorType.EQUAL_TO,
ComparisonOperatorType.GREATER_THAN,
ComparisonOperatorType.GREATER_THAN_EQUAL_TO,
ComparisonOperatorType.LESS_THAN,
ComparisonOperatorType.LESS_THAN_EQUAL_TO,
ComparisonOperatorType.EQUAL_TO, ComparisonOperatorType.NOT_EQUAL_TO));
protected static final String PROVIDER_NAME = "DDF";
protected static final String SERVICE_IDENTIFICATION = "ServiceIdentification";
protected static final String SERVICE_PROVIDER = "ServiceProvider";
protected static final String OPERATIONS_METADATA = "OperationsMetadata";
protected static final String FILTER_CAPABILITIES = "Filter_Capabilities";
protected static final List<String> GET_CAPABILITIES_PARAMS = Collections.unmodifiableList(
Arrays.asList(SERVICE_IDENTIFICATION, SERVICE_PROVIDER, OPERATIONS_METADATA,
FILTER_CAPABILITIES));
private static final Logger LOGGER = LoggerFactory.getLogger(CswEndpoint.class);
private static final Configuration PARSER_CONFIG = new org.geotools.filter.v1_1.OGCConfiguration();
private static final String DEFAULT_OUTPUT_FORMAT = MediaType.APPLICATION_XML;
private static Map<String, Element> documentElements = new HashMap<String, Element>();
private static JAXBContext jaxBContext;
private final TransformerManager mimeTypeTransformerManager;
private final TransformerManager schemaTransformerManager;
private FilterBuilder builder;
private BundleContext context;
private CatalogFramework framework;
private CapabilitiesType capabilitiesType;
@Context
private UriInfo uri;
/**
* JAX-RS Server that represents a CSW v2.0.2 Server.
*/
public CswEndpoint(BundleContext context, CatalogFramework ddf, FilterBuilder filterBuilder,
TransformerManager mimeTypeManager, TransformerManager schemaManager) {
this.context = context;
this.framework = ddf;
this.builder = filterBuilder;
this.mimeTypeTransformerManager = mimeTypeManager;
this.schemaTransformerManager = schemaManager;
}
/* Constructor for unit testing */
public CswEndpoint(BundleContext context, CatalogFramework ddf, FilterBuilder filterBuilder,
UriInfo uri, TransformerManager manager, TransformerManager schemaManager) {
this(context, ddf, filterBuilder, manager, schemaManager);
this.uri = uri;
}
public static synchronized JAXBContext getJaxBContext() throws JAXBException {
if (jaxBContext == null) {
jaxBContext = JAXBContext.newInstance("net.opengis.cat.csw.v_2_0_2:"
+ "net.opengis.filter.v_1_1_0:net.opengis.gml.v_3_1_1:net.opengis.ows.v_1_0_0");
}
return jaxBContext;
}
@Override
@GET
@Consumes({"text/xml", "application/xml"})
@Produces({"text/xml", "application/xml"})
public CapabilitiesType getCapabilities(@QueryParam("") GetCapabilitiesRequest request) throws
CswException {
capabilitiesType = buildCapabilitiesType();
if (request.getAcceptVersions() != null) {
validateVersion(request.getAcceptVersions());
}
List<String> sectionList = null;
if (request.getSections() != null) {
String[] sections = request.getSections().split(",");
sectionList = Arrays.asList(sections);
}
return buildCapabilitiesType(sectionList);
}
@Override
@POST
@Consumes({"text/xml", "application/xml"})
@Produces({"text/xml", "application/xml"})
public CapabilitiesType getCapabilities(GetCapabilitiesType request) throws CswException {
capabilitiesType = buildCapabilitiesType();
if (request.getAcceptVersions() != null) {
validateVersion(request.getAcceptVersions().toString());
}
List<String> sectionList = null;
if (request.getSections() != null) {
sectionList = request.getSections().getSection();
}
return buildCapabilitiesType(sectionList);
}
@Override
@GET
@Consumes({"text/xml", "application/xml"})
@Produces({"text/xml", "application/xml"})
public DescribeRecordResponseType describeRecord(
@QueryParam("") DescribeRecordRequest request) throws CswException {
if (request == null) {
throw new CswException("DescribeRecordRequest request is null");
}
validateOutputFormat(request.getOutputFormat());
validateSchemaLanguage(request.getSchemaLanguage());
Map<String, String> namespacePrefixToUriMappings = request
.parseNamespaces(request.getNamespace());
validateTypeNameToNamespaceMappings(request.getTypeName(), request.getNamespace(),
namespacePrefixToUriMappings);
if (request.getVersion() != null) {
validateVersion(request.getVersion());
}
List<QName> types = typeStringToQNames(request.getTypeName(), namespacePrefixToUriMappings);
return buildDescribeRecordResponseFromTypes(types, request.getVersion());
}
@Override
@POST
@Consumes({"text/xml", "application/xml"})
@Produces({"text/xml", "application/xml"})
public DescribeRecordResponseType describeRecord(DescribeRecordType request) throws
CswException {
if (request == null) {
throw new CswException("DescribeRecordRequest request is null");
}
validateOutputFormat(request.getOutputFormat());
validateSchemaLanguage(request.getSchemaLanguage());
return buildDescribeRecordResponseFromTypes(request.getTypeName(),
CswConstants.VERSION_2_0_2);
}
@Override
@GET
@Produces({MediaType.WILDCARD})
public CswRecordCollection getRecords(@QueryParam("") GetRecordsRequest request) throws
CswException {
if (request == null) {
throw new CswException("GetRecordsRequest request is null");
}
if (StringUtils.isEmpty(request.getVersion())) {
request.setVersion(CswConstants.VERSION_2_0_2);
} else {
validateVersion(request.getVersion());
}
return getRecords(request.get202RecordsType());
}
@Override
@POST
@Consumes({MediaType.TEXT_XML, MediaType.APPLICATION_XML})
@Produces({MediaType.WILDCARD})
public CswRecordCollection getRecords(GetRecordsType request) throws CswException {
if (request == null) {
throw new CswException("GetRecordsType request is null");
}
validateOutputFormat(request.getOutputFormat());
validateOutputSchema(request.getOutputSchema());
if (request.getAbstractQuery() != null) {
if (!request.getAbstractQuery().getValue().getClass().equals(QueryType.class)) {
throw new CswException(
"Unknown QueryType: " + request.getAbstractQuery().getValue().getClass());
}
QueryType query = (QueryType) request.getAbstractQuery().getValue();
validateTypes(query.getTypeNames(), CswConstants.VERSION_2_0_2);
if (query.getConstraint() != null &&
query.getConstraint().isSetFilter() && query.getConstraint().isSetCqlText()) {
throw new CswException("A Csw Query can only have a Filter or CQL constraint");
}
}
return queryCsw(request);
}
@Override
@GET
@Consumes({"text/xml", "application/xml"})
@Produces({"text/xml", "application/xml"})
public CswRecordCollection getRecordById(@QueryParam("") GetRecordByIdRequest request) throws
CswException {
if (request == null) {
throw new CswException("GetRecordByIdRequest request is null");
}
validateOutputFormat(request.getOutputFormat());
validateOutputSchema(request.getOutputSchema());
if (StringUtils.isNotBlank(request.getId())) {
List<String> ids = Arrays.<String>asList(request.getId().split(CswConstants.COMMA));
CswRecordCollection response = queryById(ids);
response.setOutputSchema(request.getOutputSchema());
if (StringUtils.isNotBlank(request.getElementSetName())) {
response.setElementSetType(ElementSetType.fromValue(request.getElementSetName()));
} else {
response.setElementSetType(ElementSetType.SUMMARY);
}
return response;
} else {
throw new CswException("A GetRecordById Query must contain an ID.",
CswConstants.MISSING_PARAMETER_VALUE, "id");
}
}
@Override
@POST
@Consumes({"text/xml", "application/xml"})
@Produces({"text/xml", "application/xml"})
public CswRecordCollection getRecordById(GetRecordByIdType request) throws CswException {
if (request == null) {
throw new CswException("GetRecordByIdRequest request is null");
}
validateOutputFormat(request.getOutputFormat());
validateOutputSchema(request.getOutputSchema());
if (!request.getId().isEmpty()) {
CswRecordCollection response = queryById(request.getId());
response.setOutputSchema(request.getOutputSchema());
if (request.isSetElementSetName() && request.getElementSetName().getValue() != null) {
response.setElementSetType(request.getElementSetName().getValue());
} else {
response.setElementSetType(ElementSetType.SUMMARY);
}
return response;
} else {
throw new CswException("A GetRecordById Query must contain an ID.",
CswConstants.MISSING_PARAMETER_VALUE, "id");
}
}
@Override
@POST
@Consumes({"text/xml", "application/xml"})
@Produces({"text/xml", "application/xml"})
public TransactionResponseType transaction(TransactionType request) throws CswException {
notImplemented("Transaction_POST");
return null;
}
@GET
@Consumes({"text/xml", "application/xml"})
@Produces({"text/xml", "application/xml"})
public void unknownService(@QueryParam("") CswRequest request) throws CswException {
if (request.getService() == null) {
throw new CswException("Missing service value", CswConstants.MISSING_PARAMETER_VALUE,
"service");
}
throw new CswException("Unknown service (" + request.getService() + ")",
CswConstants.INVALID_PARAMETER_VALUE, "service");
}
@POST
@Consumes({"text/xml", "application/xml"})
@Produces({"text/xml", "application/xml"})
public void unknownService() throws CswException {
throw new CswException("Unknown Service", CswConstants.INVALID_PARAMETER_VALUE, "service");
}
@GET
@Consumes({"text/xml", "application/xml"})
@Produces({"text/xml", "application/xml"})
public void unknownOperation(@QueryParam("") CswRequest request) throws CswException {
throw new CswException("No such operation: " + request.getRequest(),
CswConstants.OPERATION_NOT_SUPPORTED, request.getRequest());
}
@POST
@Consumes({"text/xml", "application/xml"})
@Produces({"text/xml", "application/xml"})
public void unknownOperation() throws CswException {
throw new CswException("No such operation", CswConstants.OPERATION_NOT_SUPPORTED, null);
}
/**
* Validates TypeName to namspace uri mapping in query request.
*
* @param typeNames this can be a comma separated list of types which
* can be prefixed with prefixes.
* example csw:Record
* @param namespaces the namespace parameter from the request
* example NAMESPACE=xmlns(csw=http://www.opengis.net/cat/csw/2.0.2)
* @param namespacePrefixToUriMappings map of namespace prefixes to namespace uri
* example key=csw value=http://www.opengis.net/cat/csw/2.0.2
* @throws CswException
*/
private void validateTypeNameToNamespaceMappings(String typeNames, String namespaces,
Map<String, String> namespacePrefixToUriMappings) throws CswException {
// No typeName in query.
if (StringUtils.isBlank(typeNames)) {
return;
}
String[] types = typeNames.split(CswConstants.COMMA);
String prefix = null;
for (String type : types) {
if (type.contains(CswConstants.NAMESPACE_DELIMITER)) {
// Get the prefix. For example in csw:Record, get csw.
prefix = type.split(CswConstants.NAMESPACE_DELIMITER)[0];
} else {
prefix = "";
}
// if the prefix does not map to a provided namespace, throw an exception.
if (!namespacePrefixToUriMappings.containsKey(prefix)) {
throw new CswException(
"Unable to map [" + type + "] to one of the following namespaces ["
+ namespaces + "].");
}
}
}
/**
*
* Returns a list of QNames based on typeNames and namespaces given
*
*
* @param typeNames this can be a comma separated list of types which
* can be prefixed with prefixes.
* example csw:Record
* @param namespacePrefixToUriMappings map of namespace prefixes to namespace uri
* example key=csw value=http://www.opengis.net/cat/csw/2.0.2
* @return List of QNames so that types and namespaces are associated
*/
private List<QName> typeStringToQNames(String typeNames,
Map<String, String> namespacePrefixToUriMappings) throws CswException {
List<QName> qNames = new ArrayList<QName>();
if (typeNames == null) {
return qNames;
}
String[] types = typeNames.split(CswConstants.COMMA);
for (String typeName : types) {
// if type name is in the format prefix:localPart (eg. csw:Record).
if (typeName.indexOf(CswConstants.NAMESPACE_DELIMITER) != -1) {
String prefix = typeName
.substring(0, typeName.indexOf(CswConstants.NAMESPACE_DELIMITER));
String localPart = typeName
.substring(typeName.indexOf(CswConstants.NAMESPACE_DELIMITER) + 1);
QName qname = new QName(
getNamespaceFromType(prefix, localPart, namespacePrefixToUriMappings),
localPart, prefix);
qNames.add(qname);
} else {
QName qname = new QName(
getNamespaceFromType("", typeName, namespacePrefixToUriMappings), typeName);
qNames.add(qname);
}
}
return qNames;
}
/**
* for a single type, or localName, this returns the corresponding namespace
* from the qualified list of namespaces.
*
* @param typePrefix prefix to a typeName
* example csw is the prefix in the typeName csw:Record
* @param type a single type that has already been split
* @param namespacePrefixToUriMappings map of namespace prefixes to namespace uri
* example key=csw value=http://www.opengis.net/cat/csw/2.0.2
* @return corresponding namespace for the given type
*/
private String getNamespaceFromType(String typePrefix, String type,
Map<String, String> namespacePrefixToUriMappings) throws CswException {
if (namespacePrefixToUriMappings == null) {
return "";
}
String namespaceUri = namespacePrefixToUriMappings.get(typePrefix);
if (namespaceUri == null) {
throw createUnknownTypeException(type);
}
return namespaceUri;
}
private DescribeRecordResponseType buildDescribeRecordResponseFromTypes(List<QName> types,
String version) throws CswException {
validateFullyQualifiedTypes(types);
DescribeRecordResponseType response = new DescribeRecordResponseType();
List<SchemaComponentType> schemas = new ArrayList<SchemaComponentType>();
if (types.isEmpty() || types.contains(
new QName(CswConstants.CSW_OUTPUT_SCHEMA, CswConstants.CSW_RECORD_LOCAL_NAME))) {
schemas.add(getSchemaComponentType());
}
response.setSchemaComponent(schemas);
return response;
}
private CswRecordCollection queryCsw(GetRecordsType request) throws CswException {
if (LOGGER.isDebugEnabled()) {
try {
Writer writer = new StringWriter();
try {
Marshaller marshaller = getJaxBContext().createMarshaller();
marshaller.setProperty(Marshaller.JAXB_FORMATTED_OUTPUT, Boolean.TRUE);
JAXBElement<GetRecordsType> jaxbElement = new ObjectFactory()
.createGetRecords(request);
marshaller.marshal(jaxbElement, writer);
} catch (JAXBException e) {
LOGGER.debug("Unable to marshall {} to XML. Exception {}",
GetRecordsType.class, e);
}
LOGGER.debug(writer.toString());
} catch (Exception e) {
LOGGER.debug("Unable to create debug message for getRecordsType: {}", e);
}
}
QueryType query = (QueryType) request.getAbstractQuery().getValue();
CswRecordCollection response = new CswRecordCollection();
response.setRequest(request);
response.setOutputSchema(request.getOutputSchema());
response.setMimeType(request.getOutputFormat());
response.setElementName(query.getElementName());
response.setElementSetType(
(query.getElementSetName() != null) ? query.getElementSetName().getValue() : null);
response.setResultType(
(ResultType) ObjectUtils.defaultIfNull(request.getResultType(), ResultType.HITS));
if (ResultType.HITS.equals(request.getResultType()) || ResultType.RESULTS
.equals(request.getResultType())) {
CswRecordMapperFilterVisitor filterVisitor = buildFilter(query);
QueryImpl frameworkQuery = new QueryImpl(filterVisitor.getVisitedFilter());
frameworkQuery.setSortBy(buildSort(query.getSortBy()));
if (ResultType.HITS.equals(request.getResultType())
|| request.getMaxRecords().intValue() < 1) {
frameworkQuery.setStartIndex(1);
frameworkQuery.setPageSize(1);
} else {
frameworkQuery.setStartIndex(request.getStartPosition().intValue());
frameworkQuery.setPageSize(request.getMaxRecords().intValue());
}
QueryRequest queryRequest = null;
boolean isDistributed = request.getDistributedSearch() != null && (
request.getDistributedSearch().getHopCount().longValue() > 1);
if (isDistributed && CollectionUtils.isEmpty(filterVisitor.getSourceIds())) {
queryRequest = new QueryRequestImpl(frameworkQuery, true);
} else if (isDistributed && !CollectionUtils.isEmpty(filterVisitor.getSourceIds())) {
queryRequest = new QueryRequestImpl(frameworkQuery, filterVisitor.getSourceIds());
} else {
queryRequest = new QueryRequestImpl(frameworkQuery, false);
}
try {
QueryResponse queryResponse = framework
.query(queryRequest);
response.setSourceResponse(queryResponse);
} catch (UnsupportedQueryException e) {
LOGGER.warn("Unable to query", e);
throw new CswException(e);
} catch (SourceUnavailableException e) {
LOGGER.warn("Unable to query", e);
throw new CswException(e);
} catch (FederationException e) {
LOGGER.warn("Unable to query", e);
throw new CswException(e);
}
}
return response;
}
private CswRecordMapperFilterVisitor buildFilter(QueryType query) throws CswException {
CswRecordMapperFilterVisitor visitor = new CswRecordMapperFilterVisitor();
Filter filter = null;
if (query.getConstraint() != null) {
if (query.getConstraint().isSetCqlText()) {
try {
filter = CQL.toFilter(query.getConstraint().getCqlText());
} catch (CQLException e) {
throw new CswException("Unable to parse CQL Constraint: " + e.getMessage(), e);
}
} else if (query.getConstraint().isSetFilter()) {
FilterType constraintFilter = query.getConstraint().getFilter();
filter = parseFilter(constraintFilter);
}
} else {
// not supported by catalog:
//filter = Filter.INCLUDE;
filter = builder.attribute(Metacard.ID).is().like().text(FilterDelegate.WILDCARD_CHAR);
}
if (filter == null) {
throw new CswException("Invalid Filter Expression", CswConstants.NO_APPLICABLE_CODE,
null);
}
visitor.setVisitedFilter(filter);
if (query.getTypeNames().contains(new QName(CswConstants.CSW_OUTPUT_SCHEMA,
CswConstants.CSW_RECORD_LOCAL_NAME, CswConstants.CSW_NAMESPACE_PREFIX))) {
try {
visitor.setVisitedFilter((Filter) filter.accept(visitor, null));
} catch(UnsupportedOperationException ose) {
throw new CswException(ose.getMessage(), CswConstants.INVALID_PARAMETER_VALUE, null);
}
}
return visitor;
}
private SortBy buildSort(SortByType sort) throws CswException {
if (sort == null || sort.getSortProperty() == null) {
return null;
}
SortBy[] sortByArr = parseSortBy(sort);
if (sortByArr.length > 1) {
LOGGER.warn("Query request has multiple sort criteria, only primary will be used");
}
SortBy sortBy = sortByArr[0];
if (sortBy.getPropertyName() == null) {
LOGGER.warn("No property name in primary sort criteria");
return null;
}
if (!DefaultCswRecordMap.getDefaultCswRecordMap().hasDefaultMetacardFieldForPrefixedString(
sortBy.getPropertyName().getPropertyName(),
sortBy.getPropertyName().getNamespaceContext())) {
throw new CswException("Property " + sortBy.getPropertyName().getPropertyName()
+ " is not a valid SortBy Field", CswConstants.INVALID_PARAMETER_VALUE,
"SortProperty");
}
String name = DefaultCswRecordMap.getDefaultCswRecordMap()
.getDefaultMetacardFieldForPrefixedString(
sortBy.getPropertyName().getPropertyName(),
sortBy.getPropertyName().getNamespaceContext());
PropertyName propName = new AttributeExpressionImpl(new NameImpl(name));
return new SortByImpl(propName, sortBy.getSortOrder());
}
private SchemaComponentType getSchemaComponentType() throws CswException {
SchemaComponentType schemaComponentType = new SchemaComponentType();
List<Object> listOfObject = new ArrayList<Object>();
listOfObject.add(getDocElementFromResourcePath("csw/2.0.2/record.xsd"));
schemaComponentType.setContent(listOfObject);
schemaComponentType.setSchemaLanguage(CswConstants.XML_SCHEMA_LANGUAGE);
schemaComponentType.setTargetNamespace(CswConstants.CSW_OUTPUT_SCHEMA);
return schemaComponentType;
}
private Element getDocElementFromResourcePath(String resourcePath) throws CswException {
Element element = documentElements.get(resourcePath);
if (element == null) {
element = loadDocElementFromResourcePath(resourcePath);
documentElements.put(resourcePath, element);
}
return element;
}
private Element loadDocElementFromResourcePath(String resourcePath) throws CswException {
URL recordUrl = context.getBundle().getResource(resourcePath);
DocumentBuilderFactory docBuilderFactory = DocumentBuilderFactory.newInstance();
docBuilderFactory.setNamespaceAware(true);
Document doc;
try {
DocumentBuilder docBuilder = docBuilderFactory.newDocumentBuilder();
doc = docBuilder.parse(recordUrl.openStream());
} catch (ParserConfigurationException e) {
throw new CswException(e);
} catch (SAXException e) {
throw new CswException(e);
} catch (IOException e) {
throw new CswException(e);
}
if (doc == null) {
throw new CswException(
"Document was NULL in attempting to parse from resource path '" + resourcePath
+ "'");
}
return doc.getDocumentElement();
}
/**
* Creates a CapabilitiesType object with only specified sections to be returned as a
* GetCapabilities response.
*
* @param sections
* The list of desired sections for the GetCapabilities response
*
* @return The constructed CapabilitiesType object, containing only the user-specified sections
*/
private CapabilitiesType buildCapabilitiesType(List<String> sections) {
// If no sections are specified, return them all
if (sections == null || sections.size() == 0) {
return capabilitiesType;
}
CapabilitiesType cswCapabilities = new CapabilitiesType();
cswCapabilities.setVersion(capabilitiesType.getVersion());
// Grab the desired sections from the global capabilitiesType variable
for (String section : sections) {
if (section.equalsIgnoreCase(SERVICE_IDENTIFICATION)) {
cswCapabilities
.setServiceIdentification(capabilitiesType.getServiceIdentification());
} else if (section.equalsIgnoreCase(SERVICE_PROVIDER)) {
cswCapabilities.setServiceProvider(capabilitiesType.getServiceProvider());
} else if (section.equalsIgnoreCase(OPERATIONS_METADATA)) {
cswCapabilities.setOperationsMetadata(capabilitiesType.getOperationsMetadata());
}
}
// filterCapabilities is required. Add it even if it isn't in the sections list.
cswCapabilities.setFilterCapabilities(capabilitiesType.getFilterCapabilities());
return cswCapabilities;
}
/**
* Creates a full CapabilitiesType element to be returned as the GetCapabilities response
*
* @return The constructed CapabilitiesType object
*/
private CapabilitiesType buildCapabilitiesType() {
if (capabilitiesType == null) {
CapabilitiesType cswCapabilities = new CapabilitiesType();
cswCapabilities.setVersion(CswConstants.VERSION_2_0_2);
cswCapabilities.setServiceIdentification(buildServiceIdentification());
cswCapabilities.setServiceProvider(buildServiceProvider());
cswCapabilities.setOperationsMetadata(buildOperationsMetadata());
cswCapabilities.setFilterCapabilities(buildFilterCapabilities());
return cswCapabilities;
} else {
capabilitiesType.setOperationsMetadata(buildOperationsMetadata());
return capabilitiesType;
}
}
/**
* Creates the Filter_Capabilities section of the GetCapabilities response. These are defined
* statically by the DDF FilterAdapter implementation TODO: If the implementation changes,
* update this method to reflect the changes.
*
* @return The constructed FilterCapabilities object
*/
private FilterCapabilities buildFilterCapabilities() {
// Create the FilterCapabilites - These are defined statically by the
// DDF FilterAdapter implementation
FilterCapabilities filterCapabilities = new FilterCapabilities();
ScalarCapabilitiesType scalarCapabilities = new ScalarCapabilitiesType();
ComparisonOperatorsType cot = new ComparisonOperatorsType();
cot.setComparisonOperator(COMPARISON_OPERATORS);
scalarCapabilities.setLogicalOperators(new LogicalOperators());
scalarCapabilities.setComparisonOperators(cot);
filterCapabilities.setScalarCapabilities(scalarCapabilities);
SpatialOperatorsType spatialOpsType = new SpatialOperatorsType();
ArrayList<SpatialOperatorType> spatialOpTypes = new ArrayList<SpatialOperatorType>();
for (SpatialOperatorNameType sont : SPATIAL_OPERATORS) {
SpatialOperatorType sot = new SpatialOperatorType();
sot.setName(sont);
spatialOpTypes.add(sot);
}
GeometryOperandsType geometryOperands = new GeometryOperandsType();
List<QName> geoOperandsList = geometryOperands.getGeometryOperand();
geoOperandsList.add(new QName(CswConstants.GML_SCHEMA, CswConstants.GML_POINT,
CswConstants.GML_NAMESPACE_PREFIX));
geoOperandsList.add(new QName(CswConstants.GML_SCHEMA, CswConstants.GML_LINESTRING,
CswConstants.GML_NAMESPACE_PREFIX));
geoOperandsList.add(new QName(CswConstants.GML_SCHEMA, CswConstants.GML_POLYGON,
CswConstants.GML_NAMESPACE_PREFIX));
spatialOpsType.setSpatialOperator(spatialOpTypes);
SpatialCapabilitiesType spatialCaps = new SpatialCapabilitiesType();
spatialCaps.setSpatialOperators(spatialOpsType);
spatialCaps.setGeometryOperands(geometryOperands);
filterCapabilities.setSpatialCapabilities(spatialCaps);
IdCapabilitiesType idCapabilities = new IdCapabilitiesType();
idCapabilities.getEIDOrFID().add(new EID());
filterCapabilities.setIdCapabilities(idCapabilities);
return filterCapabilities;
}
/**
* Creates the OperationsMetadata portion of the GetCapabilities response TODO: As these
* operations are implemented or added, update their descriptions to ensure they match up with
* the functionality
*
* @return The constructed OperationsMetadata object
*/
private OperationsMetadata buildOperationsMetadata() {
OperationsMetadata om = new OperationsMetadata();
List<QName> getAndPost = Arrays.asList(CswConstants.GET, CswConstants.POST);
// Builds GetCapabilities operation metadata
Operation getCapabilitiesOp = buildOperation(CswConstants.GET_CAPABILITIES, getAndPost);
addOperationParameter("sections", GET_CAPABILITIES_PARAMS, getCapabilitiesOp);
// Builds DescribeRecord operation metadata
Operation describeRecordOp = buildOperation(CswConstants.DESCRIBE_RECORD, getAndPost);
addOperationParameter(CswConstants.TYPE_NAME_PARAMETER,
Arrays.asList(CswConstants.CSW_RECORD), describeRecordOp);
Set<String> mimeTypeSet = new HashSet<>();
mimeTypeSet.add(DEFAULT_OUTPUT_FORMAT);
mimeTypeSet.addAll(mimeTypeTransformerManager.getAvailableMimeTypes());
List<String> mimeTypes = new ArrayList<>(mimeTypeSet);
addOperationParameter(CswConstants.OUTPUT_FORMAT_PARAMETER, mimeTypes, describeRecordOp);
addOperationParameter("schemaLanguage", CswConstants.VALID_SCHEMA_LANGUAGES,
describeRecordOp);
// Builds GetRecords operation metadata
Operation getRecordsOp = buildOperation(CswConstants.GET_RECORDS, getAndPost);
addOperationParameter(CswConstants.RESULT_TYPE_PARAMETER,
Arrays.asList("hits", "results", "validate"), getRecordsOp);
addOperationParameter(CswConstants.OUTPUT_FORMAT_PARAMETER, mimeTypes, getRecordsOp);
addOperationParameter(CswConstants.OUTPUT_SCHEMA_PARAMETER,
schemaTransformerManager.getAvailableSchemas(), getRecordsOp);
addOperationParameter(CswConstants.TYPE_NAMES_PARAMETER,
Arrays.asList(CswConstants.CSW_RECORD), getRecordsOp);
addOperationParameter(CswConstants.CONSTRAINT_LANGUAGE_PARAMETER, Arrays.asList(
CswConstants.CONSTRAINT_LANGUAGE_FILTER,
CswConstants.CONSTRAINT_LANGUAGE_CQL),
getRecordsOp);
addFederatedCatalogs(getRecordsOp);
// Builds GetRecordById operation metadata
Operation getRecordByIdOp = buildOperation(CswConstants.GET_RECORD_BY_ID, getAndPost);
addOperationParameter(CswConstants.OUTPUT_SCHEMA_PARAMETER,
schemaTransformerManager.getAvailableSchemas(), getRecordByIdOp);
addOperationParameter(CswConstants.OUTPUT_FORMAT_PARAMETER, mimeTypes, getRecordByIdOp);
addOperationParameter(CswConstants.RESULT_TYPE_PARAMETER,
Arrays.asList("hits", "results", "validate"), getRecordByIdOp);
addOperationParameter(CswConstants.ELEMENT_SET_NAME_PARAMETER,
Arrays.asList("brief", "summary", "full"), getRecordByIdOp);
List<Operation> ops = Arrays
.asList(getCapabilitiesOp, describeRecordOp, getRecordsOp, getRecordByIdOp);
om.setOperation(ops);
om.getParameter().add(createDomainType(CswConstants.SERVICE, CswConstants.CSW));
om.getParameter().add(createDomainType(CswConstants.VERSION, CswConstants.VERSION_2_0_2));
return om;
}
/**
* Creates the ServiceIdentification portion of the GetCapabilities response TODO: Add more
* DDF-specific information if desired (Fees, Keywords, and AccessConstraints are all currently
* empty, and the abstract is very basic)
*
* @return The constructed ServiceIdentification object
*/
private ServiceIdentification buildServiceIdentification() {
ServiceIdentification si = new ServiceIdentification();
si.setTitle(SERVICE_TITLE);
si.setServiceTypeVersion(SERVICE_TYPE_VERSION);
CodeType type = new CodeType();
type.setValue(CswConstants.CSW);
si.setServiceType(type);
si.setAbstract(SERVICE_ABSTRACT);
return si;
}
/**
* Creates the ServiceProvider portion of the GetCapabilities response TODO: Add more
* DDF-specific information if desired
*
* @return The constructed ServiceProvider object
*/
private ServiceProvider buildServiceProvider() {
ServiceProvider sp = new ServiceProvider();
sp.setProviderName(PROVIDER_NAME);
sp.setProviderSite(new OnlineResourceType());
sp.setServiceContact(new ResponsiblePartySubsetType());
return sp;
}
/**
* Creates an Operation object for the OperationsMetadata section TODO: We currently don't use
* the constraint or metadata elements, those can be added in as desired
*
* @param name
* The name of the operation
* @param types
* The request types supported (GET/POST)
* @return The constructed Operation object
*/
private Operation buildOperation(String name, List<QName> types) {
Operation op = new Operation();
op.setName(name);
ArrayList<DCP> dcpList = new ArrayList<DCP>();
DCP dcp = new DCP();
HTTP http = new HTTP();
for (QName type : types) {
RequestMethodType rmt = new RequestMethodType();
rmt.setHref(uri.getBaseUri().toASCIIString());
JAXBElement<RequestMethodType> requestElement = new JAXBElement<RequestMethodType>(type,
RequestMethodType.class, rmt);
if (type.equals(CswConstants.POST)) {
requestElement.getValue().getConstraint()
.add(createDomainType(CswConstants.POST_ENCODING, CswConstants.XML));
}
http.getGetOrPost().add(requestElement);
}
dcp.setHTTP(http);
dcpList.add(dcp);
op.setDCP(dcpList);
return op;
}
private void addOperationParameter(String name, List<String> params, Operation op) {
DomainType dt = createDomainType(name, params);
op.getParameter().add(dt);
}
private DomainType createDomainType(String name, String value) {
return createDomainType(name, Collections.singletonList(value));
}
private DomainType createDomainType(String name, List<String> params) {
DomainType dt = new DomainType();
dt.setName(name);
dt.setValue(params);
return dt;
}
private void addFederatedCatalogs(Operation operation) {
List<String> sourceIds = new ArrayList<>(framework.getSourceIds());
sourceIds.remove(framework.getId());
operation.getConstraint().add(createDomainType(CswConstants.FEDERATED_CATALOGS, sourceIds));
}
/**
* Verifies that that if types are passed, then they are fully qualified
*
* @param types
* List of QNames representing types
*/
private void validateFullyQualifiedTypes(List<QName> types) throws CswException {
for (QName type : types) {
if (StringUtils.isBlank(type.getNamespaceURI())) {
throw new CswException("Unqualified type name: '" + type.getLocalPart() + "'",
CswConstants.INVALID_PARAMETER_VALUE, null);
}
}
}
/**
* Verifies that if types are passed, then they exist.
*
* @param types
* List of QNames representing types
* @param version the specified version of the types
*/
private void validateTypes(List<QName> types, String version) throws CswException {
if (types == null || types.size() == 0) {
// No type at all is valid, just return
return;
}
if (types.size() == 1) {
if (!types.get(0).equals(new QName(CswConstants.CSW_OUTPUT_SCHEMA,
CswConstants.CSW_RECORD_LOCAL_NAME))) {
throw createUnknownTypeException(types.get(0).toString());
}
}
}
private void validateOutputSchema(String schema) throws CswException {
if (schema == null || schemaTransformerManager.getTransformerBySchema(schema) != null) {
return;
}
throw createUnknownSchemaException(schema);
}
private void validateVersion(String versions) throws CswException {
if (!versions.contains(CswConstants.VERSION_2_0_2)) {
throw new CswException(
"Version(s) " + versions + " is not supported, we currently support version "
+ CswConstants.VERSION_2_0_2, CswConstants.VERSION_NEGOTIATION_FAILED,
null);
}
}
private void validateOutputFormat(String format) throws CswException {
if (!StringUtils.isEmpty(format)) {
if (!DEFAULT_OUTPUT_FORMAT.equals(format) && !mimeTypeTransformerManager
.getAvailableMimeTypes().contains(format)) {
throw new CswException("Invalid output format '" + format + "'",
CswConstants.INVALID_PARAMETER_VALUE, "outputformat");
}
}
}
private void validateSchemaLanguage(String schemaLanguage) throws CswException {
if (!StringUtils.isEmpty(schemaLanguage)) {
if (!CswConstants.VALID_SCHEMA_LANGUAGES.contains(schemaLanguage)) {
throw new CswException("Invalid schema language '" + schemaLanguage + "'",
CswConstants.INVALID_PARAMETER_VALUE, "schemaLanguage");
}
}
}
private void notImplemented(final String methodName) throws CswException {
throw new CswException("The method " + methodName + " is not yet implemented.");
}
private CswException createUnknownTypeException(final String type) {
return new CswException("The type '" + type + "' is not known to this service.",
CswConstants.INVALID_PARAMETER_VALUE, null);
}
private CswException createUnknownSchemaException(final String schema) {
return new CswException("The schema '" + schema + "' is not known to this service.",
CswConstants.INVALID_PARAMETER_VALUE, "OutputSchema");
}
private InputStream marshalJaxB(JAXBElement<?> filterElement) throws JAXBException {
ByteArrayOutputStream os = new ByteArrayOutputStream();
getJaxBContext().createMarshaller().marshal(filterElement, os);
ByteArrayInputStream input = new ByteArrayInputStream(os.toByteArray());
IOUtils.closeQuietly(os);
return input;
}
private Filter parseFilter(FilterType filterType) throws CswException {
if (!filterType.isSetComparisonOps() && !filterType.isSetId() && !filterType.isSetLogicOps()
&& !filterType.isSetSpatialOps()) {
throw new CswException("Empty Filter provided. Unable to preform query.",
CswConstants.INVALID_PARAMETER_VALUE, "Filter");
}
JAXBElement<FilterType> filterElement = new net.opengis.filter.v_1_1_0.ObjectFactory()
.createFilter(filterType);
return (Filter) parseJaxB(filterElement);
}
private SortBy[] parseSortBy(SortByType sortByType) throws CswException {
JAXBElement<SortByType> sortByElement = new net.opengis.filter.v_1_1_0.ObjectFactory()
.createSortBy(sortByType);
return (SortBy[]) parseJaxB(sortByElement);
}
private Object parseJaxB(JAXBElement<?> element) throws CswException {
Parser parser = new Parser(PARSER_CONFIG);
InputStream inputStream = null;
try {
inputStream = marshalJaxB(element);
return parser.parse(inputStream);
} catch (JAXBException e) {
throw new CswException("Failed to parse Element: (JAXBException): " + e.getMessage(),
CswConstants.INVALID_PARAMETER_VALUE, null);
} catch (IOException e) {
throw new CswException("Failed to parse Element: (IOException): " + e.getMessage(),
CswConstants.INVALID_PARAMETER_VALUE, null);
} catch (SAXException e) {
throw new CswException("Failed to parse Element: (SAXException): " + e.getMessage(),
CswConstants.INVALID_PARAMETER_VALUE, null);
} catch (ParserConfigurationException e) {
throw new CswException(
"Failed to parse Element: (ParserConfigurationException): " + e.getMessage(),
CswConstants.INVALID_PARAMETER_VALUE, null);
} catch (RuntimeException e) {
throw new CswException("Failed to parse Element: (RuntimeException): " + e.getMessage(),
CswConstants.INVALID_PARAMETER_VALUE, null);
}
}
private CswRecordCollection queryById(List<String> ids) throws CswException {
List<Filter> filters = new ArrayList<Filter>();
for (String id : ids) {
filters.add(builder.attribute(Metacard.ID).is().equalTo().text(id));
}
Filter anyOfFilter = builder.anyOf(filters);
QueryRequest queryRequest = new QueryRequestImpl(new QueryImpl(anyOfFilter), false);
try {
CswRecordCollection response = new CswRecordCollection();
response.setById(true);
QueryResponse queryResponse = framework.query(queryRequest);
List<Metacard> metacards = new LinkedList<Metacard>();
for (Result result : queryResponse.getResults()) {
metacards.add(result.getMetacard());
}
response.setCswRecords(metacards);
return response;
} catch (UnsupportedQueryException e) {
throw new CswException(e);
} catch (SourceUnavailableException e) {
throw new CswException(e);
} catch (FederationException e) {
throw new CswException(e);
}
}
}