org.geotools.data.complex.DataAccessMappingFeatureIterator.java Source code

Java tutorial

Introduction

Here is the source code for org.geotools.data.complex.DataAccessMappingFeatureIterator.java

Source

/*
 *    GeoTools - The Open Source Java GIS Toolkit
 *    http://geotools.org
 *
 *    (C) 2007-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.net.URI;
import java.util.ArrayList;
import java.util.Collection;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.Set;

import javax.xml.namespace.QName;

import org.apache.commons.lang.StringUtils;
import org.geotools.data.DataAccess;
import org.geotools.data.DataSourceException;
import org.geotools.data.FeatureSource;
import org.geotools.data.Query;
import org.geotools.data.complex.config.NonFeatureTypeProxy;
import org.geotools.data.complex.filter.XPath;
import org.geotools.data.complex.filter.XPathUtil.Step;
import org.geotools.data.complex.filter.XPathUtil.StepList;
import org.geotools.data.joining.JoiningNestedAttributeMapping;
import org.geotools.data.joining.JoiningQuery;
import org.geotools.feature.AttributeBuilder;
import org.geotools.feature.ComplexAttributeImpl;
import org.geotools.feature.FeatureCollection;
import org.geotools.feature.FeatureImpl;
import org.geotools.feature.FeatureIterator;
import org.geotools.feature.Types;
import org.geotools.filter.AttributeExpressionImpl;
import org.geotools.filter.FilterAttributeExtractor;
import org.geotools.gml2.bindings.GML2EncodingUtils;
import org.geotools.jdbc.JDBCFeatureSource;
import org.geotools.jdbc.JDBCFeatureStore;
import org.geotools.jdbc.JoiningJDBCFeatureSource;
import org.geotools.referencing.CRS;
import org.opengis.feature.Attribute;
import org.opengis.feature.ComplexAttribute;
import org.opengis.feature.Feature;
import org.opengis.feature.Property;
import org.opengis.feature.type.AttributeDescriptor;
import org.opengis.feature.type.AttributeType;
import org.opengis.feature.type.FeatureType;
import org.opengis.feature.type.GeometryDescriptor;
import org.opengis.feature.type.GeometryType;
import org.opengis.feature.type.Name;
import org.opengis.filter.Filter;
import org.opengis.feature.type.PropertyDescriptor;
import org.opengis.filter.expression.Expression;
import org.opengis.filter.expression.PropertyName;
import org.opengis.filter.identity.FeatureId;
import org.opengis.referencing.crs.CoordinateReferenceSystem;
import org.xml.sax.Attributes;

/**
 * A Feature iterator that operates over the FeatureSource of a
 * {@linkplain org.geotools.data.complex.FeatureTypeMapping} and produces Features of the output
 * schema by applying the mapping rules to the Features of the source schema.
 * <p>
 * This iterator acts like a one-to-one mapping, producing a Feature of the target type for each
 * feature of the source type.
 *
 * @author Gabriel Roldan (Axios Engineering)
 * @author Ben Caradoc-Davies (CSIRO Earth Science and Resource Engineering)
 * @author Rini Angreani (CSIRO Earth Science and Resource Engineering)
 * @author Russell Petty (GeoScience Victoria)
 * @version $Id$
 *
 *
 *
 * @source $URL$
 *         http://svn.osgeo.org/geotools/trunk/modules/unsupported/app-schema/app-schema/src/main
 *         /java/org/geotools/data/complex/DataAccessMappingFeatureIterator.java $
 * @since 2.4
 */
public class DataAccessMappingFeatureIterator extends AbstractMappingFeatureIterator {
    /**
     * Hold on to iterator to allow features to be streamed.
     */
    private FeatureIterator<? extends Feature> sourceFeatureIterator;

    /**
     * Reprojected CRS from the source simple features, or null
     */
    protected CoordinateReferenceSystem reprojection;

    /**
     * This is the feature that will be processed in next()
     */
    protected Feature curSrcFeature;

    protected FeatureSource<? extends FeatureType, ? extends Feature> mappedSource;

    protected FeatureCollection<? extends FeatureType, ? extends Feature> sourceFeatures;

    protected List<Expression> foreignIds = null;

    protected AttributeDescriptor targetFeature;

    /**
     * True if joining is turned off and pre filter exists. There's a need to run extra query to get
     * features by id because they might come from denormalised view. The rows might not match the
     * filter therefore doesn't exist in the mapped source but match the id of other rows.
     */
    private boolean isFiltered = false;

    private ArrayList<String> filteredFeatures;
    /**
     * Temporary/experimental changes for enabling subsetting for isList only. 
     */
    private Filter listFilter;

    public DataAccessMappingFeatureIterator(AppSchemaDataAccess store, FeatureTypeMapping mapping, Query query,
            boolean isFiltered) throws IOException {
        this(store, mapping, query, null);
        this.isFiltered = isFiltered;
        if (isFiltered) {
            filteredFeatures = new ArrayList<String>();
        }
    }

    public DataAccessMappingFeatureIterator(AppSchemaDataAccess store, FeatureTypeMapping mapping, Query query)
            throws IOException {
        this(store, mapping, query, null);
    }

    /**
     *
     * @param store
     * @param mapping
     *            place holder for the target type, the surrogate FeatureSource and the mappings
     *            between them.
     * @param query
     *            the query over the target feature type, that is to be unpacked to its equivalent
     *            over the surrogate feature type.
     * @throws IOException
     */
    public DataAccessMappingFeatureIterator(AppSchemaDataAccess store, FeatureTypeMapping mapping, Query query,
            Query unrolledQuery) throws IOException {
        super(store, mapping, query, unrolledQuery);
    }

