/** * Copyright (c) Codice Foundation * <p> * 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. * <p> * 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.common.source; import java.io.BufferedInputStream; import java.io.IOException; import java.io.InputStream; import java.io.Serializable; import java.io.StringWriter; import java.io.Writer; import java.math.BigInteger; import java.net.ConnectException; import java.net.URI; import java.util.ArrayList; import java.util.Arrays; import java.util.EnumSet; import java.util.HashMap; import java.util.HashSet; import java.util.List; import java.util.Map; import java.util.Properties; import java.util.Set; import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.Executors; import java.util.concurrent.ScheduledExecutorService; import java.util.concurrent.ScheduledFuture; import java.util.concurrent.TimeUnit; import java.util.function.Consumer; import java.util.stream.Collectors; import javax.net.ssl.SSLHandshakeException; import javax.ws.rs.WebApplicationException; import javax.ws.rs.core.MediaType; import javax.ws.rs.core.Response; 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 org.apache.commons.io.FilenameUtils; import org.apache.commons.lang.StringUtils; import org.apache.cxf.common.util.CollectionUtils; import org.codice.ddf.configuration.SystemBaseUrl; import org.codice.ddf.cxf.SecureCxfClientFactory; import org.codice.ddf.security.common.Security; import org.codice.ddf.spatial.ogc.catalog.MetadataTransformer; import org.codice.ddf.spatial.ogc.catalog.common.AvailabilityCommand; import org.codice.ddf.spatial.ogc.catalog.common.AvailabilityTask; import org.codice.ddf.spatial.ogc.csw.catalog.common.Csw; import org.codice.ddf.spatial.ogc.csw.catalog.common.CswAxisOrder; 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.CswJAXBElementProvider; import org.codice.ddf.spatial.ogc.csw.catalog.common.CswRecordCollection; import org.codice.ddf.spatial.ogc.csw.catalog.common.CswSourceConfiguration; import org.codice.ddf.spatial.ogc.csw.catalog.common.CswSubscribe; 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.source.reader.GetRecordsMessageBodyReader; import org.opengis.filter.Filter; import org.opengis.filter.sort.SortOrder; import org.osgi.framework.BundleContext; import org.osgi.framework.InvalidSyntaxException; import org.osgi.framework.ServiceReference; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import com.thoughtworks.xstream.converters.Converter; import ddf.catalog.Constants; import ddf.catalog.data.ContentType; import ddf.catalog.data.Metacard; import ddf.catalog.data.MetacardType; import ddf.catalog.data.Result; import ddf.catalog.data.impl.AttributeImpl; import ddf.catalog.data.impl.ContentTypeImpl; import ddf.catalog.data.impl.MetacardImpl; import ddf.catalog.data.impl.ResultImpl; import ddf.catalog.data.types.Core; import ddf.catalog.filter.FilterAdapter; import ddf.catalog.filter.FilterBuilder; import ddf.catalog.operation.Query; import ddf.catalog.operation.QueryRequest; import ddf.catalog.operation.ResourceResponse; import ddf.catalog.operation.SourceResponse; import ddf.catalog.operation.impl.QueryImpl; import ddf.catalog.operation.impl.QueryRequestImpl; import ddf.catalog.operation.impl.ResourceResponseImpl; import ddf.catalog.operation.impl.SourceResponseImpl; import ddf.catalog.resource.Resource; import ddf.catalog.resource.ResourceNotFoundException; import ddf.catalog.resource.ResourceNotSupportedException; import ddf.catalog.resource.ResourceReader; import ddf.catalog.resource.impl.ResourceImpl; import ddf.catalog.service.ConfiguredService; import ddf.catalog.source.ConnectedSource; import ddf.catalog.source.FederatedSource; import ddf.catalog.source.SourceMonitor; import ddf.catalog.source.UnsupportedQueryException; import ddf.catalog.transform.CatalogTransformerException; import ddf.catalog.util.impl.MaskableImpl; import ddf.security.SecurityConstants; import ddf.security.Subject; import ddf.security.encryption.EncryptionService; import ddf.security.service.SecurityManager; import net.opengis.cat.csw.v_2_0_2.AcknowledgementType; import net.opengis.cat.csw.v_2_0_2.CapabilitiesType; import net.opengis.cat.csw.v_2_0_2.ElementSetNameType; 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.GetRecordsResponseType; 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.QueryConstraintType; import net.opengis.cat.csw.v_2_0_2.QueryType; import net.opengis.cat.csw.v_2_0_2.ResultType; 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.PropertyNameType; import net.opengis.filter.v_1_1_0.SortByType; import net.opengis.filter.v_1_1_0.SortOrderType; import net.opengis.filter.v_1_1_0.SortPropertyType; 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.DomainType; import net.opengis.ows.v_1_0_0.Operation; import net.opengis.ows.v_1_0_0.OperationsMetadata; /** * AbstractCswSource provides a DDF {@link FederatedSource} and {@link ConnectedSource} for CSW 2.0.2 * services. */ public abstract class AbstractCswSource extends MaskableImpl implements FederatedSource, ConnectedSource, ConfiguredService { protected static final String CSW_SERVER_ERROR = "Error received from CSW server."; protected static final String CSWURL_PROPERTY = "cswUrl"; protected static final String ID_PROPERTY = "id"; protected static final String USERNAME_PROPERTY = "username"; protected static final String PASSWORD_PROPERTY = "password"; protected static final String METACARD_MAPPINGS_PROPERTY = "metacardMappings"; protected static final String COORDINATE_ORDER_PROPERTY = "coordinateOrder"; protected static final String POLL_INTERVAL_PROPERTY = "pollInterval"; protected static final String OUTPUT_SCHEMA_PROPERTY = "outputSchema"; protected static final String IS_CQL_FORCED_PROPERTY = "isCqlForced"; protected static final String FORCE_SPATIAL_FILTER_PROPERTY = "forceSpatialFilter"; protected static final String NO_FORCE_SPATIAL_FILTER = "NO_FILTER"; protected static final String CONNECTION_TIMEOUT_PROPERTY = "connectionTimeout"; protected static final String RECEIVE_TIMEOUT_PROPERTY = "receiveTimeout"; protected static final String QUERY_TYPE_NAME_PROPERTY = "queryTypeName"; protected static final String QUERY_TYPE_NAMESPACE_PROPERTY = "queryTypeNamespace"; protected static final String USE_POS_LIST_PROPERTY = "usePosList"; protected static final String SECURITY_ATTRIBUTES_PROPERTY = "securityAttributeStrings"; protected static final String EVENT_SERVICE_ADDRESS = "eventServiceAddress"; protected static final String REGISTER_FOR_EVENTS = "registerForEvents"; private static final Logger LOGGER = LoggerFactory.getLogger(AbstractCswSource.class); private static final String DEFAULT_CSW_TRANSFORMER_ID = "csw"; private static final String DESCRIBABLE_PROPERTIES_FILE = "/describable.properties"; private static final String DESCRIPTION = "description"; private static final String ORGANIZATION = "organization"; private static final String VERSION = "version"; private static final String TITLE = "name"; private static final int CONTENT_TYPE_SAMPLE_SIZE = 50; public static final String DISABLE_CN_CHECK_PROPERTY = "disableCnCheck"; private static final JAXBContext JAXB_CONTEXT = initJaxbContext(); private static final String BYTES_SKIPPED = "bytes-skipped"; private static Properties describableProperties = new Properties(); private static Map<String, Consumer<Object>> consumerMap = new HashMap<>(); private static final String OCTET_STREAM_OUTPUT_SCHEMA = "http://www.iana.org/assignments/media-types/application/octet-stream"; private static final String ERROR_ID_PRODUCT_RETRIEVAL = "Error retrieving resource for ID: %s"; private static final Security SECURITY = Security.getInstance(); private EncryptionService encryptionService; protected String configurationPid; static { try (InputStream properties = AbstractCswSource.class.getResourceAsStream( DESCRIBABLE_PROPERTIES_FILE)) { describableProperties.load(properties); } catch (IOException e) { LOGGER.info("Failed to load properties", e); } } protected CswSourceConfiguration cswSourceConfiguration; protected CswFilterDelegate cswFilterDelegate; protected Converter cswTransformConverter; protected String forceSpatialFilter = NO_FORCE_SPATIAL_FILTER; protected ScheduledFuture<?> availabilityPollFuture; protected SecurityManager securityManager; protected FilterBuilder filterBuilder; protected FilterAdapter filterAdapter; private Set<SourceMonitor> sourceMonitors = new HashSet<>(); private Map<String, ContentType> contentTypes = new ConcurrentHashMap<>(); private ResourceReader resourceReader; private DomainType supportedOutputSchemas; private Set<ElementSetType> detailLevels; private BundleContext context; private String description = null; protected CapabilitiesType capabilities; private String cswVersion; private SpatialCapabilitiesType spatialCapabilities; private ScheduledExecutorService scheduler; private AvailabilityTask availabilityTask; private boolean isConstraintCql; protected SecureCxfClientFactory<Csw> factory; protected SecureCxfClientFactory<CswSubscribe> subscribeClientFactory; protected CswJAXBElementProvider<GetRecordsType> getRecordsTypeProvider; protected List<String> jaxbElementClassNames = new ArrayList<>(); protected Map<String, String> jaxbElementClassMap = new HashMap<>(); protected String filterlessSubscriptionId = null; private List<MetacardType> metacardTypes; /** * Instantiates a CswSource. This constructor is for unit tests * * @param context The {@link BundleContext} from the OSGi Framework * @param cswSourceConfiguration the configuration of this source * @param provider transform provider to transform results * @param factory client factory already configured for this source */ public AbstractCswSource(BundleContext context, CswSourceConfiguration cswSourceConfiguration, Converter provider, SecureCxfClientFactory factory, EncryptionService encryptionService) { this.encryptionService = encryptionService; this.context = context; this.cswSourceConfiguration = cswSourceConfiguration; this.cswTransformConverter = provider; scheduler = Executors.newSingleThreadScheduledExecutor(); this.factory = factory; setConsumerMap(); } @Deprecated public AbstractCswSource(BundleContext context, CswSourceConfiguration cswSourceConfiguration, Converter provider, SecureCxfClientFactory factory) { this(context, cswSourceConfiguration, provider, factory, null); } /** * Instantiates a CswSource. */ public AbstractCswSource(EncryptionService encryptionService) { this.encryptionService = encryptionService; cswSourceConfiguration = new CswSourceConfiguration(encryptionService); scheduler = Executors.newSingleThreadScheduledExecutor(); } @Deprecated public AbstractCswSource() { this(null); } private static JAXBContext initJaxbContext() { JAXBContext jaxbContext = null; String contextPath = StringUtils.join(new String[] {CswConstants.OGC_CSW_PACKAGE, CswConstants.OGC_FILTER_PACKAGE, CswConstants.OGC_GML_PACKAGE, CswConstants.OGC_OWS_PACKAGE}, ":"); try { jaxbContext = JAXBContext.newInstance(contextPath, AbstractCswSource.class.getClassLoader()); } catch (JAXBException e) { LOGGER.info("Failed to initialize JAXBContext", e); } return jaxbContext; } /** * Initializes the CswSource by connecting to the Server */ public void init() { setConsumerMap(); LOGGER.debug("{}: Entering init()", cswSourceConfiguration.getId()); initClientFactory(); setupAvailabilityPoll(); configureEventService(); } private void initClientFactory() { if (StringUtils.isNotBlank(cswSourceConfiguration.getUsername()) && StringUtils.isNotBlank( cswSourceConfiguration.getPassword())) { factory = new SecureCxfClientFactory(cswSourceConfiguration.getCswUrl(), Csw.class, initProviders(cswTransformConverter, cswSourceConfiguration), null, cswSourceConfiguration.getDisableCnCheck(), false, cswSourceConfiguration.getConnectionTimeout(), cswSourceConfiguration.getReceiveTimeout(), cswSourceConfiguration.getUsername(), cswSourceConfiguration.getPassword()); } else { factory = new SecureCxfClientFactory(cswSourceConfiguration.getCswUrl(), Csw.class, initProviders(cswTransformConverter, cswSourceConfiguration), null, cswSourceConfiguration.getDisableCnCheck(), false, cswSourceConfiguration.getConnectionTimeout(), cswSourceConfiguration.getReceiveTimeout()); } } protected void initSubscribeClientFactory() { if (StringUtils.isNotBlank(cswSourceConfiguration.getUsername()) && StringUtils.isNotBlank( cswSourceConfiguration.getPassword())) { subscribeClientFactory = new SecureCxfClientFactory(cswSourceConfiguration.getEventServiceAddress(), CswSubscribe.class, initProviders(cswTransformConverter, cswSourceConfiguration), null, cswSourceConfiguration.getDisableCnCheck(), false, cswSourceConfiguration.getConnectionTimeout(), cswSourceConfiguration.getReceiveTimeout(), cswSourceConfiguration.getUsername(), cswSourceConfiguration.getPassword()); } else { subscribeClientFactory = new SecureCxfClientFactory(cswSourceConfiguration.getEventServiceAddress(), CswSubscribe.class, initProviders(cswTransformConverter, cswSourceConfiguration), null, cswSourceConfiguration.getDisableCnCheck(), false, cswSourceConfiguration.getConnectionTimeout(), cswSourceConfiguration.getReceiveTimeout()); } } /** * Sets the consumerMap to manipulate cswSourceConfiguration when refresh is called. */ private void setConsumerMap() { consumerMap.put(ID_PROPERTY, value -> setId((String) value)); consumerMap.put(PASSWORD_PROPERTY, value -> cswSourceConfiguration.setPassword((String) value)); consumerMap.put(USERNAME_PROPERTY, value -> cswSourceConfiguration.setUsername((String) value)); consumerMap.put(CONNECTION_TIMEOUT_PROPERTY, value -> cswSourceConfiguration.setConnectionTimeout((Integer) value)); consumerMap.put(RECEIVE_TIMEOUT_PROPERTY, value -> cswSourceConfiguration.setReceiveTimeout((Integer) value)); consumerMap.put(OUTPUT_SCHEMA_PROPERTY, value -> setConsumerOutputSchemaProperty((String) value)); consumerMap.put(QUERY_TYPE_NAME_PROPERTY, value -> cswSourceConfiguration.setQueryTypeName((String) value)); consumerMap.put(QUERY_TYPE_NAMESPACE_PROPERTY, value -> cswSourceConfiguration.setQueryTypeNamespace((String) value)); consumerMap.put(METACARD_MAPPINGS_PROPERTY, value -> setMetacardMappings((String[]) value)); consumerMap.put(DISABLE_CN_CHECK_PROPERTY, value -> cswSourceConfiguration.setDisableCnCheck((Boolean) value)); consumerMap.put(COORDINATE_ORDER_PROPERTY, value -> setCoordinateOrder((String) value)); consumerMap.put(USE_POS_LIST_PROPERTY, value -> cswSourceConfiguration.setUsePosList((Boolean) value)); consumerMap.put(POLL_INTERVAL_PROPERTY, value -> setConsumerPollInterval((Integer) value)); consumerMap.put(CSWURL_PROPERTY, value -> setConsumerUrlProp((String) value)); consumerMap.put(IS_CQL_FORCED_PROPERTY, value -> cswSourceConfiguration.setIsCqlForced((Boolean) value)); consumerMap.put(SECURITY_ATTRIBUTES_PROPERTY, value -> cswSourceConfiguration.setSecurityAttributes((String[]) value)); consumerMap.put(REGISTER_FOR_EVENTS, value -> cswSourceConfiguration.setRegisterForEvents((Boolean) value)); consumerMap.put(EVENT_SERVICE_ADDRESS, value -> cswSourceConfiguration.setEventServiceAddress((String) value)); consumerMap.putAll(getAdditionalConsumers()); } protected abstract Map<String, Consumer<Object>> getAdditionalConsumers(); /** * Consumer function that sets the cswSourceConfiguration OutputSchema if it is changed. */ private void setConsumerOutputSchemaProperty(String newSchemaProp) { String oldOutputSchema = cswSourceConfiguration.getOutputSchema(); cswSourceConfiguration.setOutputSchema(newSchemaProp); LOGGER.debug("{}: new output schema: {}", cswSourceConfiguration.getId(), cswSourceConfiguration.getOutputSchema()); LOGGER.debug("{}: old output schema: {}", cswSourceConfiguration.getId(), oldOutputSchema); } /** * Consumer function that sets the cswSourceConfiguration ContentType if it is changed. */ private void setConsumerContentTypeMapping(String newContentTypeMapping) { String previousContentTypeMapping = cswSourceConfiguration.getMetacardMapping(Metacard.CONTENT_TYPE); LOGGER.debug("{}: Previous content type mapping: {}.", cswSourceConfiguration.getId(), previousContentTypeMapping); newContentTypeMapping = newContentTypeMapping.trim(); if (!newContentTypeMapping.equals(previousContentTypeMapping)) { LOGGER.debug("{}: The content type has been updated from {} to {}.", cswSourceConfiguration.getId(), previousContentTypeMapping, newContentTypeMapping); contentTypes.clear(); } cswSourceConfiguration.putMetacardCswMapping(Metacard.CONTENT_TYPE, newContentTypeMapping); LOGGER.debug("{}: Current content type mapping: {}.", cswSourceConfiguration.getId(), newContentTypeMapping); } /** * Consumer function that sets the cswSourceConfiguration PollInterval if it is changed. */ private void setConsumerPollInterval(Integer newPollInterval) { if (!newPollInterval.equals(cswSourceConfiguration.getPollIntervalMinutes())) { LOGGER.debug("Poll Interval was changed for source {}.", cswSourceConfiguration.getId()); cswSourceConfiguration.setPollIntervalMinutes(newPollInterval); forcePoll(); } } /** * Consumer function that sets the cswSourceConfiguration CswUrl if it is changed. */ private void setConsumerUrlProp(String newCswUrlProp) { if (!newCswUrlProp.equals(cswSourceConfiguration.getCswUrl())) { cswSourceConfiguration.setCswUrl(newCswUrlProp); LOGGER.debug("Setting url : {}.", newCswUrlProp); } } private void forcePoll() { if (availabilityPollFuture != null) { availabilityPollFuture.cancel(true); } setupAvailabilityPoll(); } /** * Clean-up when shutting down the CswSource */ public void destroy(int code) { LOGGER.debug("{}: Entering destroy()", cswSourceConfiguration.getId()); availabilityPollFuture.cancel(true); scheduler.shutdownNow(); removeEventServiceSubscription(); } protected List<? extends Object> initProviders(Converter cswTransformProvider, CswSourceConfiguration cswSourceConfiguration) { getRecordsTypeProvider = new CswJAXBElementProvider<>(); getRecordsTypeProvider.setMarshallAsJaxbElement(true); // Adding class names that need to be marshalled/unmarshalled to // jaxbElementClassNames list jaxbElementClassNames.add(GetRecordsType.class.getName()); jaxbElementClassNames.add(CapabilitiesType.class.getName()); jaxbElementClassNames.add(GetCapabilitiesType.class.getName()); jaxbElementClassNames.add(GetRecordsResponseType.class.getName()); jaxbElementClassNames.add(AcknowledgementType.class.getName()); getRecordsTypeProvider.setJaxbElementClassNames(jaxbElementClassNames); // Adding map entry of <Class Name>,<Qualified Name> to jaxbElementClassMap String expandedName = new QName(CswConstants.CSW_OUTPUT_SCHEMA, CswConstants.GET_RECORDS).toString(); LOGGER.debug("{} expanded name: {}", CswConstants.GET_RECORDS, expandedName); jaxbElementClassMap.put(GetRecordsType.class.getName(), expandedName); String getCapsExpandedName = new QName(CswConstants.CSW_OUTPUT_SCHEMA, CswConstants.GET_CAPABILITIES).toString(); LOGGER.debug("{} expanded name: {}", CswConstants.GET_CAPABILITIES, expandedName); jaxbElementClassMap.put(GetCapabilitiesType.class.getName(), getCapsExpandedName); String capsExpandedName = new QName(CswConstants.CSW_OUTPUT_SCHEMA, CswConstants.CAPABILITIES).toString(); LOGGER.debug("{} expanded name: {}", CswConstants.CAPABILITIES, capsExpandedName); jaxbElementClassMap.put(CapabilitiesType.class.getName(), capsExpandedName); String caps201ExpandedName = new QName("http://www.opengis.net/cat/csw", CswConstants.CAPABILITIES).toString(); LOGGER.debug("{} expanded name: {}", CswConstants.CAPABILITIES, caps201ExpandedName); jaxbElementClassMap.put(CapabilitiesType.class.getName(), caps201ExpandedName); String acknowledgmentName = new QName("http://www.opengis.net/cat/csw/2.0.2", "Acknowledgement").toString(); jaxbElementClassMap.put(AcknowledgementType.class.getName(), acknowledgmentName); getRecordsTypeProvider.setJaxbElementClassMap(jaxbElementClassMap); GetRecordsMessageBodyReader grmbr = new GetRecordsMessageBodyReader(cswTransformProvider, cswSourceConfiguration); return Arrays.asList(getRecordsTypeProvider, new CswResponseExceptionMapper(), grmbr); } /** * Reinitializes the CswSource when there is a configuration change. Otherwise, it checks with * the server to see if any capabilities have changed. * * @param configuration The configuration with which to connect to the server */ public void refresh(Map<String, Object> configuration) { LOGGER.debug("{}: Entering refresh()", cswSourceConfiguration.getId()); if (configuration == null || configuration.isEmpty()) { LOGGER.info("Received null or empty configuration during refresh for {}: {}", this.getClass() .getSimpleName(), cswSourceConfiguration.getId()); return; } // Set Blank Defaults String spatialFilter = (String) configuration.get(FORCE_SPATIAL_FILTER_PROPERTY); if (StringUtils.isBlank(spatialFilter)) { spatialFilter = NO_FORCE_SPATIAL_FILTER; } setForceSpatialFilter(spatialFilter); String currentContentTypeMapping = (String) configuration.get(Metacard.CONTENT_TYPE); if (StringUtils.isBlank(currentContentTypeMapping)) { cswSourceConfiguration.putMetacardCswMapping(Metacard.CONTENT_TYPE, CswConstants.CSW_TYPE); } //if the event service address has changed attempt to remove the subscription before changing to the new event service address if (cswSourceConfiguration.getEventServiceAddress() != null && cswSourceConfiguration.isRegisterForEvents() && !cswSourceConfiguration.getEventServiceAddress() .equals(configuration.get(EVENT_SERVICE_ADDRESS))) { removeEventServiceSubscription(); } // Filter Configuration Map Map<String, Object> filteredConfiguration = filter(configuration); // Run Consumers from Filtered Configuration Map for (Map.Entry<String, Object> entry : filteredConfiguration.entrySet()) { String key = entry.getKey(); Consumer consumer = consumerMap.get(key); if (consumer != null) { LOGGER.debug("Refreshing Configuration : {} with : {}", key, entry.getValue()); consumer.accept(entry.getValue()); } } configureCswSource(); initClientFactory(); configureEventService(); forcePoll(); } private Map<String, Object> filter(Map<String, Object> configuration) { Map<String, Object> filteredConfiguration = new HashMap<>(); // Filter out Blank Strings and null Integers and Booleans filteredConfiguration.putAll(configuration.entrySet() .stream() .filter(entry -> (entry.getValue() != null)) .collect(Collectors.toMap(Map.Entry::getKey, Map.Entry::getValue))); return filteredConfiguration; } protected void setupAvailabilityPoll() { LOGGER.debug("Setting Availability poll task for {} minute(s) on Source {}", cswSourceConfiguration.getPollIntervalMinutes(), cswSourceConfiguration.getId()); long interval = TimeUnit.MINUTES.toMillis(cswSourceConfiguration.getPollIntervalMinutes()); if (availabilityPollFuture == null || availabilityPollFuture.isCancelled()) { if (availabilityTask == null) { availabilityTask = new AvailabilityTask(interval, new CswSourceAvailabilityCommand(), cswSourceConfiguration.getId()); } else { // Any run of the polling operation should trigger the task to run availabilityTask.updateLastAvailableTimestamp(0L); availabilityTask.setInterval(interval); } // Run the availability check immediately prior to scheduling it in a thread. // This is necessary to allow the catalog framework to have the correct // availability when the source is bound availabilityTask.run(); // Schedule the availability check every 1 second. The actually call to // the remote server will only occur if the pollInterval has // elapsed. availabilityPollFuture = scheduler.scheduleWithFixedDelay(availabilityTask, AvailabilityTask.NO_DELAY, AvailabilityTask.ONE_SECOND, TimeUnit.SECONDS); } else { LOGGER.debug("No changes being made on the poller."); } } public void setConnectionTimeout(Integer timeout) { this.cswSourceConfiguration.setConnectionTimeout(timeout); } public Integer getConnectionTimeout() { return this.cswSourceConfiguration.getConnectionTimeout(); } public void setReceiveTimeout(Integer timeout) { this.cswSourceConfiguration.setReceiveTimeout(timeout); } public Integer getReceiveTimeout() { return this.cswSourceConfiguration.getReceiveTimeout(); } public void setContext(BundleContext context) { this.context = context; } @Override public Set<ContentType> getContentTypes() { return new HashSet<ContentType>(contentTypes.values()); } public ResourceReader getResourceReader() { return resourceReader; } public void setResourceReader(ResourceReader resourceReader) { this.resourceReader = resourceReader; } public void setOutputSchema(String outputSchema) { cswSourceConfiguration.setOutputSchema(outputSchema); LOGGER.debug("Setting output schema to: {}", outputSchema); } public void setQueryTypeName(String queryTypeName) { cswSourceConfiguration.setQueryTypeName(queryTypeName); LOGGER.debug("Setting queryTypeQName to: {}", queryTypeName); } public void setQueryTypeNamespace(String queryTypeNamespace) { cswSourceConfiguration.setQueryTypeNamespace(queryTypeNamespace); LOGGER.debug("Setting queryTypePrefix to: {}", queryTypeNamespace); } @Override public boolean isAvailable() { return availabilityTask.isAvailable(); } @Override public boolean isAvailable(SourceMonitor sourceMonitor) { sourceMonitors.add(sourceMonitor); return isAvailable(); } @Override public SourceResponse query(QueryRequest queryRequest) throws UnsupportedQueryException { Subject subject = (Subject) queryRequest.getPropertyValue(SecurityConstants.SECURITY_SUBJECT); Csw csw = factory.getClientForSubject(subject); return query(queryRequest, ElementSetType.FULL, null, csw); } protected SourceResponse query(QueryRequest queryRequest, ElementSetType elementSetName, List<QName> elementNames, Csw csw) throws UnsupportedQueryException { Query query = queryRequest.getQuery(); LOGGER.debug("{}: Received query:\n{}", cswSourceConfiguration.getId(), query); GetRecordsType getRecordsType = createGetRecordsRequest(query, elementSetName, elementNames); if (LOGGER.isDebugEnabled()) { LOGGER.debug("{}: GetRecords request:\n {}", cswSourceConfiguration.getId(), getGetRecordsTypeAsXml(getRecordsType)); } LOGGER.debug("{}: Sending query to: {}", cswSourceConfiguration.getId(), cswSourceConfiguration.getCswUrl()); List<Result> results; Long totalHits; try { CswRecordCollection cswRecordCollection = csw.getRecords(getRecordsType); if (cswRecordCollection == null) { throw new UnsupportedQueryException("Invalid results returned from server"); } this.availabilityTask.updateLastAvailableTimestamp(System.currentTimeMillis()); LOGGER.debug("{}: Received [{}] record(s) of the [{}] record(s) matched from {}.", cswSourceConfiguration.getId(), cswRecordCollection.getNumberOfRecordsReturned(), cswRecordCollection.getNumberOfRecordsMatched(), cswSourceConfiguration.getCswUrl()); results = createResults(cswRecordCollection); totalHits = cswRecordCollection.getNumberOfRecordsMatched(); } catch (CswException cswe) { LOGGER.info(CSW_SERVER_ERROR, cswe); throw new UnsupportedQueryException(CSW_SERVER_ERROR, cswe); } catch (WebApplicationException wae) { String msg = handleWebApplicationException(wae); throw new UnsupportedQueryException(msg, wae); } catch (Exception ce) { String msg = handleClientException(ce); throw new UnsupportedQueryException(msg, ce); } LOGGER.debug("{}: Adding {} result(s) to the source response.", cswSourceConfiguration.getId(), results.size()); SourceResponseImpl sourceResponse = new SourceResponseImpl(queryRequest, results, totalHits); addContentTypes(sourceResponse); return sourceResponse; } @Override public String getDescription() { StringBuilder sb = new StringBuilder(); sb.append(describableProperties.getProperty(DESCRIPTION)) .append(System.getProperty("line.separator")) .append(description); return sb.toString(); } @Override public String getId() { String sourceId = super.getId(); // Note, returning "UNKNOWN" causes issues for collecting source metrics on // ConnectedSources. This method is called initially when the connected source is first // added and the sourceId is null at that time, but this causes metrics for an UNKNOWN // source to be created and never deleted. Returning super.getId() for the ConnectedSources // until a problem is discovered. return sourceId; } public void setId(String id) { cswSourceConfiguration.setId(id); super.setId(id); } @Override public void maskId(String newSourceId) { final String methodName = "maskId"; LOGGER.debug("ENTERING: {} with sourceId = {}", methodName, newSourceId); if (newSourceId != null) { super.maskId(newSourceId); } LOGGER.debug("EXITING: {}", methodName); } @Override public String getOrganization() { return describableProperties.getProperty(ORGANIZATION); } @Override public String getTitle() { return describableProperties.getProperty(TITLE); } @Override public String getVersion() { if (StringUtils.isNotBlank(cswVersion)) { return cswVersion; } return describableProperties.getProperty(VERSION); } @Override public Set<String> getOptions(Metacard arg0) { return null; } @Override public Set<String> getSupportedSchemes() { return null; } @Override public ResourceResponse retrieveResource(URI resourceUri, Map<String, Serializable> requestProperties) throws IOException, ResourceNotFoundException, ResourceNotSupportedException { if (canRetrieveResourceById()) { // If no resource reader was found, retrieve the product through a GetRecordById request Serializable serializableId = null; if (requestProperties != null) { serializableId = requestProperties.get(Core.ID); } if (serializableId == null) { throw new ResourceNotFoundException( "Unable to retrieve resource because no metacard ID was found."); } String metacardId = serializableId.toString(); LOGGER.debug("Retrieving resource for ID : {}", metacardId); Csw csw = factory.getClientForSubject((Subject) requestProperties.get(SecurityConstants.SECURITY_SUBJECT)); GetRecordByIdRequest getRecordByIdRequest = new GetRecordByIdRequest(); getRecordByIdRequest.setService(CswConstants.CSW); getRecordByIdRequest.setOutputSchema(OCTET_STREAM_OUTPUT_SCHEMA); getRecordByIdRequest.setOutputFormat(MediaType.APPLICATION_OCTET_STREAM); getRecordByIdRequest.setId(metacardId); String rangeValue = ""; long requestedBytesToSkip = 0; if (requestProperties.containsKey(CswConstants.BYTES_TO_SKIP)) { requestedBytesToSkip = (Long) requestProperties.get(CswConstants.BYTES_TO_SKIP); rangeValue = String.format("%s%s-", CswConstants.BYTES_EQUAL, requestProperties.get(CswConstants.BYTES_TO_SKIP) .toString()); LOGGER.debug("Range: {}", rangeValue); } CswRecordCollection recordCollection; try { recordCollection = csw.getRecordById(getRecordByIdRequest, rangeValue); Resource resource = recordCollection.getResource(); if (resource != null) { long responseBytesSkipped = 0L; if (recordCollection.getResourceProperties() .get(BYTES_SKIPPED) != null) { responseBytesSkipped = (Long) recordCollection.getResourceProperties() .get(BYTES_SKIPPED); } alignStream(resource.getInputStream(), requestedBytesToSkip, responseBytesSkipped); return new ResourceResponseImpl(new ResourceImpl(new BufferedInputStream( resource.getInputStream()), resource.getMimeTypeValue(), FilenameUtils.getName(resource.getName()))); } } catch (CswException | IOException e) { throw new ResourceNotFoundException(String.format(ERROR_ID_PRODUCT_RETRIEVAL, metacardId), e); } } LOGGER.debug("Retrieving resource at : {}", resourceUri); return resourceReader.retrieveResource(resourceUri, requestProperties); } private void alignStream(InputStream in, long requestedBytesToSkip, long responseBytesSkipped) throws IOException { long misalignment = requestedBytesToSkip - responseBytesSkipped; if (misalignment == 0) { LOGGER.trace("Server responded with the correct byte range."); return; } try { if (requestedBytesToSkip > responseBytesSkipped) { LOGGER.debug("Server returned incorrect byte range, skipping first [{}] bytes", misalignment); if (in.skip(misalignment) != misalignment) { throw new IOException(String.format( "Input Stream could not be skipped %d bytes.", misalignment)); } } else { throw new IOException( "Server skipped more bytes than requested in the range header."); } } catch (IOException e) { throw new IOException(String.format( "Unable to align input stream with the requested byteOffset of %d", requestedBytesToSkip), e); } } public void setCswUrl(String cswUrl) { LOGGER.debug("Setting cswUrl to {}", cswUrl); cswSourceConfiguration.setCswUrl(cswUrl); } public void setUsername(String username) { cswSourceConfiguration.setUsername(username); } public void setPassword(String password) { String updatedPassword = password; if (encryptionService != null) { updatedPassword = encryptionService.decryptValue(password); } cswSourceConfiguration.setPassword(updatedPassword); } public void setDisableCnCheck(Boolean disableCnCheck) { cswSourceConfiguration.setDisableCnCheck(disableCnCheck); } public void setCoordinateOrder(String coordinateOrder) { CswAxisOrder cswAxisOrder = CswAxisOrder.LON_LAT; if (StringUtils.isNotBlank(coordinateOrder)) { cswAxisOrder = CswAxisOrder.valueOf(CswAxisOrder.class, coordinateOrder); if (cswAxisOrder == null) { cswAxisOrder = CswAxisOrder.LON_LAT; } } LOGGER.debug("{}: Setting CSW coordinate order to {}", cswSourceConfiguration.getId(), cswAxisOrder); cswSourceConfiguration.setCswAxisOrder(cswAxisOrder); } public void setUsePosList(Boolean usePosList) { cswSourceConfiguration.setUsePosList(usePosList); LOGGER.debug("Using posList rather than individual pos elements?: {}", usePosList); } public void setIsCqlForced(Boolean isCqlForced) { cswSourceConfiguration.setIsCqlForced(isCqlForced); } public void setMetacardMappings(String[] configuredMappings) { if (configuredMappings != null && configuredMappings.length > 0) { Map<String, String> mappings = new HashMap<>(configuredMappings.length); Arrays.stream(configuredMappings) .forEach(m -> { String[] parts = m.split("="); mappings.put(parts[0], parts[1]); }); cswSourceConfiguration.setMetacardCswMappings(mappings); } } public void setFilterAdapter(FilterAdapter filterAdapter) { this.filterAdapter = filterAdapter; } public void setFilterBuilder(FilterBuilder filterBuilder) { this.filterBuilder = filterBuilder; } public void setPollInterval(Integer interval) { this.cswSourceConfiguration.setPollIntervalMinutes(interval); } public Converter getCswTransformConverter() { return this.cswTransformConverter; } public void setCswTransformConverter(Converter provider) { this.cswTransformConverter = provider; } public String getForceSpatialFilter() { return forceSpatialFilter; } public void setForceSpatialFilter(String forceSpatialFilter) { this.forceSpatialFilter = forceSpatialFilter; } private GetRecordsType createGetRecordsRequest(Query query, ElementSetType elementSetName, List<QName> elementNames) throws UnsupportedQueryException { GetRecordsType getRecordsType = new GetRecordsType(); getRecordsType.setVersion(cswVersion); getRecordsType.setService(CswConstants.CSW); getRecordsType.setResultType(ResultType.RESULTS); getRecordsType.setStartPosition(BigInteger.valueOf(query.getStartIndex())); getRecordsType.setMaxRecords(BigInteger.valueOf(query.getPageSize())); getRecordsType.setOutputFormat(MediaType.APPLICATION_XML); if (!isOutputSchemaSupported()) { String msg = "CSW Source: " + cswSourceConfiguration.getId() + " does not support output schema: " + cswSourceConfiguration.getOutputSchema() + "."; throw new UnsupportedQueryException(msg); } getRecordsType.setOutputSchema(cswSourceConfiguration.getOutputSchema()); getRecordsType.setAbstractQuery(createQuery(query, elementSetName, elementNames)); return getRecordsType; } private ElementSetNameType createElementSetName(ElementSetType type) { ElementSetNameType elementSetNameType = new ElementSetNameType(); elementSetNameType.setValue(type); return elementSetNameType; } private JAXBElement<QueryType> createQuery(Query query, ElementSetType elementSetType, List<QName> elementNames) throws UnsupportedQueryException { QueryType queryType = new QueryType(); QName queryTypeQName = null; try { if (StringUtils.isNotBlank(cswSourceConfiguration.getQueryTypeName())) { String[] parts = cswSourceConfiguration.getQueryTypeName() .split(":"); if (parts.length > 1) { queryTypeQName = new QName(cswSourceConfiguration.getQueryTypeNamespace(), parts[1], parts[0]); } else { queryTypeQName = new QName(cswSourceConfiguration.getQueryTypeNamespace(), cswSourceConfiguration.getQueryTypeName()); } } } catch (IllegalArgumentException e) { LOGGER.debug("Unable to parse query type QName of {}. Defaulting to CSW Record", cswSourceConfiguration.getQueryTypeName()); } if (queryTypeQName == null) { queryTypeQName = new QName(CswConstants.CSW_OUTPUT_SCHEMA, CswConstants.CSW_RECORD_LOCAL_NAME, CswConstants.CSW_NAMESPACE_PREFIX); } queryType.setTypeNames(Arrays.asList(queryTypeQName)); if (null != elementSetType) { queryType.setElementSetName(createElementSetName(elementSetType)); } else if (!CollectionUtils.isEmpty(elementNames)) { queryType.setElementName(elementNames); } else { queryType.setElementSetName(createElementSetName(ElementSetType.FULL)); } SortByType sortBy = createSortBy(query); if (sortBy != null) { queryType.setSortBy(sortBy); } QueryConstraintType constraint = createQueryConstraint(query); if (null != constraint) { queryType.setConstraint(constraint); } ObjectFactory objectFactory = new ObjectFactory(); return objectFactory.createQuery(queryType); } private SortByType createSortBy(Query query) { SortByType sortBy = null; if (query.getSortBy() != null) { sortBy = new SortByType(); SortPropertyType sortProperty = new SortPropertyType(); PropertyNameType propertyName = new PropertyNameType(); String propName; if (query.getSortBy() .getPropertyName() != null) { propName = query.getSortBy() .getPropertyName() .getPropertyName(); if (propName != null) { if (Result.TEMPORAL.equals(propName) || Metacard.ANY_DATE.equals(propName)) { propName = Core.MODIFIED; } else if (Result.RELEVANCE.equals(propName) || Metacard.ANY_TEXT.equals( propName)) { propName = Core.TITLE; } else if (Result.DISTANCE.equals(propName) || Metacard.ANY_GEO.equals(propName)) { return null; } propertyName.setContent(Arrays.asList((Object) cswFilterDelegate.mapPropertyName( propName))); sortProperty.setPropertyName(propertyName); if (SortOrder.DESCENDING.equals(query.getSortBy() .getSortOrder())) { sortProperty.setSortOrder(SortOrderType.DESC); } else { sortProperty.setSortOrder(SortOrderType.ASC); } sortBy.getSortProperty() .add(sortProperty); } } else { return null; } } return sortBy; } private QueryConstraintType createQueryConstraint(Query query) throws UnsupportedQueryException { FilterType filter = createFilter(query); if (null == filter) { return null; } QueryConstraintType queryConstraintType = new QueryConstraintType(); queryConstraintType.setVersion(CswConstants.CONSTRAINT_VERSION); if (isConstraintCql || cswSourceConfiguration.isCqlForced()) { queryConstraintType.setCqlText(CswCqlTextFilter.getInstance() .getCqlText(filter)); } else { queryConstraintType.setFilter(filter); } return queryConstraintType; } private FilterType createFilter(Query query) throws UnsupportedQueryException { OperationsMetadata operationsMetadata = capabilities.getOperationsMetadata(); if (null == operationsMetadata) { LOGGER.debug("{}: CSW Source contains no operations", cswSourceConfiguration.getId()); return new FilterType(); } if (null == capabilities.getFilterCapabilities()) { LOGGER.debug( "{}: CSW Source did not provide Filter Capabilities, unable to preform query.", cswSourceConfiguration.getId()); throw new UnsupportedQueryException(cswSourceConfiguration.getId() + ": CSW Source did not provide Filter Capabilities, unable to preform query."); } return this.filterAdapter.adapt(query, cswFilterDelegate); } protected List<Result> createResults(CswRecordCollection cswRecordCollection) { List<Result> results = new ArrayList<>(); LOGGER.debug("Found {} metacard(s) in the CswRecordCollection.", cswRecordCollection.getCswRecords() .size()); String transformerId = getMetadataTransformerId(); MetadataTransformer transformer = lookupMetadataTransformer(transformerId); for (Metacard metacard : cswRecordCollection.getCswRecords()) { MetacardImpl wrappedMetacard = new MetacardImpl(metacard); wrappedMetacard.setSourceId(getId()); if (wrappedMetacard.getAttribute(Core.RESOURCE_DOWNLOAD_URL) != null && wrappedMetacard.getAttribute(Core.RESOURCE_DOWNLOAD_URL) .getValue() != null) { wrappedMetacard.setAttribute(Core.RESOURCE_URI, wrappedMetacard.getAttribute(Core.RESOURCE_DOWNLOAD_URL) .getValue()); } if (wrappedMetacard.getAttribute(Core.DERIVED_RESOURCE_DOWNLOAD_URL) != null && !wrappedMetacard.getAttribute(Core.DERIVED_RESOURCE_DOWNLOAD_URL) .getValues() .isEmpty()) { wrappedMetacard.setAttribute(new AttributeImpl(Core.DERIVED_RESOURCE_URI, wrappedMetacard.getAttribute(Core.DERIVED_RESOURCE_DOWNLOAD_URL) .getValues())); } Metacard tranformedMetacard = wrappedMetacard; if (transformer != null) { tranformedMetacard = transform(metacard, transformer); } Result result = new ResultImpl(tranformedMetacard); results.add(result); } return results; } protected String getMetadataTransformerId() { return DEFAULT_CSW_TRANSFORMER_ID; } /** * Transforms the Metacard created from the CSW Record using the transformer specified by its * ID. * * @param metacard * @return */ protected Metacard transform(Metacard metacard, MetadataTransformer transformer) { if (metacard == null) { throw new IllegalArgumentException( cswSourceConfiguration.getId() + ": Metacard is null."); } try { return transformer.transform(metacard); } catch (CatalogTransformerException e) { LOGGER.debug("{} :Metadata Transformation Failed for metacard: {}", cswSourceConfiguration.getId(), metacard.getId(), e); } return metacard; } protected MetadataTransformer lookupMetadataTransformer(String transformerId) { ServiceReference<?>[] refs; try { refs = context.getServiceReferences(MetadataTransformer.class.getName(), "(" + Constants.SERVICE_ID + "=" + transformerId + ")"); } catch (InvalidSyntaxException e) { LOGGER.debug("{}: Invalid transformer ID.", cswSourceConfiguration.getId(), e); return null; } if (refs == null || refs.length == 0) { LOGGER.debug("{}: Metadata Transformer {} not found.", cswSourceConfiguration.getId(), transformerId); return null; } else { return (MetadataTransformer) context.getService(refs[0]); } } private String getGetRecordsTypeAsXml(GetRecordsType getRecordsType) { Writer writer = new StringWriter(); try { Marshaller marshaller = JAXB_CONTEXT.createMarshaller(); marshaller.setProperty(Marshaller.JAXB_FORMATTED_OUTPUT, Boolean.TRUE); JAXBElement<GetRecordsType> jaxbElement = new JAXBElement<GetRecordsType>(new QName( CswConstants.CSW_OUTPUT_SCHEMA, CswConstants.GET_RECORDS), GetRecordsType.class, getRecordsType); marshaller.marshal(jaxbElement, writer); } catch (JAXBException e) { LOGGER.debug("{}: Unable to marshall {} to XML.", cswSourceConfiguration.getId(), GetRecordsType.class, e); } return writer.toString(); } protected CapabilitiesType getCapabilities() { CapabilitiesType caps = null; Subject subject = getSystemSubject(); Csw csw = factory.getClientForSubject(subject); try { LOGGER.debug("Doing getCapabilities() call for CSW"); GetCapabilitiesRequest request = new GetCapabilitiesRequest(CswConstants.CSW); request.setAcceptVersions( CswConstants.VERSION_2_0_2 + "," + CswConstants.VERSION_2_0_1); caps = csw.getCapabilities(request); } catch (CswException cswe) { LOGGER.info(CSW_SERVER_ERROR + " Received HTTP code '{}' from server for source with id='{}'. Set Logging to DEBUG for details.", cswe.getHttpStatus(), cswSourceConfiguration.getId()); LOGGER.debug(CSW_SERVER_ERROR, cswe); } catch (WebApplicationException wae) { LOGGER.debug(handleWebApplicationException(wae), wae); } catch (Exception ce) { handleClientException(ce); } return caps; } protected Subject getSystemSubject() { return SECURITY.runAsAdmin(SECURITY::getSystemSubject); } public void configureCswSource() { detailLevels = EnumSet.noneOf(ElementSetType.class); capabilities = getCapabilities(); if (null != capabilities) { cswVersion = capabilities.getVersion(); if (CswConstants.VERSION_2_0_1.equals(cswVersion)) { setCsw201(); } if (capabilities.getFilterCapabilities() == null) { return; } readGetRecordsOperation(capabilities); loadContentTypes(); LOGGER.debug("{}: {}", cswSourceConfiguration.getId(), capabilities.toString()); } else { LOGGER.info("{}: CSW Server did not return any capabilities.", cswSourceConfiguration.getId()); } } private Operation getOperation(OperationsMetadata operations, String operation) { for (Operation op : operations.getOperation()) { if (operation.equals(op.getName())) { return op; } } LOGGER.info("{}: CSW Server did not contain getRecords operation", cswSourceConfiguration.getId()); return null; } /** * Parses the getRecords {@link Operation} to understand the capabilities of the org.codice.ddf.spatial.ogc.csw.catalog.common.Csw Server. A * sample GetRecords Operation may look like this: * <p> * <pre> * <ows:Operation name="GetRecords"> * <ows:DCP> * <ows:HTTP> * <ows:Get xlink:href="http://www.cubewerx.com/cwcsw.cgi?" /> * <ows:Post xlink:href="http://www.cubewerx.com/cwcsw.cgi" /> * </ows:HTTP> * </ows:DCP> * <ows:Parameter name="TypeName"> * <ows:Value>csw:Record</ows:Value> * </ows:Parameter> * <ows:Parameter name="outputFormat"> * <ows:Value>application/xml</ows:Value> * <ows:Value>text/html</ows:Value> * <ows:Value>text/plain</ows:Value> * </ows:Parameter> * <ows:Parameter name="outputSchema"> * <ows:Value>http://www.opengis.net/cat/csw/2.0.2</ows:Value> * </ows:Parameter> * <ows:Parameter name="resultType"> * <ows:Value>hits</ows:Value> * <ows:Value>results</ows:Value> * <ows:Value>validate</ows:Value> * </ows:Parameter> * <ows:Parameter name="ElementSetName"> * <ows:Value>brief</ows:Value> * <ows:Value>summary</ows:Value> * <ows:Value>full</ows:Value> * </ows:Parameter> * <ows:Parameter name="CONSTRAINTLANGUAGE"> * <ows:Value>Filter</ows:Value> * </ows:Parameter> * </ows:Operation> * </pre> * * @param capabilitiesType The capabilities the org.codice.ddf.spatial.ogc.csw.catalog.common.Csw Server supports */ private void readGetRecordsOperation(CapabilitiesType capabilitiesType) { OperationsMetadata operationsMetadata = capabilitiesType.getOperationsMetadata(); if (null == operationsMetadata) { LOGGER.info("{}: CSW Source contains no operations", cswSourceConfiguration.getId()); return; } description = capabilitiesType.getServiceIdentification() .getAbstract(); Operation getRecordsOp = getOperation(operationsMetadata, CswConstants.GET_RECORDS); if (null == getRecordsOp) { LOGGER.info("{}: CSW Source contains no getRecords Operation", cswSourceConfiguration.getId()); return; } this.supportedOutputSchemas = getParameter(getRecordsOp, CswConstants.OUTPUT_SCHEMA_PARAMETER); DomainType constraintLanguage = getParameter(getRecordsOp, CswConstants.CONSTRAINT_LANGUAGE_PARAMETER); if (null != constraintLanguage) { DomainType outputFormatValues = getParameter(getRecordsOp, CswConstants.OUTPUT_FORMAT_PARAMETER); DomainType resultTypesValues = getParameter(getRecordsOp, CswConstants.RESULT_TYPE_PARAMETER); readSetDetailLevels(getParameter(getRecordsOp, CswConstants.ELEMENT_SET_NAME_PARAMETER)); List<String> constraints = new ArrayList<>(); for (String s : constraintLanguage.getValue()) { constraints.add(s.toLowerCase()); } if (constraints.contains(CswConstants.CONSTRAINT_LANGUAGE_CQL.toLowerCase()) && !constraints.contains(CswConstants.CONSTRAINT_LANGUAGE_FILTER.toLowerCase())) { isConstraintCql = true; } else { isConstraintCql = false; } setFilterDelegate(getRecordsOp, capabilitiesType.getFilterCapabilities(), outputFormatValues, resultTypesValues, cswSourceConfiguration); spatialCapabilities = capabilitiesType.getFilterCapabilities() .getSpatialCapabilities(); if (!NO_FORCE_SPATIAL_FILTER.equals(forceSpatialFilter)) { SpatialOperatorType sot = new SpatialOperatorType(); SpatialOperatorNameType sont = SpatialOperatorNameType.fromValue(forceSpatialFilter); sot.setName(sont); sot.setGeometryOperands(cswFilterDelegate.getGeoOpsForSpatialOp(sont)); SpatialOperatorsType spatialOperators = new SpatialOperatorsType(); spatialOperators.setSpatialOperator(Arrays.asList(sot)); cswFilterDelegate.setSpatialOps(spatialOperators); } } } /** * Sets the {@link ddf.catalog.filter.FilterDelegate} used by the AbstractCswSource. May be overridden * in order to provide a custom ddf.catalog.filter.FilterDelegate implementation. * * @param getRecordsOp * @param filterCapabilities * @param outputFormatValues * @param resultTypesValues * @param cswSourceConfiguration */ protected void setFilterDelegate(Operation getRecordsOp, FilterCapabilities filterCapabilities, DomainType outputFormatValues, DomainType resultTypesValues, CswSourceConfiguration cswSourceConfiguration) { LOGGER.trace("Setting cswFilterDelegate to default CswFilterDelegate"); cswFilterDelegate = new CswFilterDelegate(getRecordsOp, filterCapabilities, outputFormatValues, resultTypesValues, cswSourceConfiguration); } private void readSetDetailLevels(DomainType elementSetNamesValues) { if (null != elementSetNamesValues) { for (String esn : elementSetNamesValues.getValue()) { try { detailLevels.add(ElementSetType.fromValue(esn.toLowerCase())); } catch (IllegalArgumentException iae) { LOGGER.debug("{}: \"{}\" is not a ElementSetType, Error: {}", cswSourceConfiguration.getId(), esn, iae); } } } } protected void loadContentTypes() { Filter filter = filterBuilder.attribute(CswConstants.ANY_TEXT) .is() .like() .text(CswConstants.WILD_CARD); Query query = new QueryImpl(filter, 1, CONTENT_TYPE_SAMPLE_SIZE, null, true, 0); QueryRequest queryReq = new QueryRequestImpl(query); try { query(queryReq); } catch (UnsupportedQueryException e) { LOGGER.info("{}: Failed to read Content-Types from CSW Server, Error: {}", getId(), e); } } /** * Searches every query response for previously unknown content types * * @param response A Query Response */ private void addContentTypes(SourceResponse response) { if (response == null || response.getResults() == null) { return; } for (Result result : response.getResults()) { Metacard metacard = result.getMetacard(); if (metacard != null) { final String name = metacard.getContentTypeName(); String version = metacard.getContentTypeVersion(); final URI namespace = metacard.getContentTypeNamespace(); if (!StringUtils.isEmpty(name) && !contentTypes.containsKey(name)) { if (version == null) { version = ""; } contentTypes.put(name, new ContentTypeImpl(name, version, namespace)); } } } } public DomainType getParameter(Operation operation, String name) { for (DomainType parameter : operation.getParameter()) { if (name.equalsIgnoreCase(parameter.getName())) { return parameter; } } LOGGER.debug("{}: CSW Operation \"{}\" did not contain the \"{}\" parameter", cswSourceConfiguration.getId(), operation.getName(), name); return null; } protected String handleWebApplicationException(WebApplicationException wae) { Response response = wae.getResponse(); CswException cswException = new CswResponseExceptionMapper().fromResponse(response); // Add the CswException message to the error message being logged. Do // not include the CswException stack trace because it will not be // meaningful since it will not show the root cause of the exception // because the ExceptionReport was sent from CSW as an "OK" JAX-RS // status rather than an error status. return CSW_SERVER_ERROR + " " + cswSourceConfiguration.getId() + "\n" + cswException.getMessage(); } protected String handleClientException(Exception ce) { String msg; Throwable cause = ce.getCause(); String sourceId = cswSourceConfiguration.getId(); if (cause instanceof WebApplicationException) { msg = handleWebApplicationException((WebApplicationException) cause); } else if (cause instanceof IllegalArgumentException) { msg = CSW_SERVER_ERROR + " Source '" + sourceId + "'. The URI '" + cswSourceConfiguration.getCswUrl() + "' does not specify a valid protocol or could not be correctly parsed. " + ce.getMessage(); } else if (cause instanceof SSLHandshakeException) { msg = CSW_SERVER_ERROR + " Source '" + sourceId + "' with URL '" + cswSourceConfiguration.getCswUrl() + "': " + cause; } else if (cause instanceof ConnectException) { msg = CSW_SERVER_ERROR + " Source '" + sourceId + "' may not be running.\n" + ce.getMessage(); } else { msg = CSW_SERVER_ERROR + " Source '" + sourceId + "'\n" + ce; } LOGGER.info(msg); LOGGER.debug(msg, ce); return msg; } private void availabilityChanged(boolean isAvailable) { if (isAvailable) { LOGGER.debug("CSW source {} is available.", cswSourceConfiguration.getId()); } else { LOGGER.debug("CSW source {} is unavailable.", cswSourceConfiguration.getId()); } for (SourceMonitor monitor : this.sourceMonitors) { if (isAvailable) { LOGGER.debug("Notifying source monitor that CSW source {} is available.", cswSourceConfiguration.getId()); monitor.setAvailable(); } else { LOGGER.debug("Notifying source monitor that CSW source {} is unavailable.", cswSourceConfiguration.getId()); monitor.setUnavailable(); } } } /** * Determine if the resource should be retrieved using a ResourceReader or by calling * GetRecordById. */ private boolean canRetrieveResourceById() { OperationsMetadata operationsMetadata = capabilities.getOperationsMetadata(); Operation getRecordByIdOp = getOperation(operationsMetadata, CswConstants.GET_RECORD_BY_ID); if (getRecordByIdOp != null) { DomainType getRecordByIdOutputSchemas = getParameter(getRecordByIdOp, CswConstants.OUTPUT_SCHEMA_PARAMETER); if (getRecordByIdOutputSchemas != null && getRecordByIdOutputSchemas.getValue() != null && getRecordByIdOutputSchemas.getValue() .contains(OCTET_STREAM_OUTPUT_SCHEMA)) { return true; } } return false; } private boolean isOutputSchemaSupported() { return (this.cswSourceConfiguration.getOutputSchema() != null && this.supportedOutputSchemas != null) && this.supportedOutputSchemas.getValue() .contains(cswSourceConfiguration.getOutputSchema()); } public void setAvailabilityTask(AvailabilityTask availabilityTask) { this.availabilityTask = availabilityTask; } public void setSecurityManager(SecurityManager securityManager) { this.securityManager = securityManager; } @Override public String getConfigurationPid() { return configurationPid; } @Override public void setConfigurationPid(String configurationPid) { this.configurationPid = configurationPid; } @Override public Map<String, Set<String>> getSecurityAttributes() { return this.cswSourceConfiguration.getSecurityAttributes(); } public void setMetacardTypes(List<MetacardType> types) { this.metacardTypes = types; } /** * Callback class to check the Availability of the CswSource. * <p> * NOTE: Ideally, the framework would call isAvailable on the Source and the SourcePoller would * have an AvailabilityTask that cached each Source's availability. Until that is done, allow * the command to handle the logic of managing availability. */ private class CswSourceAvailabilityCommand implements AvailabilityCommand { @Override public boolean isAvailable() { LOGGER.debug("Checking availability for source {} ", cswSourceConfiguration.getId()); boolean oldAvailability = AbstractCswSource.this.isAvailable(); boolean newAvailability; // Simple "ping" to ensure the source is responding newAvailability = (getCapabilities() != null); if (oldAvailability != newAvailability) { // If the source becomes available, configure it. if (newAvailability) { configureCswSource(); } availabilityChanged(newAvailability); } return newAvailability; } } /** * Set the version to CSW 2.0.1. The schemas don't vary much between 2.0.2 and 2.0.1. The * largest difference is the namespace itself. This method tells CXF JAX-RS to transform * outgoing messages CSW namespaces to 2.0.1. */ public void setCsw201() { Map<String, String> outTransformElements = new HashMap<>(); outTransformElements.put("{" + CswConstants.CSW_OUTPUT_SCHEMA + "}*", "{http://www.opengis.net/cat/csw}*"); getRecordsTypeProvider.setOutTransformElements(outTransformElements); } private void configureEventService() { if (!cswSourceConfiguration.isRegisterForEvents()) { LOGGER.debug("registerForEvents = false - do not configure site {} for events", this.getId()); removeEventServiceSubscription(); return; } if (StringUtils.isEmpty(cswSourceConfiguration.getEventServiceAddress())) { LOGGER.debug( "eventServiceAddress is NULL or empty - do not configure site {} for events", this.getId()); return; } // If filterless subscription has already been configured then do not // try to configure // another one (because the DDF will allow it and you will get multiple // events sent when // a single event should be sent) if (filterlessSubscriptionId != null) { LOGGER.debug("filterless subscription already configured for site " + filterlessSubscriptionId); return; } initSubscribeClientFactory(); CswSubscribe cswSubscribe = subscribeClientFactory.getClientForSubject(getSystemSubject()); GetRecordsType request = createSubscriptionGetRecordsRequest(); try { Response response = cswSubscribe.createRecordsSubscription(request); if (Response.Status.OK.getStatusCode() == response.getStatus()) { AcknowledgementType acknowledgementType = response.readEntity(AcknowledgementType.class); filterlessSubscriptionId = acknowledgementType.getRequestId(); } } catch (CswException e) { LOGGER.info("Failed to register a subscription for events from csw source with id of " + this.getId()); } } private GetRecordsType createSubscriptionGetRecordsRequest() { GetRecordsType getRecordsType = new GetRecordsType(); getRecordsType.setVersion(cswVersion); getRecordsType.setService(CswConstants.CSW); getRecordsType.setResultType(ResultType.RESULTS); getRecordsType.setStartPosition(BigInteger.ONE); getRecordsType.setMaxRecords(BigInteger.TEN); getRecordsType.setOutputFormat(MediaType.APPLICATION_XML); getRecordsType.setOutputSchema("urn:catalog:metacard"); getRecordsType.getResponseHandler() .add(SystemBaseUrl.constructUrl("csw/subscription/event", true)); QueryType queryType = new QueryType(); queryType.setElementSetName(createElementSetName(ElementSetType.FULL)); ObjectFactory objectFactory = new ObjectFactory(); getRecordsType.setAbstractQuery(objectFactory.createQuery(queryType)); return getRecordsType; } private void removeEventServiceSubscription() { if (filterlessSubscriptionId != null && subscribeClientFactory != null) { CswSubscribe cswSubscribe = subscribeClientFactory.getClientForSubject(getSystemSubject()); try { cswSubscribe.deleteRecordsSubscription(filterlessSubscriptionId); } catch (CswException e) { LOGGER.info( "Failed to remove filterless subscription registered for id {} for csw source with id of {}", filterlessSubscriptionId, this.getId()); } filterlessSubscriptionId = null; } } public void setRegisterForEvents(Boolean registerForEvents) { this.cswSourceConfiguration.setRegisterForEvents(registerForEvents); } public void setEventServiceAddress(String eventServiceAddress) { this.cswSourceConfiguration.setEventServiceAddress(eventServiceAddress); } protected void addSourceMonitor(SourceMonitor sourceMonitor) { sourceMonitors.add(sourceMonitor); } }