Package org.geotools.data.complex

Source Code of org.geotools.data.complex.DataAccessMappingFeatureIterator

/*
*    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.Literal;
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;

    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;
   
    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, boolean removeQueryLimitIfDenormalised) throws IOException {
        this(store, mapping, query, isFiltered, removeQueryLimitIfDenormalised, false);       
    }
   
    public DataAccessMappingFeatureIterator(AppSchemaDataAccess store, FeatureTypeMapping mapping,
            Query query, boolean isFiltered, boolean removeQueryLimitIfDenormalised, boolean hasPostFilter) throws IOException {
        super(store, mapping, query, null, removeQueryLimitIfDenormalised, hasPostFilter);
        this.isFiltered = isFiltered;
        if (isFiltered) {
            filteredFeatures = new ArrayList<String>();
        }
    }
   
    public DataAccessMappingFeatureIterator(AppSchemaDataAccess store, FeatureTypeMapping mapping,
            Query query) throws IOException {
        this(store, mapping, query, null, false);
    }

    /**
     *
     * @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, boolean removeQueryLimitIfDenormalised) throws IOException {
        super(store, mapping, query, unrolledQuery, removeQueryLimitIfDenormalised);
    }

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

        if (!isHasNextCalled()) {
            if (featureCounter < requestMaxFeatures) {
                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>();
        Expression idExpression = mapping.getFeatureIdExpression();
        if (Expression.NIL.equals(idExpression) || idExpression instanceof Literal) {
            // GEOT-4554: if idExpression is not specified, should use PK
            if (source instanceof Feature) {
                for (Property p : ((Feature) source).getProperties()) {
                    if (p.getName().getLocalPart().startsWith(JoiningJDBCFeatureSource.PRIMARY_KEY)) {
                        ids.add(p.getValue());
                    }
                }
            }
        } else {
            FilterAttributeExtractor extractor = new FilterAttributeExtractor();
            idExpression.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
        // GEOT-4550: exclude this part for WMS requests because the reprojection happens during rendering
        // not at ReprojectingFilterVisitor.
        // The original CRS should be preserved so the reprojection could happen at rendering.
        if (targetCRS == null && version != null && !version.contains("wms")) {
            // 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());
        query.setMaxFeatures(dataMaxFeatures);
        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, mapping);

            }

        }

    }

    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
          if (attMapping instanceof JoiningNestedAttributeMapping) {
            // GEOT-4417: update skipped ids when skipping with
            // toXlinkHref
            if (values == null && source != null) {
              values = getValues(attMapping.isMultiValued(),
                  sourceExpression, source);
            }
            if (values != null) {
              List<Object> idValues = getIdValues(source);
              if (values instanceof Collection) {
                for (Object singleVal : (Collection) values) {
                  ((JoiningNestedAttributeMapping) attMapping)
                      .skip(this, singleVal, idValues);
                }
              } else {
                ((JoiningNestedAttributeMapping) attMapping)
                    .skip(this, values, idValues);
              }
            }
          }
          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);
        int newResolveDepth = resolveDepth;
        //if resolving, no xlink:href for chained feature
        boolean ignoreXlinkHref = false;
        if (isHRefLink && newResolveDepth > 0) {
          isHRefLink = false;
          newResolveDepth--;
          ignoreXlinkHref = true;               
        }
        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, newResolveDepth, resolveTimeOut));
                    }
                }
                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, newResolveDepth, resolveTimeOut);
            }
            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 = setAttributeContent(target, xpath, valueList, id, targetNodeType, false, sourceExpression, source, clientPropsMappings, ignoreXlinkHref);
            }
        } 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 = setAttributeContent(target, xpath, values, id, targetNodeType, false, sourceExpression, source, clientPropsMappings, ignoreXlinkHref);

        }
        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
     * @param attMapping
     * @param list
     * @throws IOException
     */
    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;
    }
}
TOP

Related Classes of org.geotools.data.complex.DataAccessMappingFeatureIterator

TOP
Copyright © 2018 www.massapi.com. All rights reserved.
All source code are property of their respective owners. Java is a trademark of Sun Microsystems, Inc and owned by ORACLE Inc. Contact coftware#gmail.com.