    @Override
    public boolean hasNext() {
        boolean exists = !isNextSourceFeatureNull();

        if (!isHasNextCalled()) {
            if (featureCounter < maxFeatures) {
                if (!exists && getSourceFeatureIterator() != null && getSourceFeatureIterator().hasNext()) {
                    this.curSrcFeature = getSourceFeatureIterator().next();
                    exists = true;
                }
                if (exists && filteredFeatures != null) {
                    // get the next one if this row has already been added to the target
                    // feature from setNextFilteredFeature
                    while (exists && filteredFeatures.contains(extractIdForFeature(this.curSrcFeature))) {
                        if (getSourceFeatureIterator() != null && getSourceFeatureIterator().hasNext()) {
                            this.curSrcFeature = getSourceFeatureIterator().next();
                            exists = true;
                        } else {
                            exists = false;
                        }
                    }
                }
                // HACK HACK HACK
                // evaluate filter that applies to this list as we want a subset
                // instead of full result
                // this is a temporary solution for Bureau of Meteorology
                // requirement for timePositionList
                if (listFilter != null) {
                    while (exists && !listFilter.evaluate(curSrcFeature)) {
                        // only add to subset if filter matches value
                        if (getSourceFeatureIterator() != null && getSourceFeatureIterator().hasNext()) {
                            this.curSrcFeature = getSourceFeatureIterator().next();
                            exists = true;
                        } else {
                            exists = false;
                        }
                    }
                }
                // END OF HACK
            } else {
                exists = false;
            }
        }

        if (!exists) {
            LOGGER.finest("no more features, produced " + featureCounter);
            close();
            curSrcFeature = null;
        }

        setHasNextCalled(true);

        return exists;
    }

    protected FeatureIterator<? extends Feature> getSourceFeatureIterator() {
        return sourceFeatureIterator;
    }

    protected boolean isSourceFeatureIteratorNull() {
        return getSourceFeatureIterator() == null;
    }

    protected Object peekValue(Object source, Expression prop) {
        Object o = prop.evaluate(source);
        if (o instanceof Attribute) {
            o = ((Attribute) o).getValue();
        }
        return o;
    }

    public Object peekNextValue(Expression prop) {
        return peekValue(curSrcFeature, prop);
    }

    /**
     * Only used for Joining, to make sure that rows with different foreign id's
     * aren't interpreted as one feature and merged.
     */
    public void setForeignIds(List<Expression> ids) {
        foreignIds = ids;
    }

    /**
     * Only used for Joining, to make sure that rows with different foreign id's
     * aren't interpreted as one feature and merged.
     */
    public List<Object> getForeignIdValues(Object source) {
        if (foreignIds != null) {
            List<Object> foreignIdValues = new ArrayList<Object>();
            for (int i = 0; i < foreignIds.size(); i++) {
                foreignIdValues.add(i, peekValue(source, foreignIds.get(i)));
            }
            return foreignIdValues;
        }
        return null;
    }

    /**
     * Only used for Joining, to make sure that rows with different foreign id's
     * aren't interpreted as one feature and merged.
     */
    protected boolean checkForeignIdValues(List<Object> foreignIdValues, Feature next) {
        if (foreignIds != null) {
            for (int i = 0; i < foreignIds.size(); i++) {
                if (!peekValue(next, foreignIds.get(i)).toString().equals(foreignIdValues.get(i).toString())) {
                    return false;
                }
            }
        }
        return true;
    }

    /**
     * Only used for Joining, to make sure that rows with different foreign id's
     * aren't interpreted as one feature and merged.
     */
    public List<Object> getIdValues(Object source) {
        List<Object> ids = new ArrayList<Object>();
        FilterAttributeExtractor extractor = new FilterAttributeExtractor();
        mapping.getFeatureIdExpression().accept(extractor, null);
        for (String att : extractor.getAttributeNameSet()) {
            ids.add(peekValue(source, namespaceAwareFilterFactory.property(att)));
        }

        if (foreignIds != null) {
            ids.addAll(getForeignIdValues(source));
        }
        return ids;
    }

    /**
     * Only used for Joining, to make sure that rows with different foreign id's
     * aren't interpreted as one feature and merged.
     */
    public boolean checkForeignIdValues(List<Object> foreignIdValues) {
        return checkForeignIdValues(foreignIdValues, curSrcFeature);
    }

