/* * GeoTools - The Open Source Java GIS Toolkit * http://geotools.org * * (C) 2004-2008, Open Source Geospatial Foundation (OSGeo) * * This library 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; * version 2.1 of the License. * * This library 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. */ package org.geotools.data.view; import java.io.IOException; import java.net.URI; import java.net.URISyntaxException; import java.util.HashSet; import java.util.LinkedList; import java.util.List; import java.util.Set; import java.util.logging.Logger; import org.geotools.data.DataSourceException; import org.geotools.data.DataStore; import org.geotools.data.DataUtilities; import org.geotools.data.DefaultQuery; import org.geotools.data.FeatureListener; import org.geotools.data.FeatureLocking; import org.geotools.data.FeatureSource; import org.geotools.data.FeatureStore; import org.geotools.data.Query; import org.geotools.data.QueryCapabilities; import org.geotools.data.ResourceInfo; import org.geotools.data.crs.ForceCoordinateSystemFeatureResults; import org.geotools.data.crs.ReprojectFeatureResults; import org.geotools.data.simple.SimpleFeatureCollection; import org.geotools.data.simple.SimpleFeatureSource; import org.geotools.factory.CommonFactoryFinder; import org.geotools.feature.SchemaException; import org.geotools.geometry.jts.ReferencedEnvelope; import org.opengis.feature.simple.SimpleFeatureType; import org.opengis.feature.type.Name; import org.opengis.filter.Filter; import org.opengis.filter.FilterFactory; import org.opengis.referencing.crs.CoordinateReferenceSystem; /** * Wrapper for SimpleFeatureSource constrained by a Query. * * <p> * Support SimpleFeatureSource decorator that takes care of mapping a Query & * SimpleFeatureSource with the schema and definition query configured for it. * </p> * * <p> * Because GeoServer requires that attributes always be returned in the same * order we need a way to smoothly inforce this. Could we use this class to do * so? * </p> * <p> * WARNING: this class is a placeholder for ideas right now - it may not always * impement FeatureSource. * </p> * * @author Gabriel Rold�n * * @source $URL$ */ public class DefaultView implements SimpleFeatureSource { /** Shared package logger */ private static final Logger LOGGER = org.geotools.util.logging.Logging.getLogger("org.geotools.data.view"); /** SimpleFeatureSource being served up */ protected SimpleFeatureSource source; /** * Schema generated by provided constraintQuery */ private SimpleFeatureType schema; /** Query provided as a constraint */ private Query constraintQuery; /** * Creates a new GeoServerFeatureSource object. * <p> * Grabs the following from query: * <ul> * <li>typeName - only used if client does not supply * <li>cs - only used if client does not supply * <li>csForce - only used if client does not supply * <li>filter - combined with client filter * <li>propertyNames - combined with client filter (indicate property names * that *must* be included) * </ul> * </p> * Schema is generated based on this information. * </p> * * @param source * a FeatureSource * @param query * Filter used to limit results * @throws SchemaException */ public DefaultView(SimpleFeatureSource source, Query query) throws SchemaException { this.source = source; this.constraintQuery = query; SimpleFeatureType origionalType = source.getSchema(); CoordinateReferenceSystem cs = null; if (query.getCoordinateSystemReproject() != null) { cs = query.getCoordinateSystemReproject(); } else if (query.getCoordinateSystem() != null) { cs = query.getCoordinateSystem(); } schema = DataUtilities.createSubType(origionalType, query.getPropertyNames(), cs, query .getTypeName(), null); } /** * @see FeatureSource#getName() * @since 2.5 */ public Name getName() { return getSchema().getName(); } /** * Factory that make the correct decorator for the provided featureSource. * * <p> * This factory method is public and will be used to create all required * subclasses. By comparison the constructors for this class have package * visibiliy. * </p> * * TODO: revisit this - I am not sure I want write access to views * (especially if they do reprojection). * * @param source * @param query * * @return @throws * SchemaException */ public static SimpleFeatureSource create( SimpleFeatureSource source, Query query) throws SchemaException { if (source instanceof FeatureLocking) { // return new GeoServerFeatureLocking((FeatureLocking<SimpleFeatureType, SimpleFeature>) source, // schema, definitionQuery); } else if (source instanceof FeatureStore) { //return new GeoServerFeatureStore((SimpleFeatureStore) source, schema, // definitionQuery); } return new DefaultView(source, query); } /** * Takes a query and adapts it to match re definitionQuery filter configured * for a feature type. It won't handle coordinate system changes * <p> * Grabs the following from query: * <ul> * <li>typeName - only used if client does not supply * <li>filter - combined with client filter * <li>propertyNames - combined with client filter (indicate property names * that *must* be included) * </ul> * </p> * * @param query * Query against this DataStore * * @return Query restricted to the limits of definitionQuery * * @throws IOException * See DataSourceException * @throws DataSourceException * If query could not meet the restrictions of definitionQuery */ protected DefaultQuery makeDefinitionQuery(Query query) throws IOException { if ((query == Query.ALL) || query.equals(Query.ALL)) { return new DefaultQuery(constraintQuery); } try { String[] propNames = extractAllowedAttributes(query); String typeName = query.getTypeName(); if (typeName == null) { typeName = constraintQuery.getTypeName(); } URI namespace = query.getNamespace(); if (namespace == null || namespace == Query.NO_NAMESPACE) { namespace = constraintQuery.getNamespace(); } Filter filter = makeDefinitionFilter(query.getFilter()); int maxFeatures = Math.min(query.getMaxFeatures(), constraintQuery.getMaxFeatures()); String handle = query.getHandle(); if (handle == null) { handle = constraintQuery.getHandle(); } else if (constraintQuery.getHandle() != null) { handle = handle + "(" + constraintQuery.getHandle() + ")"; } DefaultQuery defaultQuery = new DefaultQuery(typeName, namespace, filter, maxFeatures, propNames, handle); defaultQuery.setSortBy(query.getSortBy()); return defaultQuery; } catch (Exception ex) { throw new DataSourceException( "Could not restrict the query to the definition criteria: " + ex.getMessage(), ex); } } /** * List of allowed attributes. * * <p> * Creates a list of FeatureTypeInfo's attribute names based on the * attributes requested by <code>query</code> and making sure they not * contain any non exposed attribute. * </p> * * <p> * Exposed attributes are those configured in the "attributes" element of * the FeatureTypeInfo's configuration * </p> * * @param query * User's origional query * * @return List of allowed attribute types */ private String[] extractAllowedAttributes(Query query) { String[] propNames = null; if (query.retrieveAllProperties()) { propNames = new String[schema.getAttributeCount()]; for (int i = 0; i < schema.getAttributeCount(); i++) { propNames[i] = schema.getDescriptor(i).getLocalName(); } } else { String[] queriedAtts = query.getPropertyNames(); int queriedAttCount = queriedAtts.length; List allowedAtts = new LinkedList(); for (int i = 0; i < queriedAttCount; i++) { if (schema.getDescriptor(queriedAtts[i]) != null) { allowedAtts.add(queriedAtts[i]); } else { LOGGER.info("queried a not allowed property: " + queriedAtts[i] + ". Ommitting it from query"); } } propNames = (String[]) allowedAtts.toArray(new String[allowedAtts.size()]); } return propNames; } /** * If a definition query has been configured for the FeatureTypeInfo, makes * and return a new Filter that contains both the query's filter and the * layer's definition one, by logic AND'ing them. * * @param filter * Origional user supplied Filter * * @return Filter adjusted to the limitations of definitionQuery * * @throws DataSourceException * If the filter could not meet the limitations of * definitionQuery */ protected Filter makeDefinitionFilter(Filter filter) throws DataSourceException { Filter newFilter = filter; Filter constraintFilter = constraintQuery.getFilter(); try { if (constraintFilter != Filter.INCLUDE) { FilterFactory ff = CommonFactoryFinder.getFilterFactory(null); newFilter = ff.and(constraintFilter, filter); } } catch (Exception ex) { throw new DataSourceException("Can't create the constraint filter", ex); } return newFilter; } /** * Implement getDataStore. * * <p> * Description ... * </p> * * @return @see org.geotools.data.FeatureSource#getDataStore() */ public DataStore getDataStore() { return (DataStore) source.getDataStore(); } /** * Implement addFeatureListener. * * <p> * Description ... * </p> * * @param listener * * @see org.geotools.data.FeatureSource#addFeatureListener(org.geotools.data.FeatureListener) */ public void addFeatureListener(FeatureListener listener) { source.addFeatureListener(listener); } /** * Implement removeFeatureListener. * * <p> * Description ... * </p> * * @param listener * * @see org.geotools.data.FeatureSource#removeFeatureListener(org.geotools.data.FeatureListener) */ public void removeFeatureListener(FeatureListener listener) { source.removeFeatureListener(listener); } /** * Implement getFeatures. * * <p> * Description ... * </p> * * @param query * * @return @throws * IOException * * @see org.geotools.data.FeatureSource#getFeatures(org.geotools.data.Query) */ public SimpleFeatureCollection getFeatures(Query query) throws IOException { DefaultQuery mergedQuery = makeDefinitionQuery(query); SimpleFeatureCollection results = source.getFeatures(mergedQuery); // Get all the coordinate systems involved in the two queries CoordinateReferenceSystem cCs = constraintQuery.getCoordinateSystem(); CoordinateReferenceSystem cCsr = constraintQuery.getCoordinateSystemReproject(); CoordinateReferenceSystem qCs = query.getCoordinateSystem(); CoordinateReferenceSystem qCsr = query.getCoordinateSystemReproject(); /* * Here we create all the needed transformations. We assume for the * moment that the data stores are incapable of any kind of cs * transformation and neither capable of forcing cs. We also assume that * concatenating multiple forced and reprojected wrappers is inexpensive * since they are optimized to recognize each other and to avoid useless * object creation */ try { if (qCsr != null && cCsr != null) { if (cCs != null) results = new ForceCoordinateSystemFeatureResults(results, cCs); results = new ReprojectFeatureResults(results, cCsr); if (qCs != null) results = new ForceCoordinateSystemFeatureResults(results, qCs); results = new ReprojectFeatureResults(results, qCsr); } else if (qCs != null && cCsr != null) { // complex case 2, reprojected then forced // mergedQuery.setCoordinateSystem(cCs); // mergedQuery.setCoordinateSystemReproject(cCsr); try { if (cCs != null) results = new ForceCoordinateSystemFeatureResults(results, cCs); results = new ReprojectFeatureResults(source.getFeatures(mergedQuery), cCsr); results = new ForceCoordinateSystemFeatureResults(results, qCs); } catch (SchemaException e) { throw new DataSourceException("This should not happen", e); } } else { // easy case, we can just put toghether one forced cs and one // reprojection cs // in the mixed query and let it go // mergedQuery.setCoordinateSystem(qCs != null ? qCs : cCs); // mergedQuery.setCoordinateSystemReproject(qCsr != null ? qCsr // : cCsr); CoordinateReferenceSystem forcedCS = qCs != null ? qCs : cCs; CoordinateReferenceSystem reprojectCS = qCsr != null ? qCsr : cCsr; if (forcedCS != null) results = new ForceCoordinateSystemFeatureResults(results, forcedCS); if (reprojectCS != null) results = new ReprojectFeatureResults(results, reprojectCS); } } catch (IOException e) { throw e; } catch (Exception e) { throw new DataSourceException("A problem occurred while handling forced " + "coordinate systems and reprojection", e); } return results; } /** * Implement getFeatures. * * <p> * Description ... * </p> * * @param filter * * @return @throws * IOException */ public SimpleFeatureCollection getFeatures(Filter filter) throws IOException { return getFeatures(new DefaultQuery(schema.getTypeName(),filter)); } /** * Implement getFeatures. * * <p> * Description ... * </p> * * @return @throws * IOException * * @see org.geotools.data.FeatureSource#getFeatures() */ public SimpleFeatureCollection getFeatures() throws IOException { return getFeatures(Query.ALL); } /** * Implement getSchema. * * <p> * Description ... * </p> * * @return @see org.geotools.data.FeatureSource#getSchema() */ public SimpleFeatureType getSchema() { return schema; } public ResourceInfo getInfo() { return new ResourceInfo(){ final Set<String> words = new HashSet<String>(); { words.add("features"); words.add("view"); words.add( DefaultView.this.getSchema().getTypeName() ); } public ReferencedEnvelope getBounds() { try { return DefaultView.this.getBounds(); } catch (IOException e) { return null; } } public CoordinateReferenceSystem getCRS() { return DefaultView.this.getSchema().getCoordinateReferenceSystem(); } public String getDescription() { return null; } public Set<String> getKeywords() { return words; } public String getName() { return DefaultView.this.getSchema().getTypeName(); } public URI getSchema() { Name name = DefaultView.this.getSchema().getName(); URI namespace; try { namespace = new URI( name.getNamespaceURI() ); return namespace; } catch (URISyntaxException e) { return null; } } public String getTitle() { Name name = DefaultView.this.getSchema().getName(); return name.getLocalPart(); } }; } /** * Retrieves the total extent of this FeatureSource. * * <p> * Please note this extent will reflect the provided definitionQuery. * </p> * * @return Extent of this FeatureSource, or <code>null</code> if no * optimizations exist. * * @throws IOException * If bounds of definitionQuery */ public ReferencedEnvelope getBounds() throws IOException { if (constraintQuery.getCoordinateSystemReproject() == null) { if (constraintQuery.getFilter() == null || constraintQuery.getFilter() == Filter.INCLUDE || Filter.INCLUDE.equals(constraintQuery.getFilter())) { return source.getBounds(); } return source.getBounds(constraintQuery); } // this will create a feature results that can reproject the // features, and will // properly compute the bouds return getFeatures().getBounds(); } /** * Retrive the extent of the Query. * * <p> * This method provides access to an optimized getBounds opperation. If no * optimized opperation is available <code>null</code> will be returned. * </p> * * <p> * You may still make use of getFeatures( Query ).getCount() which will * return the correct answer (even if it has to itterate through all the * results to do so. * </p> * * @param query * User's query * * @return Extend of Query or <code>null</code> if no optimization is * available * * @throws IOException * If a problem is encountered with source */ public ReferencedEnvelope getBounds(Query query) throws IOException { if (constraintQuery.getCoordinateSystemReproject() == null) { try { query = makeDefinitionQuery(query); } catch (IOException ex) { return null; } return source.getBounds(query); } // this will create a feature results that can reproject the // features, and will // properly compute the bouds return getFeatures(query).getBounds(); } /** * Adjust query and forward to source. * * <p> * This method provides access to an optimized getCount opperation. If no * optimized opperation is available <code>-1</code> will be returned. * </p> * * <p> * You may still make use of getFeatures( Query ).getCount() which will * return the correct answer (even if it has to itterate through all the * results to do so). * </p> * * @param query * User's query. * * @return Number of Features for Query, or -1 if no optimization is * available. */ public int getCount(Query query) { try { query = makeDefinitionQuery(query); } catch (IOException ex) { return -1; } try { return source.getCount(query); } catch (IOException e) { return 0; } } public Set getSupportedHints() { return source.getSupportedHints(); } public QueryCapabilities getQueryCapabilities() { return source.getQueryCapabilities(); } }