/** * 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.v1_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.ConnectException; import java.net.URI; import java.util.ArrayList; import java.util.Arrays; 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.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.JAXBException; import javax.xml.bind.Marshaller; import javax.xml.namespace.QName; import org.apache.commons.io.IOUtils; import org.apache.commons.lang.StringUtils; import org.apache.cxf.common.util.CollectionUtils; import org.apache.cxf.jaxrs.provider.JAXBElementProvider; import org.apache.ws.commons.schema.XmlSchema; import org.codice.ddf.cxf.SecureCxfClientFactory; 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.wfs.catalog.common.FeatureMetacardType; import org.codice.ddf.spatial.ogc.wfs.catalog.common.WfsException; import org.codice.ddf.spatial.ogc.wfs.catalog.common.WfsFeatureCollection; import org.codice.ddf.spatial.ogc.wfs.catalog.converter.FeatureConverter; import org.codice.ddf.spatial.ogc.wfs.catalog.converter.impl.GenericFeatureConverter; import org.codice.ddf.spatial.ogc.wfs.catalog.source.MarkableStreamInterceptor; import org.codice.ddf.spatial.ogc.wfs.v1_0_0.catalog.common.DescribeFeatureTypeRequest; import org.codice.ddf.spatial.ogc.wfs.v1_0_0.catalog.common.GetCapabilitiesRequest; import org.codice.ddf.spatial.ogc.wfs.v1_0_0.catalog.common.Wfs; import org.codice.ddf.spatial.ogc.wfs.v1_0_0.catalog.common.Wfs10Constants; import org.codice.ddf.spatial.ogc.wfs.v1_0_0.catalog.converter.FeatureConverterFactory; import org.codice.ddf.spatial.ogc.wfs.v1_0_0.catalog.source.reader.FeatureCollectionMessageBodyReaderWfs10; import org.codice.ddf.spatial.ogc.wfs.v1_0_0.catalog.source.reader.XmlSchemaMessageBodyReaderWfs10; 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.QueryImpl; 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.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.encryption.EncryptionService; import ddf.security.service.SecurityServiceException; import edu.umd.cs.findbugs.annotations.SuppressFBWarnings; import ogc.schema.opengis.filter.v_1_0_0.FilterType; import ogc.schema.opengis.wfs.v_1_0_0.GetFeatureType; import ogc.schema.opengis.wfs.v_1_0_0.ObjectFactory; import ogc.schema.opengis.wfs.v_1_0_0.QueryType; import ogc.schema.opengis.wfs_capabilities.v_1_0_0.FeatureTypeType; import ogc.schema.opengis.wfs_capabilities.v_1_0_0.WFSCapabilitiesType; /** * Provides a Federated and Connected source implementation for OGC WFS servers. */ public class WfsSource extends MaskableImpl implements FederatedSource, ConnectedSource, ConfiguredService { 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 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 DEFAULT_WFS_TRANSFORMER_ID = "wfs"; private static final String POLL_INTERVAL_PROPERTY = "pollInterval"; public static final String DISABLE_CN_CHECK_PROPERTY = "disableCnCheck"; private static Properties describableProperties = new Properties(); private final EncryptionService encryptionService; static { try (InputStream properties = WfsSource.class.getResourceAsStream( DESCRIBABLE_PROPERTIES_FILE)) { describableProperties.load(properties); } catch (IOException e) { LOGGER.info(e.getMessage(), e); } } private String wfsUrl; private String wfsVersion; private Map<QName, WfsFilterDelegate> featureTypeFilters = new HashMap<QName, WfsFilterDelegate>(); private String username; private String password; private Boolean disableCnCheck = Boolean.FALSE; private FilterAdapter filterAdapter; 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 List<String> supportedGeoFilters; private ScheduledExecutorService scheduler; private ScheduledFuture<?> availabilityPollFuture; private AvailabilityTask availabilityTask; private Set<SourceMonitor> sourceMonitors = new HashSet<SourceMonitor>(); private SecureCxfClientFactory<Wfs> factory; protected String configurationPid; private String forcedFeatureType; private FeatureCollectionMessageBodyReaderWfs10 featureCollectionReader; public WfsSource(FilterAdapter filterAdapter, BundleContext context, AvailabilityTask task, SecureCxfClientFactory factory, EncryptionService encryptionService) throws SecurityServiceException { this.filterAdapter = filterAdapter; this.context = context; this.availabilityTask = task; this.factory = factory; this.encryptionService = encryptionService; initProviders(); configureWfsFeatures(); } public WfsSource(EncryptionService encryptionService) { // Required for bean creation scheduler = Executors.newSingleThreadScheduledExecutor(); this.encryptionService = encryptionService; } /** * Init is called when the bundle is initially configured. * <p/> * <p/> * The init process creates a RemoteWfs object using the connection parameters from the * configuration. */ public void init() { createClientFactory(); setupAvailabilityPoll(); } public void destroy(int code) { 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) throws SecurityServiceException { 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(DISABLE_CN_CHECK_PROPERTY); String id = (String) configuration.get(ID_PROPERTY); if(hasSourceIdChanged(id)) { super.setId(id); configureWfsFeatures(); } setConnectionTimeout((Integer) configuration.get(CONNECTION_TIMEOUT_PROPERTY)); setReceiveTimeout((Integer) configuration.get(RECEIVE_TIMEOUT_PROPERTY)); String[] nonQueryableProperties = (String[]) configuration.get(NON_QUERYABLE_PROPS_PROPERTY); this.nonQueryableProperties = nonQueryableProperties; Integer newPollInterval = (Integer) configuration.get(POLL_INTERVAL_PROPERTY); if (hasWfsUrlChanged(wfsUrl) || hasDisableCnCheck(disableCnCheckProp)) { this.wfsUrl = wfsUrl; this.password = encryptionService.decryptValue(password); this.username = username; this.disableCnCheck = disableCnCheckProp; createClientFactory(); configureWfsFeatures(); } else { // Only need to update the supportedGeos if we don't reconnect. String spatialFilter = (String) configuration.get(SPATIAL_FILTER_PROPERTY); if (!StringUtils.equals(forceSpatialFilter, spatialFilter)) { List<String> geoFilters = new ArrayList<String>(); if (NO_FORCED_SPATIAL_FILTER.equals(spatialFilter)) { geoFilters.addAll(supportedGeoFilters); } else { geoFilters.add(spatialFilter); } for (WfsFilterDelegate delegate : featureTypeFilters.values()) { delegate.setSupportedGeoFilters(geoFilters); } } } if (!pollInterval.equals(newPollInterval)) { LOGGER.debug("Poll Interval was changed for source {}.", getId()); setPollInterval(newPollInterval); availabilityPollFuture.cancel(true); setupAvailabilityPoll(); } } /* This method should only be called after all properties have been set. */ private void createClientFactory() { if (StringUtils.isNotBlank(username) && StringUtils.isNotBlank(password)) { factory = new SecureCxfClientFactory(wfsUrl, Wfs.class, initProviders(), new MarkableStreamInterceptor(), this.disableCnCheck, false, null, null, username, password); } else { factory = new SecureCxfClientFactory(wfsUrl, Wfs.class, initProviders(), new MarkableStreamInterceptor(), this.disableCnCheck, false); } } private List<? extends Object> initProviders() { // We need to tell the JAXBElementProvider to marshal the GetFeatureType // class as an element because it is missing the @XmlRootElement Annotation JAXBElementProvider<GetFeatureType> provider = new JAXBElementProvider<GetFeatureType>(); Map<String, String> jaxbClassMap = new HashMap<String, String>(); // Ensure a namespace is used when the GetFeature request is generated String expandedName = new QName(Wfs10Constants.WFS_NAMESPACE, Wfs10Constants.GET_FEATURE).toString(); jaxbClassMap.put(GetFeatureType.class.getName(), expandedName); provider.setJaxbElementClassMap(jaxbClassMap); provider.setMarshallAsJaxbElement(true); featureCollectionReader = new FeatureCollectionMessageBodyReaderWfs10(); return Arrays.asList(provider, new WfsResponseExceptionMapper(), new XmlSchemaMessageBodyReaderWfs10(), featureCollectionReader); } private boolean hasWfsUrlChanged(String wfsUrl) { return !StringUtils.equals(this.wfsUrl, wfsUrl); } private boolean hasSourceIdChanged(String id) { return !StringUtils.equals(getId(), id); } private boolean hasPasswordChanged(String password) { return !StringUtils.equals(this.password, password); } private boolean hasUsernameChanged(String username) { return !StringUtils.equals(this.username, username); } @SuppressFBWarnings("RC_REF_COMPARISON_BAD_PRACTICE_BOOLEAN") private boolean hasDisableCnCheck(Boolean disableCnCheck) { return this.disableCnCheck != disableCnCheck; } 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 availabilityChanged(boolean isAvailable) { if (isAvailable) { LOGGER.info("WFS source {} is available.", getId()); } else { LOGGER.info("WFS source {} is unavailable.", getId()); } 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() throws SecurityServiceException { WFSCapabilitiesType capabilities = null; Wfs wfs = factory.getClient(); try { capabilities = wfs.getCapabilities(new GetCapabilitiesRequest()); } catch (WfsException wfse) { LOGGER.info(WFS_ERROR_MESSAGE + " Received HTTP code '{}' from server for source with id='{}'", wfse.getHttpStatus(), getId()); LOGGER.debug(WFS_ERROR_MESSAGE, wfse); } catch (WebApplicationException wae) { LOGGER.debug(handleWebApplicationException(wae), wae); } catch (Exception e) { handleClientException(e); } return capabilities; } private void configureWfsFeatures() throws SecurityServiceException { WFSCapabilitiesType capabilities = getCapabilities(); if (capabilities != null) { wfsVersion = capabilities.getVersion(); List<FeatureTypeType> featureTypes = getFeatureTypes(capabilities); List<String> supportedGeo = getSupportedGeo(capabilities); buildFeatureFilters(featureTypes, supportedGeo); } else { LOGGER.info("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.debug("\"WfsSource {}: No feature types found.", getId()); } return featureTypes; } private List<String> getSupportedGeo(WFSCapabilitiesType capabilities) { supportedGeoFilters = new ArrayList<String>(); List<Object> geoTypes = capabilities.getFilterCapabilities() .getSpatialCapabilities() .getSpatialOperators() .getBBOXOrEqualsOrDisjoint(); for (Object geoType : geoTypes) { supportedGeoFilters.add(geoType.getClass() .getSimpleName()); } if (!NO_FORCED_SPATIAL_FILTER.equals(forceSpatialFilter)) { return Arrays.asList(forceSpatialFilter); } return supportedGeoFilters; } private void buildFeatureFilters(List<FeatureTypeType> featureTypes, List<String> supportedGeo) throws SecurityServiceException { // 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>(); Wfs wfs = factory.getClient(); for (FeatureTypeType featureTypeType : featureTypes) { String ftName = featureTypeType.getName() .getLocalPart(); if (StringUtils.isNotBlank(forcedFeatureType) && !StringUtils.equals(forcedFeatureType, ftName)) { continue; } if (mcTypeRegs.containsKey(ftName)) { LOGGER.debug( "WfsSource {}: MetacardType {} is already registered - skipping to next metacard type", getId(), ftName); continue; } LOGGER.debug("ftName: {}", ftName); try { XmlSchema schema = wfs.describeFeatureType(new DescribeFeatureTypeRequest( featureTypeType.getName())); if ((schema != null)) { FeatureMetacardType ftMetacard = new FeatureMetacardType(schema, featureTypeType.getName(), nonQueryableProperties != null ? Arrays.asList(nonQueryableProperties) : new ArrayList<String>(), Wfs10Constants.GML_NAMESPACE); Dictionary<String, Object> props = new Hashtable<String, Object>(); props.put(Metacard.CONTENT_TYPE, new String[] {ftName}); LOGGER.debug("WfsSource {}: Registering MetacardType: {}", getId(), ftName); // Update local map with enough info to create actual MetacardType registrations // later mcTypeRegs.put(ftName, new MetacardTypeRegistration(ftMetacard, props, featureTypeType.getSRS())); FeatureConverter featureConverter = null; if (!CollectionUtils.isEmpty(featureConverterFactories)) { for (FeatureConverterFactory factory : featureConverterFactories) { if (ftName.equalsIgnoreCase(factory.getFeatureType())) { featureConverter = factory.createConverter(); LOGGER.debug( "WFS Source {}: Features of type: {} will be converted using {}", getId(), ftName, featureConverter.getClass() .getSimpleName()); break; } } if (featureConverter == null) { LOGGER.debug( "WfsSource {}: Unable to find a feature specific converter; {} will be converted using the GenericFeatureConverter", getId(), ftName); featureConverter = new GenericFeatureConverter(featureTypeType.getSRS()); } } else { LOGGER.debug( "WfsSource {}: Unable to find a feature specific converter; {} will be converted using the GenericFeatureConverter", getId(), ftName); featureConverter = new GenericFeatureConverter(featureTypeType.getSRS()); } featureConverter.setSourceId(getId()); featureConverter.setMetacardType(ftMetacard); featureConverter.setWfsUrl(wfsUrl); // Add the Feature Type name as an alias for xstream featureCollectionReader.registerConverter(featureConverter); } } catch (WfsException | IllegalArgumentException wfse) { LOGGER.debug(WFS_ERROR_MESSAGE, wfse); } catch (WebApplicationException wae) { LOGGER.debug(handleWebApplicationException(wae), wae); } } // 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(); this.featureTypeFilters.clear(); if (!mcTypeRegs.isEmpty()) { Set<Entry<String, MetacardTypeRegistration>> entries = mcTypeRegs.entrySet(); for (Map.Entry<String, MetacardTypeRegistration> entry : mcTypeRegs.entrySet()) { MetacardTypeRegistration mcTypeReg = entry.getValue(); FeatureMetacardType ftMetacard = mcTypeReg.getFtMetacard(); ServiceRegistration serviceRegistration = context.registerService(MetacardType.class.getName(), ftMetacard, mcTypeReg.getProps()); this.metacardTypeServiceRegistrations.put(entry.getKey(), serviceRegistration); this.featureTypeFilters.put(ftMetacard.getFeatureType(), new WfsFilterDelegate(ftMetacard, supportedGeo, mcTypeReg.getSrs())); } } if (featureTypeFilters.isEmpty()) { LOGGER.info( "Wfs Source {}: No Feature Type schemas validated. Marking source as unavailable", getId()); } LOGGER.debug("Wfs Source {}: Number of validated Features = {}", getId(), featureTypeFilters.size()); } @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 { Wfs wfs = factory.getClient(); Query query = request.getQuery(); LOGGER.debug("WFS Source {}: Received query: \n{}", getId(), query); if (query.getStartIndex() < 1) { throw new UnsupportedQueryException( "Start Index is one-based and must be an integer greater than 0; should not be [" + query.getStartIndex() + "]"); } SourceResponseImpl simpleResponse = null; // WFS v1.0 specification does not support response indicating total // number // of features satisfying query constraints. // Hence, we save off the original // page size from the query request and create a copy of the query, // changing // the page size by a multiplier and the current page number of results // so that // more features are returned as the user pages through the results, // getting // a better sense of how many total features exist that satisfy the // query. int origPageSize = query.getPageSize(); if (origPageSize <= 0 || origPageSize > WFS_MAX_FEATURES_RETURNED) { origPageSize = WFS_MAX_FEATURES_RETURNED; } QueryImpl modifiedQuery = new QueryImpl(query); // Determine current page number of results being requested. // Example: startIndex = 21 and origPageSize=10, then requesting to go // to page number 3. // Note: Integer division will truncate remainders so 4 / 2 will return 0 and not .5. Also, // pages are numbered 1 - N so we add 1 to the result int pageNumber = query.getStartIndex() / origPageSize + 1; // Modified page size is based on current page number and a constant // multiplier, // but limited to a max value to prevent time consuming queries just to // get an // approximation of total number of features. // So as page number increases the pageSize increases. // Example: // pageNumber=2, modifiedPageSize=60 // pageNumber=3, modifiedPageSize=90 int modifiedPageSize = Math.min(pageNumber * origPageSize * WFS_QUERY_PAGE_SIZE_MULTIPLIER, WFS_MAX_FEATURES_RETURNED); LOGGER.debug("WFS Source {}: modified page size = {}", getId(), modifiedPageSize); modifiedQuery.setPageSize(modifiedPageSize); GetFeatureType getFeature = buildGetFeatureRequest(modifiedQuery); try { LOGGER.debug("WFS Source {}: Sending query ...", getId()); WfsFeatureCollection featureCollection = wfs.getFeature(getFeature); if (featureCollection == null) { throw new UnsupportedQueryException("Invalid results returned from server"); } availabilityTask.updateLastAvailableTimestamp(System.currentTimeMillis()); LOGGER.debug("WFS Source {}: Received featureCollection with {} metacards.", getId(), featureCollection.getFeatureMembers() .size()); // Only return the number of results originally asked for in the // query, or the entire list of results if it is smaller than the // original page size. int numberOfResultsToReturn = Math.min(origPageSize, featureCollection.getFeatureMembers() .size()); List<Result> results = new ArrayList<Result>(numberOfResultsToReturn); int stopIndex = Math.min((origPageSize * pageNumber) + query.getStartIndex(), featureCollection.getFeatureMembers() .size() + 1); LOGGER.debug( "WFS Source {}: startIndex = {}, stopIndex = {}, origPageSize = {}, pageNumber = {}", getId(), query.getStartIndex(), stopIndex, origPageSize, pageNumber); for (int i = query.getStartIndex(); i < stopIndex; i++) { Metacard mc = featureCollection.getFeatureMembers() .get(i - 1); mc = transform(mc, DEFAULT_WFS_TRANSFORMER_ID); Result result = new ResultImpl(mc); results.add(result); debugResult(result); } Long totalHits = (long) featureCollection.getFeatureMembers() .size(); simpleResponse = new SourceResponseImpl(request, results, totalHits); } catch (WfsException wfse) { LOGGER.debug(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; } private 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(); wfsQuery.setTypeName(filterDelegateEntry.getKey()); FilterType filter = filterAdapter.adapt(query, filterDelegateEntry.getValue()); if (filter != null) { if (areAnyFiltersSet(filter)) { wfsQuery.setFilter(filter); } queries.add(wfsQuery); } else { LOGGER.debug("WFS Source {}: {} has an invalid filter.", getId(), filterDelegateEntry.getKey()); } } } if (queries != null && !queries.isEmpty()) { GetFeatureType getFeatureType = new GetFeatureType(); getFeatureType.setMaxFeatures(BigInteger.valueOf(query.getPageSize())); getFeatureType.getQuery() .addAll(queries); getFeatureType.setService(Wfs10Constants.WFS); getFeatureType.setVersion(Wfs10Constants.VERSION_1_0_0); logMessage(getFeatureType); return getFeatureType; } else { throw new UnsupportedQueryException( "Unable to build query. No filters could be created from query criteria."); } } private boolean areAnyFiltersSet(FilterType filter) { if (filter != null) { return (filter.isSetComparisonOps() || filter.isSetFeatureId() || filter.isSetLogicOps() || filter.isSetSpatialOps()); } 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.debug("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.debug("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.debug("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() { if (StringUtils.isNotBlank(wfsVersion)) { return wfsVersion; } 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 = encryptionService.decryptValue(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 Integer getConnectionTimeout() { return this.connectionTimeout; } public void setReceiveTimeout(Integer timeout) { this.receiveTimeout = timeout; } public Integer getReceiveTimeout() { return this.receiveTimeout; } public void setFilterAdapter(FilterAdapter filterAdapter) { this.filterAdapter = filterAdapter; } 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; } private String handleWebApplicationException(WebApplicationException wae) { Response response = wae.getResponse(); WfsException wfsException = new WfsResponseExceptionMapper().fromResponse(response); return "Error received from WFS Server " + getId() + "\n" + wfsException.getMessage(); } private String handleClientException(Exception ce) { String msg = ""; Throwable cause = ce.getCause(); String sourceId = getId(); if (cause instanceof WebApplicationException) { msg = handleWebApplicationException((WebApplicationException) cause); } else if (cause instanceof IllegalArgumentException) { msg = WFS_ERROR_MESSAGE + " Source '" + sourceId + "'. The URI '" + getWfsUrl() + "' does not specify a valid protocol or could not be correctly parsed. " + ce.getMessage(); } else if (cause instanceof SSLHandshakeException) { msg = WFS_ERROR_MESSAGE + " Source '" + sourceId + "' with URL '" + getWfsUrl() + "': " + ce.getMessage(); } else if (cause instanceof ConnectException) { msg = WFS_ERROR_MESSAGE + " Source '" + sourceId + "' may not be running.\n" + ce.getMessage(); } else { msg = WFS_ERROR_MESSAGE + " Source '" + sourceId + "'\n" + ce; } LOGGER.info(msg); LOGGER.debug(msg, ce); return msg; } private void logMessage(GetFeatureType getFeature) { if (LOGGER.isDebugEnabled()) { try { StringWriter writer = new StringWriter(); JAXBContext contextObj = JAXBContext.newInstance(GetFeatureType.class); Marshaller marshallerObj = contextObj.createMarshaller(); marshallerObj.setProperty(Marshaller.JAXB_FORMATTED_OUTPUT, true); marshallerObj.marshal(new 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()); } } } private static 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; } } @Override public String getConfigurationPid() { return configurationPid; } @Override public void setConfigurationPid(String configurationPid) { this.configurationPid = configurationPid; } /** * Callback class to check the Availability of the WfsSource. * <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. * * @author kcwire */ 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; try { // Simple "ping" to ensure the source is responding newAvailability = (null != getCapabilities()); if (oldAvailability != newAvailability) { availabilityChanged(newAvailability); // If the source becomes available, configure it. if (newAvailability) { configureWfsFeatures(); } } } catch (SecurityServiceException sse) { LOGGER.info("Could not get a client to connect to the endpointUrl.", sse); } return newAvailability; } } public void setForcedFeatureType(String featureType) { this.forcedFeatureType = featureType; } }