    protected void initialiseSourceFeatures(FeatureTypeMapping mapping, Query query,
            CoordinateReferenceSystem targetCRS) throws IOException {
        mappedSource = mapping.getSource();

        //NC - joining query
        if (query instanceof JoiningQuery) {
            if (mappedSource instanceof JDBCFeatureSource) {
                mappedSource = new JoiningJDBCFeatureSource((JDBCFeatureSource) mappedSource);
            } else if (mappedSource instanceof JDBCFeatureStore) {
                mappedSource = new JoiningJDBCFeatureSource((JDBCFeatureStore) mappedSource);
            } else {
                throw new IllegalArgumentException("Joining queries are only supported on JDBC data stores");
            }

        }
        String version = (String) this.mapping.getTargetFeature().getType().getUserData().get("targetVersion");
        //might be because top level feature has no geometry
        if (targetCRS == null && version != null) {
            // figure out the crs the data is in
            CoordinateReferenceSystem crs = null;
            try {
                crs = this.mappedSource.getSchema().getCoordinateReferenceSystem();
            } catch (UnsupportedOperationException e) {
                //do nothing as mappedSource is a WSFeatureSource
            }
            // gather declared CRS
            CoordinateReferenceSystem declaredCRS = this.getDeclaredCrs(crs, version);
            CoordinateReferenceSystem target;
            Object crsobject = this.mapping.getTargetFeature().getType().getUserData().get("targetCrs");
            if (crsobject instanceof CoordinateReferenceSystem) {
                target = (CoordinateReferenceSystem) crsobject;
            } else if (crsobject instanceof URI) {

                URI uri = (URI) crsobject;
                if (uri != null) {
                    try {
                        target = CRS.decode(uri.toString());
                    } catch (Exception e) {
                        String msg = "Unable to support srsName: " + uri;
                        throw new UnsupportedOperationException(msg, e);
                    }
                } else {
                    target = declaredCRS;
                }
            } else {
                target = declaredCRS;
            }
            this.reprojection = target;

        } else {
            this.reprojection = targetCRS;
        }

        //clean up user data related to request
        mapping.getTargetFeature().getType().getUserData().put("targetVersion", null);
        mapping.getTargetFeature().getType().getUserData().put("targetCrs", null);

        //reproject target feature
        targetFeature = reprojectAttribute(mapping.getTargetFeature());

        // we need to disable the max number of features retrieved so we can
        // sort them manually just in case the data is denormalised
        query.setMaxFeatures(Query.DEFAULT_MAX);
        sourceFeatures = mappedSource.getFeatures(query);
        if (reprojection != null) {
            xpathAttributeBuilder.setCRS(reprojection);
            if (sourceFeatures.getSchema().getGeometryDescriptor() == null || this.isReprojectionCrsEqual(
                    this.mappedSource.getSchema().getCoordinateReferenceSystem(), this.reprojection)) {
                // VT: No point trying to re-project without any geometry.
                query.setCoordinateSystemReproject(null);
            }
        }
        if (!(this instanceof XmlMappingFeatureIterator)) {
            this.sourceFeatureIterator = sourceFeatures.features();
        }

        // NC - joining nested atts
        for (AttributeMapping attMapping : selectedMapping) {

            if (attMapping instanceof JoiningNestedAttributeMapping) {
                ((JoiningNestedAttributeMapping) attMapping).open(this, query);

            }

        }

    }

    protected boolean unprocessedFeatureExists() {

        boolean exists = getSourceFeatureIterator().hasNext();
        if (exists && this.curSrcFeature == null) {
            this.curSrcFeature = getSourceFeatureIterator().next();
        }

        return exists;
    }

    protected String extractIdForFeature(Feature feature) {
        if (mapping.getFeatureIdExpression().equals(Expression.NIL)) {
            if (feature.getIdentifier() == null) {
                return null;
            } else {
                return feature.getIdentifier().getID();
            }
        }
        return mapping.getFeatureIdExpression().evaluate(feature, String.class);
    }

    protected String extractIdForAttribute(final Expression idExpression, Object sourceInstance) {
        String value = (String) idExpression.evaluate(sourceInstance, String.class);
        return value;
    }

    protected boolean isNextSourceFeatureNull() {
        return curSrcFeature == null;
    }

    protected boolean sourceFeatureIteratorHasNext() {
        return getSourceFeatureIterator().hasNext();
    }

    protected Object getValues(boolean isMultiValued, Expression expression, Object sourceFeatureInput) {
        if (isMultiValued && sourceFeatureInput instanceof FeatureImpl
                && expression instanceof AttributeExpressionImpl) {
            // RA: Feature Chaining
            // complex features can have multiple nodes of the same attribute.. and if they are used
            // as input to an app-schema data access to be nested inside another feature type of a
            // different XML type, it has to be mapped like this:
            // <AttributeMapping>
            // <targetAttribute>
            // gsml:composition
            // </targetAttribute>
            // <sourceExpression>
            // <inputAttribute>mo:composition</inputAttribute>
            // <linkElement>gsml:CompositionPart</linkElement>
            // <linkField>gml:name</linkField>
            // </sourceExpression>
            // <isMultiple>true</isMultiple>
            // </AttributeMapping>
            // As there can be multiple nodes of mo:composition in this case, we need to retrieve
            // all of them
            AttributeExpressionImpl attribExpression = ((AttributeExpressionImpl) expression);
            String xpath = attribExpression.getPropertyName();
            ComplexAttribute sourceFeature = (ComplexAttribute) sourceFeatureInput;
            StepList xpathSteps = XPath.steps(sourceFeature.getDescriptor(), xpath, namespaces);
            return getProperties(sourceFeature, xpathSteps);
        }
        return expression.evaluate(sourceFeatureInput);
    }

