/* * GeoTools - The Open Source Java GIS Toolkit * http://geotools.org * * (C) 2008-2011, 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.complex; import java.io.IOException; import java.util.ArrayList; import java.util.Collection; import java.util.HashSet; import java.util.Iterator; import java.util.LinkedHashMap; import java.util.LinkedList; import java.util.List; import java.util.Map; import java.util.Set; import java.util.logging.Logger; import org.geotools.data.DataAccess; import org.geotools.data.DataSourceException; import org.geotools.data.DataStore; import org.geotools.data.Query; import org.geotools.data.FeatureSource; import org.geotools.data.SchemaNotFoundException; import org.geotools.data.ServiceInfo; import org.geotools.data.complex.config.NonFeatureTypeProxy; import org.geotools.data.complex.filter.UnmappingFilterVisitor; import org.geotools.data.complex.filter.UnmappingFilterVistorFactory; import org.geotools.data.complex.filter.XPath; import org.geotools.data.complex.filter.XPath.StepList; import org.geotools.data.joining.JoiningQuery; import org.geotools.factory.CommonFactoryFinder; import org.geotools.feature.Types; import org.geotools.filter.FilterAttributeExtractor; import org.geotools.filter.SortByImpl; import org.geotools.geometry.jts.ReferencedEnvelope; import org.opengis.feature.Feature; import org.opengis.feature.type.AttributeDescriptor; import org.opengis.feature.type.AttributeType; import org.opengis.feature.type.ComplexType; import org.opengis.feature.type.FeatureType; import org.opengis.feature.type.Name; import org.opengis.feature.type.PropertyDescriptor; import org.opengis.filter.Filter; import org.opengis.filter.FilterFactory2; import org.opengis.filter.expression.Expression; import org.opengis.filter.expression.PropertyName; import org.opengis.filter.sort.SortBy; import org.opengis.filter.sort.SortOrder; /** * A {@link DataAccess} that maps a "simple" source {@link DataStore} into a source of full Feature * features conforming to an application schema. * * @author Gabriel Roldan (Axios Engineering) * @author Ben Caradoc-Davies (CSIRO Earth Science and Resource Engineering) * @author Rini Angreani (CSIRO Earth Science and Resource Engineering) * @version $Id$ * * * @source $URL$ * @since 2.4 */ public class AppSchemaDataAccess implements DataAccess<FeatureType, Feature> { private static final Logger LOGGER = org.geotools.util.logging.Logging .getLogger(AppSchemaDataAccess.class.getPackage().getName()); private Map<Name, FeatureTypeMapping> mappings = new LinkedHashMap<Name, FeatureTypeMapping>(); private FilterFactory2 filterFac = CommonFactoryFinder.getFilterFactory2(null); /** * Constructor. * * @param mappings * a Set containing a {@linkplain FeatureTypeMapping} for each FeatureType this * DataAccess is going to produce. * @throws IOException */ public AppSchemaDataAccess(Set<FeatureTypeMapping> mappings) throws IOException { try { for (FeatureTypeMapping mapping : mappings) { Name name = mapping.getMappingName(); if (name == null) { name = mapping.getTargetFeature().getName(); } if (this.mappings.containsKey(name) || DataAccessRegistry.hasName(name)) { // check both mappings and the registry, because the data access is // only registered at the bottom of this constructor, so it might not // be in the registry yet throw new DataSourceException( "Duplicate mappingName or targetElement across FeatureTypeMapping instances detected.\n" + "They have to be unique, or app-schema doesn't know which one to get.\n" + "Please check your mapping file(s) with mappingName or targetElement of: " + name); } this.mappings.put(name, mapping); // if the type is not a feature, it should be wrapped with // a fake feature type, so attributes can be chained/nested AttributeType type = mapping.getTargetFeature().getType(); if (!(type instanceof FeatureType)) { // nasty side-effect: constructor edits mapping to use this type proxy new NonFeatureTypeProxy((ComplexType) type, mapping); } } } catch (RuntimeException e) { // dispose all source data stores in the input mappings for (FeatureTypeMapping mapping : mappings) { mapping.getSource().getDataStore().dispose(); } throw e; } register(); } /** * Registers this data access to the registry so the mappings can be retrieved globally */ protected void register() { DataAccessRegistry.register(this); } /** * Returns the set of target type names this DataAccess holds, where the term 'target type name' * refers to the name of one of the types this DataAccess produces by mapping another ones * through the definitions stored in its {@linkplain FeatureTypeMapping}s */ public Name[] getTypeNames() throws IOException { Name[] typeNames = new Name[mappings.size()]; int i = 0; for (FeatureTypeMapping mapping : mappings.values()) { typeNames[i] = mapping.getTargetFeature().getName(); i++; } return typeNames; } /** * Finds the target FeatureType named <code>typeName</code> in this ComplexDatastore's internal * list of FeatureType mappings and returns it. */ public FeatureType getSchema(Name typeName) throws IOException { return (FeatureType) getMappingByElement(typeName).getTargetFeature().getType(); } /** * Returns the mapping suite for the given targetElement name or mappingName. * * <p> * Note this method is public just for unit testing purposes * </p> * * @param typeName * @return * @throws IOException */ public FeatureTypeMapping getMappingByName(Name typeName) throws IOException { FeatureTypeMapping mapping = (FeatureTypeMapping) this.mappings.get(typeName); if (mapping == null) { throw new DataSourceException(typeName + " not found. Available: " + mappings.keySet().toString()); } return mapping; } /** * Returns the mapping suite for the given target type name. * * <p> * Note this method is public just for unit testing purposes * </p> * * @param typeName * @return * @throws IOException */ public FeatureTypeMapping getMappingByElement(Name typeName) throws IOException { for (FeatureTypeMapping mapping : mappings.values()) { if (mapping.getTargetFeature().getName().equals(typeName)) { return mapping; } } ArrayList<String> availables = new ArrayList<String>(); for (FeatureTypeMapping mapping : mappings.values()) { availables.add(mapping.getTargetFeature().getName().toString()); } throw new DataSourceException(typeName + " not found. Available: " + availables.toString()); } /** * @param name * mappingName or targetElement * @return true if this data access contains mapping with for provided name */ public boolean hasName(Name name) { return this.mappings.containsKey(name); } /** * @param typeName * targetElement name * @return true if this data access contains mapping for provided targetElement name */ public boolean hasElement(Name typeName) { for (FeatureTypeMapping mapping : mappings.values()) { if (mapping.getTargetFeature().getName().equals(typeName)) { return true; } } return false; } /** * Computes the bounds of the features for the specified feature type that satisfy the query * provided that there is a fast way to get that result. * <p> * Will return null if there is not fast way to compute the bounds. Since it's based on some * kind of header/cached information, it's not guaranteed to be real bound of the features * </p> * * @param query * @return the bounds, or null if too expensive * @throws SchemaNotFoundException * @throws IOException */ protected ReferencedEnvelope getBounds(Query query) throws IOException { FeatureTypeMapping mapping = getMappingByElement(getName(query)); Query unmappedQuery = unrollQuery(query, mapping); return mapping.getSource().getBounds(unmappedQuery); } /** * Gets the number of the features that would be returned by this query for the specified * feature type. * <p> * If getBounds(Query) returns <code>-1</code> due to expense consider using * <code>getFeatures(Query).getCount()</code> as a an alternative. * </p> * * @param targetQuery * Contains the Filter and MaxFeatures to find the bounds for. * @return The number of Features provided by the Query or <code>-1</code> if count is too * expensive to calculate or any errors or occur. * @throws IOException * * @throws IOException * if there are errors getting the count */ protected int getCount(final Query targetQuery) throws IOException { final FeatureTypeMapping mapping = getMappingByElement(getName(targetQuery)); final FeatureSource<FeatureType, Feature> mappedSource = mapping.getSource(); Query unmappedQuery = unrollQuery(targetQuery, mapping); unmappedQuery.setMaxFeatures(targetQuery.getMaxFeatures()); return mappedSource.getCount(unmappedQuery); } /** * Return the name of the type that is queried. * * @param query * @return Name constructed from the query. */ private Name getName(Query query) { if (query.getNamespace() == null) { return Types.typeName(query.getTypeName()); } else { return Types.typeName(query.getNamespace().toString(), query.getTypeName()); } } /** * Returns <code>Filter.INCLUDE</code>, as the whole filter is unrolled and passed back to the * underlying DataStore to be treated. * * @return <code>Filter.INLCUDE</code> */ protected Filter getUnsupportedFilter(String typeName, Filter filter) { return Filter.INCLUDE; } /** * Creates a <code>org.geotools.data.Query</code> that operates over the surrogate DataStore, by * unrolling the <code>org.geotools.filter.Filter</code> contained in the passed * <code>query</code>, and replacing the list of required attributes by the ones of the mapped * FeatureType. * * @param query * @param mapping * @return */ @SuppressWarnings("unchecked") public Query unrollQuery(Query query, FeatureTypeMapping mapping) { Query unrolledQuery = Query.ALL; FeatureSource<FeatureType, Feature> source = mapping.getSource(); if (!Query.ALL.equals(query)) { Filter complexFilter = query.getFilter(); Filter unrolledFilter = AppSchemaDataAccess.unrollFilter(complexFilter, mapping); Object includeProps = query.getHints().get(Query.INCLUDE_MANDATORY_PROPS); List<PropertyName> propNames = getSurrogatePropertyNames(query.getProperties(), mapping, includeProps instanceof Boolean && ((Boolean) includeProps).booleanValue()); Query newQuery = new Query(); String name = source.getName().getLocalPart(); newQuery.setTypeName(name); newQuery.setFilter(unrolledFilter); newQuery.setProperties(propNames); newQuery.setCoordinateSystem(query.getCoordinateSystem()); newQuery.setCoordinateSystemReproject(query.getCoordinateSystemReproject()); newQuery.setHandle(query.getHandle()); newQuery.setMaxFeatures(query.getMaxFeatures()); if (query instanceof JoiningQuery) { FilterAttributeExtractor extractor = new FilterAttributeExtractor(); mapping.getFeatureIdExpression().accept(extractor, null); List<SortBy> sort = new ArrayList<SortBy>(); for (String att : extractor.getAttributeNameSet()) { sort.add(new SortByImpl(filterFac.property(att), SortOrder.ASCENDING )); } newQuery.setSortBy( sort.toArray(new SortBy[sort.size()]) ); JoiningQuery jQuery = new JoiningQuery(newQuery); jQuery.setJoins(((JoiningQuery)query).getJoins()); unrolledQuery = jQuery; } else { unrolledQuery = newQuery; } } return unrolledQuery; } /** * Helper method for getSurrogatePropertyNames to match a requested x-path property with a * target x-path * * @param requestedProperty * requested property x-path * @param targetXPath * target x-path * @return whether they match, i.e. when one of them is completely contained in the other */ protected static boolean matchProperty(StepList requestedProperty, StepList target) { // NC - include all parent and children paths of the requested property // i.e.: requested "measurement", found mapping of "measurement/result". // "result" must be included to create "measurement" // in other cases, requested property is a nested x-path, // so get all mappings that could be needed // i.e.: requested "measurement/result", found mapping of "measurement". // "measurement" must be included to create "result" int minSize = Math.min(requestedProperty.size(), target.size()); for (int i = 0; i < minSize; i++) { if (!target.get(i).getName().equals(requestedProperty.get(i).getName())) { return false; } } return true; } /** * Helper method for getSurrogatePropertyNames to match a requested single step property * with a target x-path, ignoring namespaces * * @param requestedProperty requested property x-path * @param targetXPath target x-path * @return whether they match, i.e. when one of them is completely contained in the other */ protected static boolean matchProperty(String requestedProperty, StepList target) { //requested Properties are top level nodes, so get all mappings inside node return target.get(0).getName().getLocalPart().equals(requestedProperty); } /** * * @param mappingProperties * @param mapping * @return <code>null</code> if all surrogate attributes shall be queried, else the list of * needed surrogate attributes to satisfy the mapping of prorperties in * <code>mappingProperties</code> */ private List<PropertyName> getSurrogatePropertyNames(List<PropertyName> requestedProperties, FeatureTypeMapping mapping, boolean includeMandatory) { List<PropertyName> propNames = null; final AttributeDescriptor targetDescriptor = mapping.getTargetFeature(); if (requestedProperties != null && requestedProperties.size() > 0) { requestedProperties = new ArrayList<PropertyName>(requestedProperties); Set<PropertyName> requestedSurrogateProperties = new HashSet<PropertyName>(); // add all surrogate attributes involved in mapping of the requested // target schema attributes List<AttributeMapping> attMappings = mapping.getAttributeMappings(); // NC - add feature to list, to include its ID expression requestedProperties.add(filterFac.property(mapping.getTargetFeature().getName())); // get source type AttributeType mappedType; try { mappedType = mapping.getSource().getSchema(); } catch (UnsupportedOperationException e) { // web service backend doesn't support getSchema() mappedType = null; } for (final AttributeMapping entry : attMappings) { final StepList targetSteps = entry.getTargetXPath(); boolean addThis = false; if (includeMandatory) { PropertyName targetProp = filterFac.property(targetSteps.toString(), mapping.getNamespaces()); Object descr = targetProp.evaluate(targetDescriptor.getType()); if (descr instanceof PropertyDescriptor) { if (((PropertyDescriptor) descr).getMinOccurs() >= 1) { addThis = true; } } } if (!addThis) { for (PropertyName requestedProperty : requestedProperties) { StepList requestedPropertySteps = requestedProperty.getNamespaceContext() == null ? null : XPath.steps(targetDescriptor, requestedProperty.getPropertyName(), requestedProperty.getNamespaceContext()); if (requestedPropertySteps == null ? matchProperty( requestedProperty.getPropertyName(), targetSteps) : matchProperty( requestedPropertySteps, targetSteps)) { addThis = true; break; } } } if (addThis) { final Expression sourceExpression = entry.getSourceExpression(); final Expression idExpression = entry.getIdentifierExpression(); // NC - include client properties final Collection<Expression> clientProperties = entry.getClientProperties() .values(); FilterAttributeExtractor extractor = new FilterAttributeExtractor(); sourceExpression.accept(extractor, null); idExpression.accept(extractor, null); Iterator<Expression> it = clientProperties.iterator(); while (it.hasNext()) { it.next().accept(extractor, null); } Set<String> exprAtts = extractor.getAttributeNameSet(); for (String mappedAtt : exprAtts) { if (!mappedAtt.equals("Expression.NIL")) { // NC - ignore Nil Expression if (mappedType == null) { // web service backend.. no underlying simple feature // so just assume that it exists.. requestedSurrogateProperties.add(filterFac.property(mappedAtt)); } else { PropertyName propExpr = filterFac.property(mappedAtt); Object object = propExpr.evaluate(mappedType); AttributeDescriptor mappedAttribute = (AttributeDescriptor) object; if (mappedAttribute != null) { requestedSurrogateProperties.add(filterFac.property(mappedAtt)); } else { LOGGER.info("mapped type does not contains property " + mappedAtt); } } } } LOGGER.fine("adding atts needed for : " + exprAtts); } } propNames = new ArrayList<PropertyName>(requestedSurrogateProperties); } return propNames; } /** * Takes a filter that operates against a {@linkplain FeatureTypeMapping}'s target FeatureType, * and unrolls it creating a new Filter that operates against the mapping's source FeatureType. * * @param complexFilter * @return TODO: implement filter unrolling */ public static Filter unrollFilter(Filter complexFilter, FeatureTypeMapping mapping) { UnmappingFilterVisitor visitor = UnmappingFilterVistorFactory.getInstance(mapping); Filter unrolledFilter = (Filter) complexFilter.accept(visitor, null); return unrolledFilter; } public void dispose() { DataAccessRegistry.unregister(this); // dispose all the source data stores for (FeatureTypeMapping mapping : mappings.values()) { mapping.getSource().getDataStore().dispose(); } mappings.clear(); } /** * Not a supported operation. * * @see org.geotools.data.DataAccess#getInfo() */ public ServiceInfo getInfo() { throw new UnsupportedOperationException(); } /** * Return the names of the target features. * * @see org.geotools.data.DataAccess#getNames() */ public List<Name> getNames() { List<Name> names = new LinkedList<Name>(); names.addAll(mappings.keySet()); return names; } /** * Not a supported operation. * * @see org.geotools.data.DataAccess#createSchema(org.opengis.feature.type.FeatureType) */ public void createSchema(FeatureType featureType) throws IOException { throw new UnsupportedOperationException(); } /** * Return a feature source that can be used to obtain features of a particular type. * * @see org.geotools.data.DataAccess#getFeatureSource(org.opengis.feature.type.Name) */ public FeatureSource<FeatureType, Feature> getFeatureSource(Name typeName) throws IOException { return new MappingFeatureSource(this, getMappingByElement(typeName)); } /** * Not a supported operation. * * @see org.geotools.data.DataAccess#updateSchema(org.opengis.feature.type.Name, * org.opengis.feature.type.FeatureType) */ public void updateSchema(Name typeName, FeatureType featureType) throws IOException { throw new UnsupportedOperationException(); } /** * Return a feature source that can be used to obtain features of a particular name. This name * would be the mappingName in the TypeMapping if it exists, otherwise it's the target element * name. * * @param typeName * mappingName or targetElement * @return Mapping feature source * @throws IOException */ public FeatureSource<FeatureType, Feature> getFeatureSourceByName(Name typeName) throws IOException { return new MappingFeatureSource(this, getMappingByName(typeName)); } }