/** * 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.wfs.v2_0_0.catalog.source; import java.io.IOException; import java.io.InputStream; import java.io.Serializable; import java.io.StringWriter; import java.math.BigInteger; import java.net.URI; import java.util.ArrayList; import java.util.Arrays; import java.util.Collections; import java.util.Dictionary; import java.util.HashMap; import java.util.HashSet; import java.util.Hashtable; import java.util.List; import java.util.Map; import java.util.Map.Entry; import java.util.Properties; import java.util.Set; import java.util.concurrent.Executors; import java.util.concurrent.ScheduledExecutorService; import java.util.concurrent.ScheduledFuture; import java.util.concurrent.TimeUnit; 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.collections.CollectionUtils; import org.apache.commons.io.IOUtils; import org.apache.commons.lang.StringUtils; import org.apache.ws.commons.schema.XmlSchema; 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.catalog.common.ContentTypeFilterDelegate; import org.codice.ddf.spatial.ogc.catalog.common.TrustedRemoteSource; import org.codice.ddf.spatial.ogc.wfs.catalog.common.FeatureMetacardType; import org.codice.ddf.spatial.ogc.wfs.catalog.common.WfsConstants; import org.codice.ddf.spatial.ogc.wfs.catalog.common.WfsException; import org.codice.ddf.spatial.ogc.wfs.catalog.converter.FeatureConverter; import org.codice.ddf.spatial.ogc.wfs.catalog.mapper.MetacardMapper; import org.codice.ddf.spatial.ogc.wfs.v2_0_0.catalog.common.DescribeFeatureTypeRequest; import org.codice.ddf.spatial.ogc.wfs.v2_0_0.catalog.common.GetCapabilitiesRequest; import org.codice.ddf.spatial.ogc.wfs.v2_0_0.catalog.common.Wfs20Constants; import org.codice.ddf.spatial.ogc.wfs.v2_0_0.catalog.common.Wfs20FeatureCollection; import org.codice.ddf.spatial.ogc.wfs.v2_0_0.catalog.converter.FeatureConverterFactory; import org.codice.ddf.spatial.ogc.wfs.v2_0_0.catalog.converter.impl.GenericFeatureConverterWfs20; import org.opengis.filter.sort.SortBy; import org.opengis.filter.sort.SortOrder; import org.osgi.framework.BundleContext; import org.osgi.framework.InvalidSyntaxException; import org.osgi.framework.ServiceReference; import org.osgi.framework.ServiceRegistration; import org.slf4j.Logger; import org.slf4j.LoggerFactory; 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.ContentTypeImpl; import ddf.catalog.data.impl.ResultImpl; import ddf.catalog.filter.FilterAdapter; 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.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.impl.ResourceImpl; 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.settings.SecuritySettingsService; import net.opengis.filter.v_2_0_0.FilterCapabilities; import net.opengis.filter.v_2_0_0.FilterType; import net.opengis.filter.v_2_0_0.SortByType; import net.opengis.filter.v_2_0_0.SortOrderType; import net.opengis.filter.v_2_0_0.SortPropertyType; import net.opengis.filter.v_2_0_0.SpatialOperatorType; import net.opengis.filter.v_2_0_0.SpatialOperatorsType; import net.opengis.wfs.v_2_0_0.FeatureTypeType; import net.opengis.wfs.v_2_0_0.GetFeatureType; import net.opengis.wfs.v_2_0_0.QueryType; import net.opengis.wfs.v_2_0_0.WFSCapabilitiesType; /** * Provides a Federated and Connected source implementation for OGC WFS servers. * */ public class WfsSource extends MaskableImpl implements FederatedSource, ConnectedSource { public static final int WFS_MAX_FEATURES_RETURNED = 1000; protected static final int WFS_QUERY_PAGE_SIZE_MULTIPLIER = 3; private static final Logger LOGGER = LoggerFactory.getLogger(WfsSource.class); 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 String WFSURL_PROPERTY = "wfsUrl"; private static final String ID_PROPERTY = "id"; private static final String USERNAME_PROPERTY = "username"; private static final String PASSWORD_PROPERTY = "password"; private static final String NON_QUERYABLE_PROPS_PROPERTY = "nonQueryableProperties"; private static final String SPATIAL_FILTER_PROPERTY = "forceSpatialFilter"; private static final String COORDINATE_ORDER = "coordinateOrder"; private static final String DISABLE_SORTING = "disableSorting"; private static final String NO_FORCED_SPATIAL_FILTER = "NO_FILTER"; private static final String CONNECTION_TIMEOUT_PROPERTY = "connectionTimeout"; private static final String RECEIVE_TIMEOUT_PROPERTY = "receiveTimeout"; private static final String WFS_ERROR_MESSAGE = "Error received from Wfs Server."; private static final String UNKNOWN = "unknown"; private static final String DEFAULT_WFS_TRANSFORMER_ID = "wfs_2_0"; private static final String POLL_INTERVAL_PROPERTY = "pollInterval"; private static Properties describableProperties = new Properties(); static { try (InputStream properties = WfsSource.class .getResourceAsStream(DESCRIBABLE_PROPERTIES_FILE)) { describableProperties.load(properties); } catch (IOException e) { LOGGER.info(e.getMessage(), e); } } protected Map<QName, WfsFilterDelegate> featureTypeFilters = new HashMap<QName, WfsFilterDelegate>(); private String wfsUrl; private String username; private String password; private Boolean disableCnCheck = Boolean.FALSE; private String coordinateOrder = WfsConstants.LAT_LON_ORDER; private FilterAdapter filterAdapter; private RemoteWfs remoteWfs; private BundleContext context; private Map<String, ServiceRegistration> metacardTypeServiceRegistrations = new HashMap<String, ServiceRegistration>(); private String[] nonQueryableProperties; private List<FeatureConverterFactory> featureConverterFactories; private Integer pollInterval; private Integer connectionTimeout; private Integer receiveTimeout; private String forceSpatialFilter = NO_FORCED_SPATIAL_FILTER; private SpatialOperatorsType supportedSpatialOperators; private ScheduledExecutorService scheduler; private ScheduledFuture<?> availabilityPollFuture; private AvailabilityTask availabilityTask; private Set<SourceMonitor> sourceMonitors = new HashSet<SourceMonitor>(); private List<MetacardMapper> metacardToFeatureMappers; private boolean disableSorting; private SecuritySettingsService securitySettingsService; public WfsSource(RemoteWfs remoteWfs, FilterAdapter filterAdapter, BundleContext context, AvailabilityTask task) { this.remoteWfs = remoteWfs; this.filterAdapter = filterAdapter; this.context = context; this.availabilityTask = task; this.metacardToFeatureMappers = Collections.emptyList(); this.disableSorting = false; configureWfsFeatures(); } public WfsSource() { // Required for bean creation LOGGER.debug("Creating {}", WfsSource.class.getName()); scheduler = Executors.newSingleThreadScheduledExecutor(); } /** * Init is called when the bundle is initially configured. * * <p> * The init process creates a RemoteWfs object using the connection parameters from the * configuration. * */ public void init() { setupAvailabilityPoll(); } public void destroy() { unregisterAllMetacardTypes(); availabilityPollFuture.cancel(true); scheduler.shutdownNow(); } /** * Refresh is called if the bundle configuration is updated. * * <p> * If any of the connection related properties change, an attempt is made to re-connect. * * @param configuration */ public void refresh(Map<String, Object> configuration) { LOGGER.debug("WfsSource {}: Refresh called", getId()); String wfsUrl = (String) configuration.get(WFSURL_PROPERTY); String password = (String) configuration.get(PASSWORD_PROPERTY); String username = (String) configuration.get(USERNAME_PROPERTY); Boolean disableCnCheckProp = (Boolean) configuration .get(TrustedRemoteSource.DISABLE_CN_CHECK_PROPERTY); String coordinateOrder = (String) configuration.get(COORDINATE_ORDER); boolean disableSorting = (Boolean) configuration.get(DISABLE_SORTING); String id = (String) configuration.get(ID_PROPERTY); setConnectionTimeout((Integer) configuration.get(CONNECTION_TIMEOUT_PROPERTY)); setReceiveTimeout((Integer) configuration.get(RECEIVE_TIMEOUT_PROPERTY)); updateTimeouts(); String[] nonQueryableProperties = (String[]) configuration .get(NON_QUERYABLE_PROPS_PROPERTY); this.nonQueryableProperties = nonQueryableProperties; Integer newPollInterval = (Integer) configuration.get(POLL_INTERVAL_PROPERTY); super.setId(id); this.wfsUrl = wfsUrl; this.password = password; this.username = username; this.disableCnCheck = disableCnCheckProp; this.coordinateOrder = coordinateOrder; this.disableSorting = disableSorting; this.forceSpatialFilter = (String) configuration.get(SPATIAL_FILTER_PROPERTY); connectToRemoteWfs(); configureWfsFeatures(); if (!pollInterval.equals(newPollInterval)) { LOGGER.debug("Poll Interval was changed for source {}.", getId()); setPollInterval(newPollInterval); availabilityPollFuture.cancel(true); setupAvailabilityPoll(); } } private void setupAvailabilityPoll() { LOGGER.debug("Setting Availability poll task for {} minute(s) on Source {}", pollInterval, getId()); WfsSourceAvailabilityCommand command = new WfsSourceAvailabilityCommand(); long interval = TimeUnit.MINUTES.toMillis(pollInterval); if (availabilityPollFuture == null || availabilityPollFuture.isCancelled()) { if (availabilityTask == null) { availabilityTask = new AvailabilityTask(interval, command, getId()); } else { 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(); // Run 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); } } private void connectToRemoteWfs() { LOGGER.debug( "WfsSource {}: Connecting to remote WFS Server {}. SSL cert verification disabled? {}", getId(), wfsUrl, this.disableCnCheck); try { remoteWfs = new RemoteWfs(wfsUrl, username, password, disableCnCheck); remoteWfs.setSecuritySettings(securitySettingsService); remoteWfs.setTlsParameters(); remoteWfs.setTimeouts(connectionTimeout, receiveTimeout); } catch (IllegalArgumentException iae) { LOGGER.warn("Unable to create RemoteWfs.", iae); remoteWfs = null; } } private void availabilityChanged(boolean isAvailable) { if (isAvailable) { LOGGER.info("WFS source {} is available.", getId()); } else { LOGGER.info("WFS source {} is unavailable.", getId()); this.remoteWfs = null; } for (SourceMonitor monitor : this.sourceMonitors) { if (isAvailable) { LOGGER.debug("Notifying source monitor that WFS source {} is available.", getId()); monitor.setAvailable(); } else { LOGGER.debug("Notifying source monitor that WFS source {} is unavailable.", getId()); monitor.setUnavailable(); } } } private WFSCapabilitiesType getCapabilities() { WFSCapabilitiesType capabilities = null; if (remoteWfs != null) { try { capabilities = remoteWfs.getCapabilities(new GetCapabilitiesRequest()); } catch (WfsException wfse) { LOGGER.warn(WFS_ERROR_MESSAGE, wfse); } catch (WebApplicationException wae) { handleWebApplicationException(wae); } } return capabilities; } private void configureWfsFeatures() { WFSCapabilitiesType capabilities = getCapabilities(); if (capabilities != null) { List<FeatureTypeType> featureTypes = getFeatureTypes(capabilities); buildFeatureFilters(featureTypes, capabilities.getFilterCapabilities()); } else { LOGGER.warn("WfsSource {}: WFS Server did not return any capabilities.", getId()); } } private List<FeatureTypeType> getFeatureTypes(WFSCapabilitiesType capabilities) { List<FeatureTypeType> featureTypes = capabilities.getFeatureTypeList().getFeatureType(); if (featureTypes.isEmpty()) { LOGGER.warn("\"WfsSource {}: No feature types found.", getId()); } return featureTypes; } private void updateSupportedSpatialOperators(SpatialOperatorsType spatialOperatorsType) { if (spatialOperatorsType == null) { return; } supportedSpatialOperators = spatialOperatorsType; if (NO_FORCED_SPATIAL_FILTER.equals(forceSpatialFilter)) { return; } SpatialOperatorsType forcedSpatialOpType = new SpatialOperatorsType(); SpatialOperatorType sot = new SpatialOperatorType(); sot.setName(forceSpatialFilter); forcedSpatialOpType.getSpatialOperator().add(sot); for (WfsFilterDelegate delegate : featureTypeFilters.values()) { delegate.setSpatialOps(forcedSpatialOpType); } } private void buildFeatureFilters(List<FeatureTypeType> featureTypes, FilterCapabilities filterCapabilities) { if (filterCapabilities == null) { return; } // Use local Map for metacardtype registrations and once they are populated with latest // MetacardTypes, then do actual registration Map<String, MetacardTypeRegistration> mcTypeRegs = new HashMap<String, MetacardTypeRegistration>(); this.featureTypeFilters.clear(); for (FeatureTypeType featureTypeType : featureTypes) { String ftSimpleName = featureTypeType.getName().getLocalPart(); if (mcTypeRegs.containsKey(ftSimpleName)) { LOGGER.debug( "WfsSource {}: MetacardType {} is already registered - skipping to next metacard type", getId(), ftSimpleName); continue; } LOGGER.debug("ftName: {}", ftSimpleName); try { XmlSchema schema = remoteWfs.describeFeatureType( new DescribeFeatureTypeRequest(featureTypeType.getName())); if ((schema != null)) { // Update local map with enough info to create actual MetacardType registrations // later MetacardTypeRegistration registration = createFeatureMetacardTypeRegistration( featureTypeType, ftSimpleName, schema); mcTypeRegs.put(ftSimpleName, registration); FeatureMetacardType featureMetacardType = registration.getFtMetacard(); lookupFeatureConverter(ftSimpleName, featureMetacardType); MetacardMapper metacardAttributeToFeaturePropertyMapper = lookupMetacardAttributeToFeaturePropertyMapper( featureMetacardType.getFeatureType()); this.featureTypeFilters.put(featureMetacardType.getFeatureType(), new WfsFilterDelegate(featureMetacardType, filterCapabilities, registration.getSrs(), metacardAttributeToFeaturePropertyMapper, coordinateOrder)); } } catch (WfsException wfse) { LOGGER.warn(WFS_ERROR_MESSAGE, wfse); } catch (WebApplicationException wae) { handleWebApplicationException(wae); } catch (IllegalArgumentException ie) { LOGGER.warn(WFS_ERROR_MESSAGE, ie); } } registerFeatureMetacardTypes(mcTypeRegs); if (featureTypeFilters.isEmpty()) { LOGGER.warn("Wfs Source {}: No Feature Type schemas validated.", getId()); } LOGGER.debug("Wfs Source {}: Number of validated Features = {}", getId(), featureTypeFilters.size()); updateSupportedSpatialOperators( filterCapabilities.getSpatialCapabilities().getSpatialOperators()); } private void registerFeatureMetacardTypes(Map<String, MetacardTypeRegistration> mcTypeRegs) { // Unregister all MetacardType services - the DescribeFeatureTypeRequest should // have returned all of the most current metacard types that will now be registered. // As Source(s) are added/removed from this instance or to other Source(s) // that this instance is federated to, the list of metacard types will change. // This is done here vs. inside the above loop so that minimal time is spent clearing and // registering the MetacardTypes - the concern is that if this registration is too lengthy // a query could come in that is handled while the MetacardType registrations are // in a state of flux. unregisterAllMetacardTypes(); if (!mcTypeRegs.isEmpty()) { for (MetacardTypeRegistration registration : mcTypeRegs.values()) { FeatureMetacardType ftMetacard = registration.getFtMetacard(); String simpleName = ftMetacard.getFeatureType().getLocalPart(); ServiceRegistration serviceRegistration = context .registerService(MetacardType.class.getName(), ftMetacard, registration.getProps()); this.metacardTypeServiceRegistrations.put(simpleName, serviceRegistration); } } } private void lookupFeatureConverter(String ftSimpleName, FeatureMetacardType ftMetacard) { FeatureConverter featureConverter = null; /** * The list of feature converter factories injected into this class is a live list. So, feature converter factories * can be added and removed from the system while running. */ if (CollectionUtils.isNotEmpty(featureConverterFactories)) { for (FeatureConverterFactory factory : featureConverterFactories) { if (ftSimpleName.equalsIgnoreCase(factory.getFeatureType())) { featureConverter = factory.createConverter(); break; } } } // Found a specific feature converter if (featureConverter != null) { LOGGER.debug("WFS Source {}: Features of type: {} will be converted using {}", getId(), ftSimpleName, featureConverter.getClass().getSimpleName()); } else { LOGGER.warn( "WfsSource {}: Unable to find a feature specific converter; {} will be converted using the GenericFeatureConverter", getId(), ftSimpleName); // Since we have no specific converter, we will check to see if we have a mapper to do // feature property to metacard attribute mappings. MetacardMapper featurePropertyToMetacardAttributeMapper = lookupMetacardAttributeToFeaturePropertyMapper( ftMetacard.getFeatureType()); if (featurePropertyToMetacardAttributeMapper != null) { featureConverter = new GenericFeatureConverterWfs20( featurePropertyToMetacardAttributeMapper); LOGGER.debug( "WFS Source {}: Created {} for feature type {} with feature property to metacard attribute mapper.", getId(), featureConverter.getClass().getSimpleName(), ftSimpleName); } else { featureConverter = new GenericFeatureConverterWfs20(); LOGGER.debug( "WFS Source {}: Created {} for feature type {} with no feature property to metacard attribute mapper.", getId(), featureConverter.getClass().getSimpleName(), ftSimpleName); } } featureConverter.setSourceId(getId()); featureConverter.setMetacardType(ftMetacard); featureConverter.setWfsUrl(wfsUrl); featureConverter.setCoordinateOrder(coordinateOrder); // Add the Feature Type name as an alias for xstream LOGGER.debug("Registering feature converter {} for feature type {}.", featureConverter.getClass().getSimpleName(), ftSimpleName); remoteWfs.getFeatureCollectionReader().registerConverter(featureConverter); } private MetacardMapper lookupMetacardAttributeToFeaturePropertyMapper(QName featureType) { MetacardMapper metacardAttributeToFeaturePropertyMapper = null; if (this.metacardToFeatureMappers != null) { for (MetacardMapper mapper : this.metacardToFeatureMappers) { if (mapper != null && StringUtils .equals(mapper.getFeatureType(), featureType.toString())) { LOGGER.debug("Found {} for feature type {}.", MetacardMapper.class.getSimpleName(), featureType.toString()); metacardAttributeToFeaturePropertyMapper = mapper; break; } } if (metacardAttributeToFeaturePropertyMapper == null) { LOGGER.warn("Unable to find a {} for feature type {}.", MetacardMapper.class.getSimpleName(), featureType.toString()); } } return metacardAttributeToFeaturePropertyMapper; } private MetacardTypeRegistration createFeatureMetacardTypeRegistration( FeatureTypeType featureTypeType, String ftName, XmlSchema schema) { FeatureMetacardType ftMetacard = new FeatureMetacardType(schema, featureTypeType.getName(), nonQueryableProperties != null ? Arrays.asList(nonQueryableProperties) : new ArrayList<String>(), Wfs20Constants.GML_3_2_NAMESPACE); Dictionary<String, Object> props = new Hashtable<String, Object>(); props.put(Metacard.CONTENT_TYPE, new String[] {ftName}); LOGGER.debug("WfsSource {}: Registering MetacardType: {}", getId(), ftName); return new MetacardTypeRegistration(ftMetacard, props, featureTypeType.getDefaultCRS()); } @Override public boolean isAvailable() { return availabilityTask.isAvailable(); } @Override public boolean isAvailable(SourceMonitor callback) { this.sourceMonitors.add(callback); return isAvailable(); } @Override public SourceResponse query(QueryRequest request) throws UnsupportedQueryException { Query query = request.getQuery(); if (query == null) { LOGGER.error("WFS Source {}: Incoming query is null.", getId()); return null; } LOGGER.debug("WFS Source {}: Received query: \n{}", getId(), query); SourceResponseImpl simpleResponse = null; GetFeatureType getFeature = buildGetFeatureRequest(query); try { LOGGER.debug("WFS Source {}: Sending query ...", getId()); Wfs20FeatureCollection featureCollection = remoteWfs.getFeature(getFeature); int numResults = -1; if (featureCollection == null) { throw new UnsupportedQueryException("Invalid results returned from server"); } numResults = featureCollection.getMembers().size(); if (featureCollection.getNumberReturned() == null) { LOGGER.warn("Number Returned Attribute was not added to the response"); } else if (!featureCollection.getNumberReturned().equals(numResults)) { LOGGER.warn( "Number Returned Attribute ({}) did not match actual number returned ({})", featureCollection.getNumberReturned(), numResults); } if (numResults > -1) { availabilityTask.updateLastAvailableTimestamp(System.currentTimeMillis()); LOGGER.debug("WFS Source {}: Received featureCollection with {} metacards.", getId(), numResults); List<Result> results = new ArrayList<Result>(numResults); for (int i = 0; i < numResults; i++) { Metacard mc = featureCollection.getMembers().get(i); mc = transform(mc, DEFAULT_WFS_TRANSFORMER_ID); Result result = new ResultImpl(mc); results.add(result); debugResult(result); } //Fetch total results available Long totalResults = new Long(0); if (featureCollection.getNumberMatched() == null) { totalResults = Long.valueOf(numResults); } else if (featureCollection.getNumberMatched().equals(UNKNOWN)) { totalResults = Long.valueOf(numResults); } else if (StringUtils.isNumeric(featureCollection.getNumberMatched())) { totalResults = Long.parseLong(featureCollection.getNumberMatched()); } simpleResponse = new SourceResponseImpl(request, results, totalResults); } else { throw new UnsupportedQueryException( "The number of features returned is a negative number"); } } catch (WfsException wfse) { LOGGER.warn(WFS_ERROR_MESSAGE, wfse); throw new UnsupportedQueryException("Error received from WFS Server", wfse); } catch (Exception ce) { String msg = handleClientException(ce); throw new UnsupportedQueryException(msg, ce); } return simpleResponse; } protected GetFeatureType buildGetFeatureRequest(Query query) throws UnsupportedQueryException { List<ContentType> contentTypes = getContentTypesFromQuery(query); List<QueryType> queries = new ArrayList<QueryType>(); for (Entry<QName, WfsFilterDelegate> filterDelegateEntry : featureTypeFilters.entrySet()) { if (contentTypes.isEmpty() || isFeatureTypeInQuery(contentTypes, filterDelegateEntry.getKey().getLocalPart())) { QueryType wfsQuery = new QueryType(); String typeName = null; if (StringUtils.isEmpty(filterDelegateEntry.getKey().getPrefix())) { typeName = filterDelegateEntry.getKey().getLocalPart(); } else { typeName = filterDelegateEntry.getKey().getPrefix() + ":" + filterDelegateEntry.getKey().getLocalPart(); } wfsQuery.setTypeNames(Arrays.asList(typeName)); wfsQuery.setHandle(filterDelegateEntry.getKey().getLocalPart()); FilterType filter = filterAdapter.adapt(query, filterDelegateEntry.getValue()); if (filter != null) { if (areAnyFiltersSet(filter)) { wfsQuery.setAbstractSelectionClause( new net.opengis.filter.v_2_0_0.ObjectFactory() .createFilter(filter)); } if (!this.disableSorting) { if (query.getSortBy() != null) { SortOrder sortOrder = query.getSortBy().getSortOrder(); if (filterDelegateEntry.getValue().isSortingSupported() && filterDelegateEntry.getValue().getAllowedSortOrders() .contains(sortOrder)) { JAXBElement<SortByType> sortBy = buildSortBy( filterDelegateEntry.getKey(), query.getSortBy()); if (sortBy != null) { LOGGER.debug("Sorting using sort order of [{}].", sortOrder.identifier()); wfsQuery.setAbstractSortingClause(sortBy); } } else if (filterDelegateEntry.getValue().isSortingSupported() && CollectionUtils.isEmpty( filterDelegateEntry.getValue().getAllowedSortOrders())) { JAXBElement<SortByType> sortBy = buildSortBy( filterDelegateEntry.getKey(), query.getSortBy()); if (sortBy != null) { LOGGER.debug( "No sort orders defined in getCapabilities. Attempting to sort using sort order of [{}].", sortOrder.identifier()); wfsQuery.setAbstractSortingClause(sortBy); } } else if (filterDelegateEntry.getValue().isSortingSupported() && !filterDelegateEntry.getValue().getAllowedSortOrders() .contains(sortOrder)) { LOGGER.warn( "Unsupported sort order of [{}]. Supported sort orders are {}.", sortOrder, filterDelegateEntry.getValue().getAllowedSortOrders()); } else if (!filterDelegateEntry.getValue().isSortingSupported()) { LOGGER.debug("Sorting is not supported."); } } } else { LOGGER.warn("Sorting is disabled."); } queries.add(wfsQuery); } else { LOGGER.debug("WFS Source {}: {} has an invalid filter.", getId(), filterDelegateEntry.getKey()); } } } if (queries != null && !queries.isEmpty()) { GetFeatureType getFeatureType = new GetFeatureType(); int pageSize = query.getPageSize(); if (pageSize < 0) { LOGGER.error("Page size has a negative value"); throw new UnsupportedQueryException( "Unable to build query. Page size has a negative value."); } int startIndex = query.getStartIndex(); if (startIndex < 0) { LOGGER.error("Start index has a negative value"); throw new UnsupportedQueryException( "Unable to build query. Start index has a negative value."); } else if (startIndex != 0) { //Convert DDF index of 1 back to index of 0 for WFS 2.0 startIndex = query.getStartIndex() - 1; } else { LOGGER.debug("Query already has a start index of 0"); } getFeatureType.setCount(BigInteger.valueOf(query.getPageSize())); getFeatureType.setStartIndex(BigInteger.valueOf(startIndex)); List<JAXBElement<?>> incomingQueries = getFeatureType.getAbstractQueryExpression(); for (QueryType queryType : queries) { incomingQueries .add(new net.opengis.wfs.v_2_0_0.ObjectFactory().createQuery(queryType)); } logMessage(getFeatureType); return getFeatureType; } else { throw new UnsupportedQueryException( "Unable to build query. No filters could be created from query criteria."); } } private JAXBElement<SortByType> buildSortBy(QName featureType, SortBy incomingSortBy) throws UnsupportedQueryException { net.opengis.filter.v_2_0_0.ObjectFactory filterObjectFactory = new net.opengis.filter.v_2_0_0.ObjectFactory(); String propertyName = mapSortByPropertyName(featureType, incomingSortBy.getPropertyName().getPropertyName()); if (propertyName != null) { SortOrder sortOrder = incomingSortBy.getSortOrder(); SortPropertyType sortPropertyType = filterObjectFactory.createSortPropertyType(); sortPropertyType.setValueReference(propertyName); if (SortOrder.ASCENDING.equals(sortOrder)) { sortPropertyType.setSortOrder(SortOrderType.ASC); } else if (SortOrder.DESCENDING.equals(sortOrder)) { sortPropertyType.setSortOrder(SortOrderType.DESC); } else { throw new UnsupportedQueryException( "Unable to build query. Unknown sort order of [" + sortOrder.identifier() + "]."); } SortByType sortByType = filterObjectFactory.createSortByType(); sortByType.getSortProperty().add(sortPropertyType); return filterObjectFactory.createSortBy(sortByType); } else { return null; } } /** * If a MetacardMapper cannot be found or there is no mapping for the incomingPropertyName, return null. * This will cause a query to be constructed without an AbstractSortingClause. */ private String mapSortByPropertyName(QName featureType, String incomingPropertyName) { MetacardMapper metacardToFeaturePropertyMapper = lookupMetacardAttributeToFeaturePropertyMapper( featureType); String mappedPropertyName = null; if (metacardToFeaturePropertyMapper != null) { if (StringUtils.equals(Result.TEMPORAL, incomingPropertyName) || StringUtils .equals(Metacard.EFFECTIVE, incomingPropertyName)) { mappedPropertyName = StringUtils.isNotBlank( metacardToFeaturePropertyMapper.getSortByTemporalFeatureProperty()) ? metacardToFeaturePropertyMapper.getSortByTemporalFeatureProperty() : null; } else if (StringUtils.equals(Result.RELEVANCE, incomingPropertyName)) { mappedPropertyName = StringUtils.isNotBlank( metacardToFeaturePropertyMapper.getSortByRelevanceFeatureProperty()) ? metacardToFeaturePropertyMapper.getSortByRelevanceFeatureProperty() : null; } else if (StringUtils.equals(Result.DISTANCE, incomingPropertyName)) { mappedPropertyName = StringUtils.isNotBlank( metacardToFeaturePropertyMapper.getSortByDistanceFeatureProperty()) ? metacardToFeaturePropertyMapper.getSortByDistanceFeatureProperty() : null; } else { mappedPropertyName = null; } } else { mappedPropertyName = null; } return mappedPropertyName; } private boolean areAnyFiltersSet(FilterType filter) { if (filter != null) { return (filter.isSetComparisonOps() || filter.isSetId() || filter.isSetLogicOps() || filter.isSetSpatialOps() || filter.isSetTemporalOps()); } else { return false; } } private boolean isFeatureTypeInQuery(final List<ContentType> contentTypes, final String featureTypeName) { for (ContentType contentType : contentTypes) { if (featureTypeName.equalsIgnoreCase(contentType.getName())) { return true; } } return false; } private Metacard transform(Metacard mc, String transformerId) { if (mc == null) { throw new IllegalArgumentException("Metacard is null"); } ServiceReference[] refs = null; try { refs = context.getServiceReferences(MetadataTransformer.class.getName(), "(" + Constants.SERVICE_ID + "=" + transformerId + ")"); } catch (InvalidSyntaxException e) { LOGGER.warn("Invalid transformer ID. Returning original metacard.", e); return mc; } if (refs == null || refs.length == 0) { LOGGER.debug("MetadataTransformer not found. Returning original metacard."); return mc; } else { try { MetadataTransformer transformer = (MetadataTransformer) context.getService(refs[0]); return transformer.transform(mc); } catch (CatalogTransformerException e) { LOGGER.warn( "Transformation Failed for transformer: {}. Returning original metacard", transformerId, e); return mc; } } } private List<ContentType> getContentTypesFromQuery(final Query query) { List<ContentType> contentTypes = null; try { contentTypes = filterAdapter.adapt(query, new ContentTypeFilterDelegate()); } catch (UnsupportedQueryException e) { LOGGER.warn("WFS Source {}: Unable to get content types from query.", getId(), e); } return contentTypes != null ? contentTypes : new ArrayList<ContentType>(); } private void unregisterAllMetacardTypes() { for (ServiceRegistration metacardTypeServiceRegistration : metacardTypeServiceRegistrations .values()) { if (metacardTypeServiceRegistration != null) { metacardTypeServiceRegistration.unregister(); } } metacardTypeServiceRegistrations.clear(); } @Override public Set<ContentType> getContentTypes() { Set<QName> typeNames = featureTypeFilters.keySet(); Set<ContentType> contentTypes = new HashSet<ContentType>(); for (QName featureName : typeNames) { contentTypes.add(new ContentTypeImpl(featureName.getLocalPart(), getVersion())); } return contentTypes; } @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; } @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 getDescription() { return describableProperties.getProperty(DESCRIPTION); } @Override public String getOrganization() { return describableProperties.getProperty(ORGANIZATION); } @Override public String getTitle() { return describableProperties.getProperty(TITLE); } @Override public String getVersion() { return describableProperties.getProperty(VERSION); } @Override public ResourceResponse retrieveResource(URI uri, Map<String, Serializable> arguments) throws IOException, ResourceNotFoundException, ResourceNotSupportedException { StringBuilder strBuilder = new StringBuilder(); strBuilder.append("<html><script type=\"text/javascript\">window.location.replace(\""); strBuilder.append(uri); strBuilder.append("\");</script></html>"); Resource resource = new ResourceImpl(IOUtils.toInputStream(strBuilder.toString()), MediaType.TEXT_HTML, getId() + " Resource"); return new ResourceResponseImpl(resource); } @Override public Set<String> getSupportedSchemes() { // TODO Auto-generated method stub - return null; } @Override public Set<String> getOptions(Metacard metacard) { // TODO Auto-generated method stub return null; } public String getWfsUrl() { return wfsUrl; } public void setWfsUrl(String wfsUrl) { this.wfsUrl = wfsUrl; } public void setUsername(String username) { this.username = username; } public void setPassword(String password) { this.password = password; } public void setDisableCnCheck(Boolean disableCnCheck) { this.disableCnCheck = disableCnCheck; } public void setPollInterval(Integer interval) { this.pollInterval = interval; } public void setConnectionTimeout(Integer timeout) { this.connectionTimeout = timeout; } public void setReceiveTimeout(Integer timeout) { this.receiveTimeout = timeout; } public void setFilterAdapter(FilterAdapter filterAdapter) { this.filterAdapter = filterAdapter; } public void setRemoteWfs(RemoteWfs remoteWfs) { this.remoteWfs = remoteWfs; } public void setFilterDelgates(Map<QName, WfsFilterDelegate> delegates) { this.featureTypeFilters = delegates; } public void setContext(BundleContext context) { this.context = context; } public void setNonQueryableProperties(String[] newNonQueryableProperties) { if (newNonQueryableProperties == null) { this.nonQueryableProperties = new String[0]; } else { this.nonQueryableProperties = Arrays .copyOf(newNonQueryableProperties, newNonQueryableProperties.length); } } public String getForceSpatialFilter() { return forceSpatialFilter; } public void setForceSpatialFilter(String forceSpatialFilter) { this.forceSpatialFilter = forceSpatialFilter; } public void setFeatureConverterFactoryList(List<FeatureConverterFactory> factories) { this.featureConverterFactories = factories; } public List<MetacardMapper> getMetacardToFeatureMapper() { return this.metacardToFeatureMappers; } public void setMetacardToFeatureMapper(List<MetacardMapper> mappers) { this.metacardToFeatureMappers = mappers; } public void setCoordinateOrder(String coordinateOrder) { this.coordinateOrder = coordinateOrder; } public void setDisableSorting(boolean disableSorting) { this.disableSorting = disableSorting; } private String handleWebApplicationException(WebApplicationException wae) { Response response = wae.getResponse(); WfsException wfsException = new WfsResponseExceptionMapper().fromResponse(response); String msg = "Error received from WFS Server " + getId() + "\n" + wfsException.getMessage(); LOGGER.error(msg, wae); return msg; } private String handleClientException(Exception ce) { String msg = ""; if (ce.getCause() instanceof WebApplicationException) { msg = handleWebApplicationException((WebApplicationException) ce.getCause()); } else { msg = "Error received from WFS Server " + getId(); } LOGGER.warn(msg); return msg; } private void logMessage(GetFeatureType getFeature) { if (LOGGER.isDebugEnabled()) { try { StringWriter writer = new StringWriter(); String context = StringUtils.join(new String[] {Wfs20Constants.OGC_FILTER_PACKAGE, Wfs20Constants.OGC_GML_PACKAGE, Wfs20Constants.OGC_OWS_PACKAGE, Wfs20Constants.OGC_WFS_PACKAGE}, ":"); JAXBContext contextObj = JAXBContext .newInstance(context, WfsSource.class.getClassLoader()); Marshaller marshallerObj = contextObj.createMarshaller(); marshallerObj.setProperty(Marshaller.JAXB_FORMATTED_OUTPUT, true); marshallerObj.marshal( new net.opengis.wfs.v_2_0_0.ObjectFactory().createGetFeature(getFeature), writer); LOGGER.debug("WfsSource {}: {}", getId(), writer.toString()); } catch (JAXBException e) { LOGGER.debug("An error occurred debugging the GetFeature request", e); } } } private void debugResult(Result result) { if (LOGGER.isDebugEnabled()) { if (result != null && result.getMetacard() != null) { StringBuffer sb = new StringBuffer(); sb.append("\nid:\t" + result.getMetacard().getId()); sb.append("\nmetacardType:\t" + result.getMetacard().getMetacardType()); if (result.getMetacard().getMetacardType() != null) { sb.append("\nmetacardType name:\t" + result.getMetacard().getMetacardType() .getName()); } sb.append("\ncontentType:\t" + result.getMetacard().getContentTypeName()); sb.append("\ntitle:\t" + result.getMetacard().getTitle()); sb.append("\nsource:\t" + result.getMetacard().getSourceId()); sb.append("\nmetadata:\t" + result.getMetacard().getMetadata()); sb.append("\nlocation:\t" + result.getMetacard().getLocation()); LOGGER.debug("Transform complete. Metacard: {}", sb.toString()); } } } public void updateTimeouts() { if (remoteWfs != null) { remoteWfs.setTimeouts(connectionTimeout, receiveTimeout); } } public void setSecuritySettings(SecuritySettingsService securitySettings) { this.securitySettingsService = securitySettings; } private class MetacardTypeRegistration { private FeatureMetacardType ftMetacard; private Dictionary<String, Object> props; private String srs; public MetacardTypeRegistration(FeatureMetacardType ftMetacard, Dictionary<String, Object> props, String srs) { this.ftMetacard = ftMetacard; this.props = props; this.srs = srs; } public FeatureMetacardType getFtMetacard() { return ftMetacard; } public Dictionary<String, Object> getProps() { return props; } public String getSrs() { return srs; } } /** * Callback class to check the Availability of the WfsSource. * * 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 WfsSourceAvailabilityCommand implements AvailabilityCommand { public WfsSourceAvailabilityCommand() { } @Override public boolean isAvailable() { LOGGER.debug("Checking availability for source {} ", getId()); boolean oldAvailability = WfsSource.this.isAvailable(); boolean newAvailability = false; // If the Remote object is null attempt to initialize it and // configure // all the capabilities. if (remoteWfs == null) { connectToRemoteWfs(); } // Simple "ping" to ensure the source is responding newAvailability = (null != getCapabilities()); // If the source becomes available, configure it. // When the source is available, we need to account for new feature converter factories being added // while the system is running. if (newAvailability) { LOGGER.debug("WFS Source {} is available...configuring.", getId()); configureWfsFeatures(); newAvailability = !featureTypeFilters.isEmpty(); } if (oldAvailability != newAvailability) { availabilityChanged(newAvailability); } return newAvailability; } } }