    /**
     * Sets the values of grouping attributes.
     *
     * @param target
     * @param source
     * @param attMapping
     * @param values
     *
     * @return Feature. Target feature sets with simple attributes
     */
    protected Attribute setAttributeValue(Attribute target, String id, final Object source,
            final AttributeMapping attMapping, Object values, StepList inputXpath,
            List<PropertyName> selectedProperties) throws IOException {

        final Expression sourceExpression = attMapping.getSourceExpression();
        final AttributeType targetNodeType = attMapping.getTargetNodeInstance();
        StepList xpath = inputXpath == null ? attMapping.getTargetXPath().clone() : inputXpath;

        Map<Name, Expression> clientPropsMappings = attMapping.getClientProperties();
        boolean isNestedFeature = attMapping.isNestedAttribute();

        if (id == null && Expression.NIL != attMapping.getIdentifierExpression()) {
            id = extractIdForAttribute(attMapping.getIdentifierExpression(), source);
        }
        if (attMapping.isNestedAttribute()) {
            NestedAttributeMapping nestedMapping = ((NestedAttributeMapping) attMapping);
            Object mappingName = nestedMapping.getNestedFeatureType(source);
            if (mappingName != null) {
                if (nestedMapping.isSameSource() && mappingName instanceof Name) {
                    // data type polymorphism mapping
                    return setPolymorphicValues((Name) mappingName, target, id, nestedMapping, source, xpath,
                            clientPropsMappings);
                } else if (mappingName instanceof String) {
                    // referential polymorphism mapping
                    return setPolymorphicReference((String) mappingName, clientPropsMappings, target, xpath,
                            targetNodeType);
                }
            } else {
                // polymorphism could result in null, to skip the attribute
                return null;
            }
        }
        if (values == null && source != null) {
            values = getValues(attMapping.isMultiValued(), sourceExpression, source);
        }
        boolean isHRefLink = isByReference(clientPropsMappings, isNestedFeature);
        if (isNestedFeature) {
            if (values == null) {
                // polymorphism use case, if the value doesn't match anything, don't encode
                return null;
            }
            // get built feature based on link value
            if (values instanceof Collection) {
                ArrayList<Attribute> nestedFeatures = new ArrayList<Attribute>(((Collection) values).size());
                for (Object val : (Collection) values) {
                    if (val instanceof Attribute) {
                        val = ((Attribute) val).getValue();
                        if (val instanceof Collection) {
                            val = ((Collection) val).iterator().next();
                        }
                        while (val instanceof Attribute) {
                            val = ((Attribute) val).getValue();
                        }
                    }
                    if (isHRefLink) {
                        // get the input features to avoid infinite loop in case the nested
                        // feature type also have a reference back to this type
                        // eg. gsml:GeologicUnit/gsml:occurence/gsml:MappedFeature
                        // and gsml:MappedFeature/gsml:specification/gsml:GeologicUnit
                        nestedFeatures.addAll(((NestedAttributeMapping) attMapping).getInputFeatures(this, val,
                                getIdValues(source), source, reprojection, selectedProperties, includeMandatory));
                    } else {
                        nestedFeatures.addAll(((NestedAttributeMapping) attMapping).getFeatures(this, val,
                                getIdValues(source), reprojection, source, selectedProperties, includeMandatory));
                    }
                }
                values = nestedFeatures;
            } else if (isHRefLink) {
                // get the input features to avoid infinite loop in case the nested
                // feature type also have a reference back to this type
                // eg. gsml:GeologicUnit/gsml:occurence/gsml:MappedFeature
                // and gsml:MappedFeature/gsml:specification/gsml:GeologicUnit
                values = ((NestedAttributeMapping) attMapping).getInputFeatures(this, values, getIdValues(source),
                        source, reprojection, selectedProperties, includeMandatory);
            } else {
                values = ((NestedAttributeMapping) attMapping).getFeatures(this, values, getIdValues(source),
                        reprojection, source, selectedProperties, includeMandatory);
            }
            if (isHRefLink) {
                // only need to set the href link value, not the nested feature properties
                setXlinkReference(target, clientPropsMappings, values, xpath, targetNodeType);
                return null;
            }
        }
        Attribute instance = null;
        if (values instanceof Collection) {
            // nested feature type could have multiple instances as the whole purpose
            // of feature chaining is to cater for multi-valued properties
            for (Object singleVal : (Collection) values) {
                ArrayList valueList = new ArrayList();
                // copy client properties from input features if they're complex features
                // wrapped in app-schema data access
                if (singleVal instanceof Attribute) {
                    // copy client properties from input features if they're complex features
                    // wrapped in app-schema data access
                    Map<Name, Expression> valueProperties = getClientProperties((Attribute) singleVal);
                    if (!valueProperties.isEmpty()) {
                        clientPropsMappings.putAll(valueProperties);
                    }
                }
                if (!isNestedFeature) {
                    if (singleVal instanceof Attribute) {
                        singleVal = ((Attribute) singleVal).getValue();
                    }
                    if (singleVal instanceof Collection) {
                        valueList.addAll((Collection) singleVal);
                    } else {
                        valueList.add(singleVal);
                    }
                } else {
                    valueList.add(singleVal);
                }
                instance = xpathAttributeBuilder.set(target, xpath, valueList, id, targetNodeType, false,
                        sourceExpression);
                setClientProperties(instance, source, clientPropsMappings);
            }
        } else {
            if (values instanceof Attribute) {
                // copy client properties from input features if they're complex features
                // wrapped in app-schema data access
                Map<Name, Expression> newClientProps = getClientProperties((Attribute) values);
                if (!newClientProps.isEmpty()) {
                    newClientProps.putAll(clientPropsMappings);
                    clientPropsMappings = newClientProps;
                }
                values = ((Attribute) values).getValue();
            }
            instance = xpathAttributeBuilder.set(target, xpath, values, id, targetNodeType, false,
                    sourceExpression);
            setClientProperties(instance, source, clientPropsMappings);

        }
        if (instance != null && attMapping.encodeIfEmpty()) {
            instance.getDescriptor().getUserData().put("encodeIfEmpty", attMapping.encodeIfEmpty());
        }
        return instance;
    }

