/*
* GeoTools - The Open Source Java GIS Toolkit
* http://geotools.org
*
* (C) 2009-2011, Open Source Geospatial Foundation (OSGeo)
*
* This library is free software; you can redistribute it and/or
* modify it under the terms of the GNU Lesser General Public
* License as published by the Free Software Foundation;
* version 2.1 of the License.
*
* This library is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
* Lesser General Public License for more details.
*/
package org.geotools.data.complex;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Collection;
import java.util.HashMap;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.logging.Logger;
import org.geotools.data.DataSourceException;
import org.geotools.data.Query;
import org.geotools.data.complex.filter.XPath;
import org.geotools.data.complex.filter.XPath.StepList;
import org.geotools.factory.CommonFactoryFinder;
import org.geotools.feature.AppSchemaFeatureFactoryImpl;
import org.geotools.feature.Types;
import org.geotools.filter.FilterFactoryImplNamespaceAware;
import org.geotools.xlink.XLINK;
import org.opengis.feature.Attribute;
import org.opengis.feature.Feature;
import org.opengis.feature.FeatureFactory;
import org.opengis.feature.type.AttributeDescriptor;
import org.opengis.feature.type.PropertyDescriptor;
import org.opengis.feature.GeometryAttribute;
import org.opengis.feature.Property;
import org.opengis.feature.simple.SimpleFeature;
import org.opengis.feature.type.Name;
import org.opengis.filter.FilterFactory;
import org.opengis.filter.FilterFactory2;
import org.opengis.filter.expression.Expression;
import org.opengis.filter.expression.PropertyName;
import org.opengis.referencing.crs.CoordinateReferenceSystem;
import org.xml.sax.Attributes;
import org.xml.sax.helpers.NamespaceSupport;
import com.vividsolutions.jts.geom.EmptyGeometry;
import com.vividsolutions.jts.geom.Geometry;
/**
* Base class for several MappingFeatureImplementation's.
*
* @author Russell Petty (GeoScience Victoria)
* @version $Id$
*
*
* @source $URL$
*/
public abstract class AbstractMappingFeatureIterator implements IMappingFeatureIterator {
/** The logger for the filter module. */
protected static final Logger LOGGER = org.geotools.util.logging.Logging
.getLogger("org.geotools.data.complex");
private FilterFactory2 filterFac = CommonFactoryFinder.getFilterFactory2(null);
/**
* Name representation of xlink:href
*/
public static final Name XLINK_HREF_NAME = Types.toTypeName(XLINK.HREF);
/**
* The mappings for the source and target schemas
*/
protected FeatureTypeMapping mapping;
/**
* Mappings after Property Selection is applied
*/
protected List<AttributeMapping> selectedMapping;
/**
* Selected Properties for Feature Chaining
*/
protected Map<AttributeMapping, List<PropertyName>> selectedProperties;
protected boolean includeMandatory;
/**
* Factory used to create the target feature and attributes
*/
protected FeatureFactory attf;
protected AppSchemaDataAccess store;
final protected XPath xpathAttributeBuilder;
protected FilterFactory namespaceAwareFilterFactory;
/**
* maxFeatures restriction value as provided by query
*/
protected final int maxFeatures;
/** counter to ensure maxFeatures is not exceeded */
protected int featureCounter;
protected NamespaceSupport namespaces;
/**
* True if hasNext has been called prior to calling next()
*/
private boolean hasNextCalled = false;
public AbstractMappingFeatureIterator(AppSchemaDataAccess store, FeatureTypeMapping mapping,
Query query) throws IOException {
this(store, mapping, query, null);
}
//NC - changed
//possibility to pass on both query and unrolled query
//so that property names can be taken out of query, also when a custom unrolled query is passed.
//one of them can be null, but not both!
public AbstractMappingFeatureIterator(AppSchemaDataAccess store, FeatureTypeMapping mapping,
Query query, Query unrolledQuery) throws IOException {
this.store = store;
this.attf = new AppSchemaFeatureFactoryImpl();
this.mapping = mapping;
namespaces = mapping.getNamespaces();
namespaceAwareFilterFactory = new FilterFactoryImplNamespaceAware(namespaces);
Object includeProps = query.getHints().get(Query.INCLUDE_MANDATORY_PROPS);
includeMandatory = includeProps instanceof Boolean && ((Boolean)includeProps).booleanValue();
// NC - property names
if (query != null && query.getProperties() != null) {
setPropertyNames(query.getProperties());
} else {
setPropertyNames(null); // we need the actual property names (not surrogates) to do
// this...
}
this.maxFeatures = query.getMaxFeatures();
if (unrolledQuery==null) {
unrolledQuery = getUnrolledQuery(query);
}
xpathAttributeBuilder = new XPath();
xpathAttributeBuilder.setFeatureFactory(attf);
initialiseSourceFeatures(mapping, unrolledQuery);
xpathAttributeBuilder.setFilterFactory(namespaceAwareFilterFactory);
}
//properties can only be set by constructor, before initialising source features
//(for joining nested mappings)
private void setPropertyNames(Collection<PropertyName> propertyNames) {
selectedProperties = new HashMap<AttributeMapping, List<PropertyName>>();
if (propertyNames == null) {
selectedMapping = mapping.getAttributeMappings();
} else {
final AttributeDescriptor targetDescriptor = mapping.getTargetFeature();
selectedMapping = new ArrayList<AttributeMapping>();
for (AttributeMapping attMapping : mapping.getAttributeMappings()) {
final StepList targetSteps = attMapping.getTargetXPath();
boolean alreadyAdded = false;
if (includeMandatory) {
PropertyName targetProp = namespaceAwareFilterFactory.property(targetSteps
.toString());
Object descr = targetProp.evaluate(targetDescriptor.getType());
if (descr instanceof PropertyDescriptor) {
if (((PropertyDescriptor) descr).getMinOccurs() >= 1) {
selectedMapping.add(attMapping);
selectedProperties.put(attMapping, new ArrayList<PropertyName>());
alreadyAdded = true;
}
}
}
for (PropertyName requestedProperty : propertyNames) {
StepList requestedPropertySteps = requestedProperty.getNamespaceContext() == null ? null
: XPath.steps(targetDescriptor, requestedProperty.getPropertyName(),
requestedProperty.getNamespaceContext());
if (requestedPropertySteps == null ? AppSchemaDataAccess.matchProperty(
requestedProperty.getPropertyName(), targetSteps) : AppSchemaDataAccess
.matchProperty(requestedPropertySteps, targetSteps)) {
if (!alreadyAdded) {
selectedMapping.add(attMapping);
selectedProperties.put(attMapping, new ArrayList<PropertyName>());
alreadyAdded = true;
}
if (requestedPropertySteps != null
&& requestedPropertySteps.size() > targetSteps.size()) {
List<PropertyName> pnList = selectedProperties.get(attMapping);
StepList subProperty = requestedPropertySteps.subList(
targetSteps.size(), requestedPropertySteps.size());
pnList.add(filterFac.property(subProperty.toString(),
requestedProperty.getNamespaceContext()));
}
}
}
}
}
}
/**
* Shall not be called, just throws an UnsupportedOperationException
*/
public void remove() {
throw new UnsupportedOperationException();
}
/**
* Closes the underlying FeatureIterator
*/
public void close() {
closeSourceFeatures();
}
/**
* Based on the set of xpath expression/id extracting expression, finds the ID for the attribute
* <code>idExpression</code> from the source complex attribute.
*
* @param idExpression
* the location path of the attribute to be created, for which to obtain the id by
* evaluating the corresponding <code>org.geotools.filter.Expression</code> from
* <code>sourceInstance</code>.
* @param sourceInstance
* a complex attribute which is the source of the mapping.
* @return the ID to be applied to a new attribute instance addressed by
* <code>attributeXPath</code>, or <code>null</code> if there is no an id mapping for
* that attribute.
*/
protected abstract String extractIdForAttribute(final Expression idExpression,
Object sourceInstance);
/**
* Return a query appropriate to its underlying feature source.
*
* @param query
* the original query against the output schema
* @return a query appropriate to be executed over the underlying feature source.
*/
protected Query getUnrolledQuery(Query query) {
return store.unrollQuery(query, mapping);
}
protected boolean isHasNextCalled() {
return hasNextCalled;
}
protected void setHasNextCalled(boolean hasNextCalled) {
this.hasNextCalled = hasNextCalled;
}
/**
* Return next feature.
*
* @see java.util.Iterator#next()
*/
public Feature next() {
if (!isHasNextCalled()) {
LOGGER.warning("hasNext not called before calling next() in the iterator!");
if (!hasNext()) {
return null;
}
}
Feature next;
try {
next = computeNext();
this.cleanEmptyElements(next);
} catch (IOException e) {
close();
throw new RuntimeException(e);
}
++featureCounter;
return next;
}
private 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) {
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 (((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 Map getClientProperties(Property attribute) throws DataSourceException {
Map<Object, Object> userData = attribute.getUserData();
Map clientProperties = new HashMap<Name, Expression>();
if (userData != null && userData.containsKey(Attributes.class)) {
Map props = (Map) userData.get(Attributes.class);
if (!props.isEmpty()) {
clientProperties.putAll(props);
}
}
return clientProperties;
}
protected void setClientProperties(final Attribute target, final Object source,
final Map<Name, Expression> clientProperties) {
if (target == null) {
return;
}
if (source == null && clientProperties.isEmpty()) {
return;
}
// NC - first calculate target attributes
final Map<Name, Object> targetAttributes = new HashMap<Name, Object>();
if (target.getUserData().containsValue(Attributes.class)) {
targetAttributes.putAll((Map<? extends Name, ? extends Object>) target.getUserData()
.get(Attributes.class));
}
for (Map.Entry<Name, Expression> entry : clientProperties.entrySet()) {
Name propName = entry.getKey();
Object propExpr = entry.getValue();
Object propValue;
if (propExpr instanceof Expression) {
propValue = getValue((Expression) propExpr, source);
} else {
propValue = propExpr;
}
if (propValue != null) {
if (propValue instanceof Collection) {
if (!((Collection)propValue).isEmpty()) {
propValue = ((Collection)propValue).iterator().next();
targetAttributes.put(propName, propValue);
}
} else {
targetAttributes.put(propName, propValue);
}
}
}
// FIXME should set a child Property.. but be careful for things that
// are smuggled in there internally and don't exist in the schema, like
// XSDTypeDefinition, CRS etc.
if (targetAttributes.size() > 0) {
target.getUserData().put(Attributes.class, targetAttributes);
}
setGeometryUserData(target, targetAttributes);
}
protected void setGeometryUserData(Attribute target, Map<Name, Object> targetAttributes) {
// with geometry objects, set ID and attributes in geometry object
if (target instanceof GeometryAttribute
&& (targetAttributes.size() > 0 || target.getIdentifier() != null)) {
Geometry geom;
if (target.getValue() == null) {
// create empty geometry if null but attributes
geom = new EmptyGeometry();
} else {
// need to clone because it seems the same geometry object from the
// db is reused instead of regenerated if different attributes refer
// to the same database row... so if we change the userData, we have
// to clone it
geom = (Geometry) ((Geometry) target.getValue()).clone();
}
if (geom != null) {
Object userData = geom.getUserData();
Map newUserData = new HashMap<Object, Object>();
if (userData != null) {
if (userData instanceof Map) {
newUserData.putAll((Map) userData);
} else if (userData instanceof CoordinateReferenceSystem) {
newUserData.put(CoordinateReferenceSystem.class, userData);
}
}
// set gml:id and attributes in Geometry userData
if (target.getIdentifier() != null) {
newUserData.put("gml:id", target.getIdentifier().toString());
}
if (targetAttributes.size() > 0) {
newUserData.put(Attributes.class, targetAttributes);
}
geom.setUserData(newUserData);
target.setValue(geom);
}
}
}
protected abstract void closeSourceFeatures();
protected abstract Iterator<SimpleFeature> getSourceFeatureIterator();
protected abstract void initialiseSourceFeatures(FeatureTypeMapping mapping, Query query)
throws IOException;
protected abstract boolean unprocessedFeatureExists();
protected abstract boolean sourceFeatureIteratorHasNext();
protected abstract boolean isNextSourceFeatureNull();
protected abstract Feature populateFeatureData(String id) throws IOException;
protected abstract Object getValue(Expression expression, Object sourceFeature);
protected abstract boolean isSourceFeatureIteratorNull();
protected abstract Feature computeNext() throws IOException;
public abstract boolean hasNext();
}