/* * 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.Collections; import java.util.HashMap; import java.util.List; import java.util.Map; import java.util.logging.Logger; import net.opengis.wfs20.ResolveValueType; import org.geotools.data.DataSourceException; import org.geotools.data.Query; import org.geotools.data.complex.config.Types; import org.geotools.data.complex.filter.XPath; import org.geotools.data.complex.filter.XPathUtil.StepList; import org.geotools.data.joining.JoiningQuery; import org.geotools.factory.CommonFactoryFinder; import org.geotools.factory.Hints; import org.geotools.feature.AppSchemaFeatureFactoryImpl; import org.geotools.feature.FeatureIterator; import org.geotools.feature.type.ComplexFeatureTypeFactoryImpl; import org.geotools.filter.FilterFactoryImplNamespaceAware; import org.geotools.filter.identity.FeatureIdImpl; 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.AttributeType; import org.opengis.feature.type.FeatureTypeFactory; import org.opengis.feature.type.PropertyDescriptor; import org.opengis.feature.GeometryAttribute; import org.opengis.feature.Property; 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"); protected FilterFactory2 filterFac = CommonFactoryFinder.getFilterFactory2(null); protected FeatureTypeFactory ftf = new ComplexFeatureTypeFactoryImpl(); /** * Name representation of xlink:href */ public static final Name XLINK_HREF_NAME = Types.toTypeName(XLINK.HREF); /** * Milliseconds between polls of resolver thread. */ public static final long RESOLVE_TIMEOUT_POLL_INTERVAL = 100; /** * 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. After the data query has run, *this* * limit is also applied to the result. */ protected final int requestMaxFeatures; /** * maximum number of features to request when running the data(base?) query. For denormalised * data sources, this neesd to be be Query.DEFAULT_MAX to trigger a full table scan. In all * other cases it will be the same value as requestMaxFeatures */ protected final int dataMaxFeatures; /** counter to ensure maxFeatures is not exceeded */ protected int featureCounter; protected NamespaceSupport namespaces; protected int resolveDepth; protected Integer resolveTimeOut; /** * 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); } public AbstractMappingFeatureIterator(AppSchemaDataAccess store, FeatureTypeMapping mapping, Query query, Query unrolledQuery) throws IOException { this(store, mapping, query, unrolledQuery, false, false); } public AbstractMappingFeatureIterator(AppSchemaDataAccess store, FeatureTypeMapping mapping, Query query, Query unrolledQuery, boolean hasPostFilter) throws IOException { this(store, mapping, query, unrolledQuery, false, hasPostFilter); } //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, boolean removeQueryLimitIfDenormalised, boolean hasPostFilter) throws IOException { this.store = store; this.attf = new AppSchemaFeatureFactoryImpl(); this.mapping = mapping; // validate and initialise resolve options Hints hints = query.getHints(); ResolveValueType resolveVal = (ResolveValueType) hints.get( Hints.RESOLVE ); boolean resolve = ResolveValueType.ALL.equals(resolveVal) || ResolveValueType.LOCAL.equals(resolveVal); if (!resolve && resolveVal!=null && !ResolveValueType.NONE.equals(resolveVal)) { throw new IllegalArgumentException("Resolve:" + resolveVal.getName() + " is not supported in app-schema!"); } Integer atd = (Integer) hints.get(Hints.ASSOCIATION_TRAVERSAL_DEPTH); resolveDepth = resolve ? atd==null? 0 : atd : 0; resolveTimeOut = (Integer) hints.get( Hints.RESOLVE_TIMEOUT ); namespaces = mapping.getNamespaces(); namespaceAwareFilterFactory = new FilterFactoryImplNamespaceAware(namespaces); Object includeProps = query.getHints().get(Query.INCLUDE_MANDATORY_PROPS); includeMandatory = includeProps instanceof Boolean && ((Boolean)includeProps).booleanValue(); if (mapping.isDenormalised()) { // we need to disable the max number of features retrieved so we can // sort them manually just in case the data is denormalised. Do this // by overriding the max features for this query just before executing // it. Note that the original maxFeatures value was copied to // this.requestMaxFeatures in the constructor and will be re-applied after // the rows have been returned if (removeQueryLimitIfDenormalised) { this.dataMaxFeatures = 1000000; if (hasPostFilter) { // true max features will be handled in PostFilteringMappingFeatureIterator this.requestMaxFeatures = 1000000; } else { this.requestMaxFeatures = query.getMaxFeatures(); } } else { this.dataMaxFeatures = query.getMaxFeatures(); this.requestMaxFeatures = query.getMaxFeatures(); } } else { this.requestMaxFeatures = query.getMaxFeatures(); this.dataMaxFeatures = query.getMaxFeatures(); } if (unrolledQuery == null) { unrolledQuery = getUnrolledQuery(query); if (query instanceof JoiningQuery && unrolledQuery instanceof JoiningQuery) { ((JoiningQuery) unrolledQuery).setRootMapping(((JoiningQuery) query) .getRootMapping()); } } // 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... } xpathAttributeBuilder = new XPath(); xpathAttributeBuilder.setFeatureFactory(attf); initialiseSourceFeatures(mapping, unrolledQuery, query.getCoordinateSystemReproject()); 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; if (requestedProperty.getNamespaceContext() == null) { requestedPropertySteps = XPath.steps(targetDescriptor, requestedProperty.getPropertyName(), namespaces); } else { requestedPropertySteps = 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 (!hasNext()) { throw new IllegalStateException("there are no more features in this iterator"); } Feature next; try { next = computeNext(); } catch (IOException e) { close(); throw new RuntimeException(e); } ++featureCounter; setHasNextCalled(false); return next; } 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; } private class FeatureFinder implements Runnable { private Feature feature = null; private String refId; private Hints hints; public Feature getFeature() { return feature; } public FeatureFinder(String refId, Hints hints) { this.refId = refId; this.hints = hints; } @Override public void run() { try { feature = DataAccessRegistry.getInstance().findFeature(new FeatureIdImpl(refId), hints); } catch (IOException e) { // ignore, no resolve } } }; protected static String referenceToIdentifier(String reference) { //TODO: support custom rules in mapping file String[] urn = reference.split(":"); String lastPart = urn[urn.length - 1]; if (lastPart.contains("#")) { lastPart = lastPart.substring(lastPart.lastIndexOf("#")); } if ("missing".equals(urn[urn.length - 1]) || "unknown".equals(urn[urn.length - 1])) { return null; } return lastPart; } protected Attribute setAttributeContent(Attribute target, StepList xpath, Object value, String id, AttributeType targetNodeType, boolean isXlinkRef, Expression sourceExpression, Object source, final Map<Name, Expression> clientProperties, boolean ignoreXlinkHref) { Attribute instance = null; Map<Name, Expression> properties = new HashMap<Name, Expression>(clientProperties); if (ignoreXlinkHref) { properties.remove(XLINK_HREF_NAME); } if (properties.containsKey(XLINK_HREF_NAME) && resolveDepth > 0) { // local resolve String refid = referenceToIdentifier(getValue(properties.get(XLINK_HREF_NAME), source) .toString()); if (refid != null) { final Hints hints = new Hints(); if (resolveDepth > 1) { hints.put(Hints.RESOLVE, ResolveValueType.ALL); // only the top-level resolve thread should monitor timeout hints.put(Hints.RESOLVE_TIMEOUT, Integer.MAX_VALUE); hints.put(Hints.ASSOCIATION_TRAVERSAL_DEPTH, resolveDepth - 1); } else { hints.put(Hints.RESOLVE, ResolveValueType.NONE); } // let's try finding it FeatureFinder finder = new FeatureFinder(refid, hints); // this will be null if joining or sleeping is interrupted Feature foundFeature = null; if (resolveTimeOut == Integer.MAX_VALUE) { // not the top-level resolve thread so do not monitor timeout finder.run(); foundFeature = finder.getFeature(); } else { Thread thread = new Thread(finder); long startTime = System.currentTimeMillis(); thread.start(); try { while (thread.isAlive() && (System.currentTimeMillis() - startTime) / 1000 < resolveTimeOut) { Thread.sleep(RESOLVE_TIMEOUT_POLL_INTERVAL); } thread.interrupt(); // joining ensures synchronisation thread.join(); foundFeature = finder.getFeature(); } catch (InterruptedException e) { // clean up as best we can thread.interrupt(); throw new RuntimeException("Interrupted while resolving resource " + refid); } } if (foundFeature != null) { // found it instance = xpathAttributeBuilder.set(target, xpath, Collections.singletonList(foundFeature), id, targetNodeType, false, sourceExpression); properties.remove(XLINK_HREF_NAME); } } } if (instance == null) { instance = xpathAttributeBuilder.set(target, xpath, value, id, targetNodeType, false, sourceExpression); } setClientProperties(instance, source, properties); return instance; } 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 FeatureIterator<? extends Feature> getSourceFeatureIterator(); protected abstract void initialiseSourceFeatures(FeatureTypeMapping mapping, Query query, CoordinateReferenceSystem crs) 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(); }