    /**
     * Special handling for polymorphic mapping where the value of the attribute determines that
     * this attribute should be a placeholder for an xlink:href.
     *
     * @param uri
     *            the xlink:href URI
     * @param clientPropsMappings
     *            client properties
     * @param target
     *            the complex feature being built
     * @param xpath
     *            the xpath of attribute
     * @param targetNodeType
     *            the type of the attribute to be cast to, if any
     */
    private Attribute setPolymorphicReference(String uri, Map<Name, Expression> clientPropsMappings,
            Attribute target, StepList xpath, AttributeType targetNodeType) {

        if (uri != null) {
            Attribute instance = xpathAttributeBuilder.set(target, xpath, null, "", targetNodeType, true, null);
            Map<Name, Expression> newClientProps = new HashMap<Name, Expression>();
            newClientProps.putAll(clientPropsMappings);
            newClientProps.put(XLINK_HREF_NAME, namespaceAwareFilterFactory.literal(uri));
            setClientProperties(instance, null, newClientProps);
            return instance;
        }
        return null;
    }

    /**
     * Special handling for polymorphic mapping. Works out the polymorphic type name by evaluating
     * the function on the feature, then set the relevant sub-type values.
     *
     * @param target
     *            The target feature to be encoded
     * @param id
     *            The target feature id
     * @param nestedMapping
     *            The mapping that is polymorphic
     * @param source
     *            The source simple feature
     * @param xpath
     *            The xpath of polymorphic type
     * @param clientPropsMappings
     *            Client properties
     * @throws IOException
     */
    private Attribute setPolymorphicValues(Name mappingName, Attribute target, String id,
            NestedAttributeMapping nestedMapping, Object source, StepList xpath,
            Map<Name, Expression> clientPropsMappings) throws IOException {
        // process sub-type mapping
        DataAccess<FeatureType, Feature> da = DataAccessRegistry.getDataAccess((Name) mappingName);
        if (da instanceof AppSchemaDataAccess) {
            // why wouldn't it be? check just to be safe
            FeatureTypeMapping fTypeMapping = ((AppSchemaDataAccess) da).getMappingByName((Name) mappingName);
            List<AttributeMapping> polymorphicMappings = fTypeMapping.getAttributeMappings();
            AttributeDescriptor attDescriptor = fTypeMapping.getTargetFeature();
            AttributeType type = attDescriptor.getType();
            Name polymorphicTypeName = attDescriptor.getName();
            StepList prefixedXpath = xpath.clone();
            prefixedXpath.add(
                    new Step(new QName(polymorphicTypeName.getNamespaceURI(), polymorphicTypeName.getLocalPart(),
                            this.namespaces.getPrefix(polymorphicTypeName.getNamespaceURI())), 1));
            if (!fTypeMapping.getFeatureIdExpression().equals(Expression.NIL)) {
                id = fTypeMapping.getFeatureIdExpression().evaluate(source, String.class);
            }
            Attribute instance = xpathAttributeBuilder.set(target, prefixedXpath, null, id, type, false,
                    attDescriptor, null);
            setClientProperties(instance, source, clientPropsMappings);
            for (AttributeMapping mapping : polymorphicMappings) {
                if (skipTopElement(polymorphicTypeName, mapping, type)) {
                    // if the top level mapping for the Feature itself, the attribute instance
                    // has already been created.. just need to set the client properties
                    setClientProperties(instance, source, mapping.getClientProperties());
                    continue;
                }
                setAttributeValue(instance, null, source, mapping, null, null, selectedProperties.get(mapping));
            }
            return instance;
        }
        return null;
    }

    /**
     * Set xlink:href client property for multi-valued chained features. This has to be specially
     * handled because we don't want to encode the nested features attributes, since it's already an
     * xLink. Also we need to eliminate duplicates.
     *
     * @param target
     *            The target attribute
     * @param clientPropsMappings
     *            Client properties mappings
     * @param value
     *            Nested features
     * @param xpath
     *            Attribute xPath where the client properties are to be set
     * @param targetNodeType
     *            Target node type
     */
    protected void setXlinkReference(Attribute target, Map<Name, Expression> clientPropsMappings, Object value,
            StepList xpath, AttributeType targetNodeType) {
        Expression linkExpression = clientPropsMappings.get(XLINK_HREF_NAME);

        for (Object singleVal : (Collection) value) {
            // Make sure the same value isn't already set
            // in case it comes from a denormalized view for many-to-many relationship.
            // (1) Get the first existing value
            Collection<Property> existingAttributes = getProperties((ComplexAttribute) target, xpath);
            boolean exists = false;

            if (existingAttributes != null) {
                for (Property existingAttribute : existingAttributes) {
                    Object existingValue = existingAttribute.getUserData().get(Attributes.class);
                    if (existingValue != null) {
                        assert existingValue instanceof HashMap;
                        existingValue = ((Map) existingValue).get(XLINK_HREF_NAME);
                    }
                    if (existingValue != null) {
                        Object hrefValue = linkExpression.evaluate(singleVal);
                        if (hrefValue != null && hrefValue.equals(existingValue)) {
                            // (2) if one of the new values matches the first existing value,
                            // that means this comes from a denormalized view,
                            // and this set has already been set
                            exists = true;
                            // stop looking once found
                            break;
                        }
                    }
                }
            }
            if (!exists) {
                Attribute instance = xpathAttributeBuilder.set(target, xpath, null, null, targetNodeType, true,
                        null);
                setClientProperties(instance, singleVal, clientPropsMappings);
            }
        }
    }

    protected List<Feature> setNextFeature(String fId, List<Object> foreignIdValues) throws IOException {
        List<Feature> features = new ArrayList<Feature>();
        features.add(curSrcFeature);
        curSrcFeature = null;

        while (getSourceFeatureIterator().hasNext()) {
            Feature next = getSourceFeatureIterator().next();
            if (extractIdForFeature(next).equals(fId) && checkForeignIdValues(foreignIdValues, next)) {
                // HACK HACK HACK
                // evaluate filter that applies to this list as we want a subset
                // instead of full result
                // this is a temporary solution for Bureau of Meteorology
                // requirement for timePositionList
                if (listFilter != null) {
                    if (listFilter.evaluate(next)) {
                        features.add(next);
                    }
                    // END OF HACK
                } else {
                    features.add(next);
                }
                // HACK HACK HACK
                // evaluate filter that applies to this list as we want a subset
                // instead of full result
                // this is a temporary solution for Bureau of Meteorology
                // requirement for timePositionList
            } else if (listFilter == null || listFilter.evaluate(next)) {
                // END OF HACK
                curSrcFeature = next;
                break;
            }
        }
        return features;
    }

    /**
     * Only used when joining is not used and pre-filter exists because the sources will match
     * the prefilter but there might be denormalised rows with same id that don't.
     * @param fId
     * @param features
     * @throws IOException
     */
    private List<Feature> setNextFilteredFeature(String fId) throws IOException {
        FeatureCollection<? extends FeatureType, ? extends Feature> matchingFeatures;
        Query query = new Query();
        if (reprojection != null) {
            if (sourceFeatures.getSchema().getGeometryDescriptor() != null
                    && !this.isReprojectionCrsEqual(this.mappedSource.getSchema().getCoordinateReferenceSystem(),
                            this.reprojection)) {
                query.setCoordinateSystemReproject(reprojection);
            }
        }

        Filter fidFilter;

        if (mapping.getFeatureIdExpression().equals(Expression.NIL)) {
            // no real feature id mapping,
            // so let's find by database row id
            Set<FeatureId> ids = new HashSet<FeatureId>();
            FeatureId featureId = namespaceAwareFilterFactory.featureId(fId);
            ids.add(featureId);
            fidFilter = namespaceAwareFilterFactory.id(ids);
        } else {
            // in case the expression is wrapped in a function, eg. strConcat
            // that's why we don't always filter by id, but do a PropertyIsEqualTo
            fidFilter = namespaceAwareFilterFactory.equals(mapping.getFeatureIdExpression(),
                    namespaceAwareFilterFactory.literal(fId));
        }

        // HACK HACK HACK
        // evaluate filter that applies to this list as we want a subset
        // instead of full result
        // this is a temporary solution for Bureau of Meteorology
        // requirement for timePositionList
        if (listFilter != null) {
            List<Filter> filters = new ArrayList<Filter>();
            filters.add(listFilter);
            filters.add(fidFilter);
            fidFilter = namespaceAwareFilterFactory.and(filters);
        }
        // END OF HACK

        query.setFilter(fidFilter);
        matchingFeatures = this.mappedSource.getFeatures(query);

        FeatureIterator<? extends Feature> iterator = matchingFeatures.features();

        List<Feature> features = new ArrayList<Feature>();
        while (iterator.hasNext()) {
            features.add(iterator.next());
        }
        // Probably cause there is no primary key nor idExpression
        if (features.isEmpty()) {
            features.add(curSrcFeature);
        }

        filteredFeatures.add(fId);

        iterator.close();

        curSrcFeature = null;

        return features;
    }

    public void skipNestedMapping(AttributeMapping attMapping, List<Feature> sources) throws IOException {
        if (attMapping instanceof JoiningNestedAttributeMapping) {

            for (Feature source : sources) {
                Object value = getValues(attMapping.isMultiValued(), attMapping.getSourceExpression(), source);

                if (value instanceof Collection) {
                    for (Object val : (Collection) value) {
                        ((JoiningNestedAttributeMapping) attMapping).skip(this, val, getIdValues(source));
                    }
                } else {
                    ((JoiningNestedAttributeMapping) attMapping).skip(this, value, getIdValues(source));
                }
            }
        }
    }

    public List<Feature> skip() throws IOException {
        setHasNextCalled(false);

        List<Feature> sources = getSources(extractIdForFeature(curSrcFeature));
        for (AttributeMapping attMapping : selectedMapping) {
            skipNestedMapping(attMapping, sources);
        }
        return sources;
    }

    private GeometryDescriptor reprojectGeometry(GeometryDescriptor descr) {
        if (descr == null) {
            return null;
        }
        GeometryType type = ftf.createGeometryType(descr.getType().getName(), descr.getType().getBinding(),
                reprojection, descr.getType().isIdentified(), descr.getType().isAbstract(),
                descr.getType().getRestrictions(), descr.getType().getSuper(), descr.getType().getDescription());
        type.getUserData().putAll(descr.getType().getUserData());
        GeometryDescriptor gd = ftf.createGeometryDescriptor(type, descr.getName(), descr.getMinOccurs(),
                descr.getMaxOccurs(), descr.isNillable(), descr.getDefaultValue());
        gd.getUserData().putAll(descr.getUserData());
        return gd;
    }

    private FeatureType reprojectType(FeatureType type) {
        Collection<PropertyDescriptor> schema = new ArrayList<PropertyDescriptor>();

        for (PropertyDescriptor descr : type.getDescriptors()) {
            if (descr instanceof GeometryDescriptor) {
                schema.add(reprojectGeometry((GeometryDescriptor) descr));
            } else {
                schema.add(descr);
            }
        }

        FeatureType ft;
        if (type instanceof NonFeatureTypeProxy) {
            ft = new NonFeatureTypeProxy(((NonFeatureTypeProxy) type).getSubject(), mapping, schema);
        } else {
            ft = ftf.createFeatureType(type.getName(), schema, reprojectGeometry(type.getGeometryDescriptor()),
                    type.isAbstract(), type.getRestrictions(), type.getSuper(), type.getDescription());
        }
        ft.getUserData().putAll(type.getUserData());
        return ft;
    }

    private AttributeDescriptor reprojectAttribute(AttributeDescriptor descr) {

        if (reprojection != null && descr.getType() instanceof FeatureType) {
            AttributeDescriptor ad = ftf.createAttributeDescriptor(reprojectType((FeatureType) descr.getType()),
                    descr.getName(), descr.getMinOccurs(), descr.getMaxOccurs(), descr.isNillable(),
                    descr.getDefaultValue());
            ad.getUserData().putAll(descr.getUserData());
            return ad;
        } else {
            return descr;
        }
    }

    protected Feature computeNext() throws IOException {

        String id = getNextFeatureId();
        List<Feature> sources = getSources(id);

        final Name targetNodeName = targetFeature.getName();

        AttributeBuilder builder = new AttributeBuilder(attf);
        builder.setDescriptor(targetFeature);
        Feature target = (Feature) builder.build(id);

        for (AttributeMapping attMapping : selectedMapping) {
            try {
                if (skipTopElement(targetNodeName, attMapping, targetFeature.getType())) {
                    // ignore the top level mapping for the Feature itself
                    // as it was already set
                    continue;
                }
                if (attMapping.isList()) {
                    Attribute instance = setAttributeValue(target, null, sources.get(0), attMapping, null, null,
                            selectedProperties.get(attMapping));
                    if (sources.size() > 1 && instance != null) {
                        List<Object> values = new ArrayList<Object>();
                        Expression sourceExpr = attMapping.getSourceExpression();
                        for (Feature source : sources) {
                            values.add(getValue(sourceExpr, source));
                        }
                        String valueString = StringUtils.join(values.iterator(), " ");
                        StepList fullPath = attMapping.getTargetXPath();
                        StepList leafPath = fullPath.subList(fullPath.size() - 1, fullPath.size());
                        if (instance instanceof ComplexAttributeImpl) {
                            // xpath builder will work out the leaf attribute to set values on
                            xpathAttributeBuilder.set(instance, leafPath, valueString, null, null, false,
                                    sourceExpr);
                        } else {
                            // simple attributes
                            instance.setValue(valueString);
                        }
                    }
                } else if (attMapping.isMultiValued()) {
                    // extract the values from multiple source features of the same id
                    // and set them to one built feature
                    for (Feature source : sources) {
                        setAttributeValue(target, null, source, attMapping, null, null,
                                selectedProperties.get(attMapping));
                    }
                } else {
                    String indexString = attMapping.getSourceIndex();
                    // if not specified, get the first row by default
                    int index = 0;
                    if (indexString != null) {
                        if (ComplexFeatureConstants.LAST_INDEX.equals(indexString)) {
                            index = sources.size() - 1;
                        } else {
                            index = Integer.parseInt(indexString);
                        }
                    }
                    setAttributeValue(target, null, sources.get(index), attMapping, null, null,
                            selectedProperties.get(attMapping));
                    // When a feature is not multi-valued but still has multiple rows with the same ID in
                    // a denormalised table, by default app-schema only takes the first row and ignores
                    // the rest (see above). The following line is to make sure that the cursors in the
                    // 'joining nested mappings'skip any extra rows that were linked to those rows that are being ignored.
                    // Otherwise the cursor will stay there in the wrong spot and none of the following feature chaining
                    // will work. That can really only occur if the foreign key is not unique for the ID of the parent
                    // feature (otherwise all of those rows would be already passed when creating the feature based on
                    // the first row). This never really occurs in practice I have noticed, but it is a theoretic
                    // possibility, as there is no requirement for the foreign key to be unique per id.
                    skipNestedMapping(attMapping, sources.subList(1, sources.size()));
                }
            } catch (Exception e) {
                throw new RuntimeException(
                        "Error applying mapping with targetAttribute " + attMapping.getTargetXPath(), e);
            }
        }
        cleanEmptyElements(target);

        return target;
    }

    /**
     * Get all source features of the provided id. This assumes the source features are grouped by
     * id.
     * 
     * @param id
     *            The feature id
     * @return list of source features
     * @throws IOException
     */
    protected List<Feature> getSources(String id) throws IOException {
        if (isFiltered) {
            return setNextFilteredFeature(id);
        } else {
            return setNextFeature(id, getForeignIdValues(curSrcFeature));
        }
    }

    protected String getNextFeatureId() {
        return extractIdForFeature(curSrcFeature);
    }

    protected void cleanEmptyElements(Feature target) throws DataSourceException {
        try {
            ArrayList values = new ArrayList<Property>();
            for (Iterator i = target.getValue().iterator(); i.hasNext();) {
                Property p = (Property) i.next();
                if (hasChild(p) || p.getDescriptor().getMinOccurs() > 0 || getEncodeIfEmpty(p)) {
                    values.add(p);
                }
            }
            target.setValue(values);
        } catch (DataSourceException e) {
            throw new DataSourceException("Unable to clean empty element", e);
        }
    }

    private boolean hasChild(Property p) throws DataSourceException {
        boolean result = false;
        if (p.getValue() instanceof Collection) {

            Collection c = (Collection) p.getValue();

            if (this.getClientProperties(p).containsKey(XLINK_HREF_NAME)) {
                return true;
            }

            ArrayList values = new ArrayList();
            for (Object o : c) {
                if (o instanceof Property) {
                    if (hasChild((Property) o)) {
                        values.add(o);
                        result = true;
                    } else if (getEncodeIfEmpty((Property) o)) {
                        values.add(o);
                        result = true;
                    } else if (((Property) o).getDescriptor().getMinOccurs() > 0) {
                        if (((Property) o).getDescriptor().isNillable()) {
                            // add nil mandatory property
                            values.add(o);
                        }
                    }
                }
            }
            p.setValue(values);
        } else if (p.getName().equals(ComplexFeatureConstants.FEATURE_CHAINING_LINK_NAME)) {
            // ignore fake attribute FEATURE_LINK
            result = false;
        } else if (p.getValue() != null && p.getValue().toString().length() > 0) {
            result = true;
        }
        return result;
    }

    protected boolean skipTopElement(Name topElement, AttributeMapping attMapping, AttributeType type) {
        // don't skip if there's OCQL
        return XPath.equals(topElement, attMapping.getTargetXPath()) && (attMapping.getSourceExpression() == null
                || Expression.NIL.equals(attMapping.getSourceExpression()));
    }

    protected Feature populateFeatureData(String id) throws IOException {
        throw new UnsupportedOperationException("populateFeatureData should not be called!");
    }

    protected void closeSourceFeatures() {
        if (sourceFeatures != null && getSourceFeatureIterator() != null) {
            sourceFeatureIterator.close();
            sourceFeatureIterator = null;
            sourceFeatures = null;
            filteredFeatures = null;
            listFilter = null;

            //NC - joining nested atts
            for (AttributeMapping attMapping : selectedMapping) {
                if (attMapping instanceof JoiningNestedAttributeMapping) {
                    ((JoiningNestedAttributeMapping) attMapping).close(this);

                }
            }
        }
    }

    protected Object getValue(final Expression expression, Object sourceFeature) {
        Object value = expression.evaluate(sourceFeature);
        if (value instanceof Attribute) {
            value = ((Attribute) value).getValue();
        }
        return value;
    }

    /**
     * Returns first matching attribute from provided root and xPath.
     *
     * @param root
     *            The root attribute to start searching from
     * @param xpath
     *            The xPath matching the attribute
     * @return The first matching attribute
     */
    private Property getProperty(Attribute root, StepList xpath) {
        Property property = root;

        final StepList steps = new StepList(xpath);

        Iterator<Step> stepsIterator = steps.iterator();

        while (stepsIterator.hasNext()) {
            assert property instanceof ComplexAttribute;
            Step step = stepsIterator.next();
            property = ((ComplexAttribute) property).getProperty(Types.toTypeName(step.getName()));
            if (property == null) {
                return null;
            }
        }
        return property;
    }

    /**
     * Return all matching properties from provided root attribute and xPath.
     *
     * @param root
     *            The root attribute to start searching from
     * @param xpath
     *            The xPath matching the attribute
     * @return The matching attributes collection
     */
    private Collection<Property> getProperties(ComplexAttribute root, StepList xpath) {

        final StepList steps = new StepList(xpath);

        Iterator<Step> stepsIterator = steps.iterator();
        Collection<Property> properties = null;
        Step step = null;
        if (stepsIterator.hasNext()) {
            step = stepsIterator.next();
            properties = ((ComplexAttribute) root).getProperties(Types.toTypeName(step.getName()));
        }

        while (stepsIterator.hasNext()) {
            step = stepsIterator.next();
            Collection<Property> nestedProperties = new ArrayList<Property>();
            for (Property property : properties) {
                assert property instanceof ComplexAttribute;
                Collection<Property> tempProperties = ((ComplexAttribute) property)
                        .getProperties(Types.toTypeName(step.getName()));
                if (!tempProperties.isEmpty()) {
                    nestedProperties.addAll(tempProperties);
                }
            }
            properties.clear();
            if (nestedProperties.isEmpty()) {
                return properties;
            }
            properties.addAll(nestedProperties);
        }
        return properties;
    }

    /**
     * Checks if client property has xlink:ref in it, if the attribute is for chained features.
     *
     * @param clientPropsMappings
     *            the client properties mappings
     * @param isNested
     *            true if we're dealing with chained/nested features
     * @return
     */
    protected boolean isByReference(Map<Name, Expression> clientPropsMappings, boolean isNested) {
        // only care for chained features
        return isNested ? (clientPropsMappings.isEmpty() ? false
                : (clientPropsMappings.get(XLINK_HREF_NAME) == null) ? false : true) : false;
    }

    /**
     * Returns the declared CRS given the native CRS and the request WFS version
     * 
     * @param nativeCRS
     * @param wfsVersion
     * @return
     */
    private CoordinateReferenceSystem getDeclaredCrs(CoordinateReferenceSystem nativeCRS, String wfsVersion) {
        try {
            if (nativeCRS == null)
                return null;

            if (wfsVersion.equals("1.0.0")) {
                return nativeCRS;
            } else {
                String code = GML2EncodingUtils.epsgCode(nativeCRS);
                //it's possible that we can't do the CRS -> code -> CRS conversion...so we'll just return what we have
                if (code == null)
                    return nativeCRS;
                return CRS.decode("urn:x-ogc:def:crs:EPSG:6.11.2:" + code);
            }
        } catch (Exception e) {
            throw new UnsupportedOperationException("We have had issues trying to flip axis of " + nativeCRS, e);
        }
    }

    public boolean isReprojectionCrsEqual(CoordinateReferenceSystem source, CoordinateReferenceSystem target) {
        return CRS.equalsIgnoreMetadata(source, target);
    }

    public void setListFilter(Filter filter) {
        listFilter = filter;
    }

    private boolean getEncodeIfEmpty(Property p) {
        Object o = ((p.getDescriptor()).getUserData().get("encodeIfEmpty"));
        if (o == null) {
            return false;
        }
        return (Boolean) o;
    }
}