/* * 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.filter; import java.io.IOException; import java.util.ArrayList; import java.util.Collection; import java.util.List; import java.util.Map; import javax.xml.namespace.QName; import org.geotools.data.complex.AppSchemaDataAccessRegistry; import org.geotools.data.complex.AttributeMapping; import org.geotools.data.complex.FeatureTypeMapping; import org.geotools.data.complex.NestedAttributeMapping; import org.geotools.data.complex.filter.XPath; import org.geotools.data.complex.filter.XPath.Step; import org.geotools.data.complex.filter.XPath.StepList; import org.geotools.factory.CommonFactoryFinder; import org.geotools.feature.Types; import org.geotools.filter.AttributeExpressionImpl; import org.geotools.xlink.XLINK; import org.opengis.feature.Attribute; import org.opengis.feature.Feature; import org.opengis.feature.type.Name; import org.opengis.filter.expression.Expression; /** * This class represents a list of expressions broken up from a single XPath expression that is * nested in more than one feature. The purpose is to allow filtering these attributes on the parent * feature. * * @author Rini Angreani (CSIRO Earth Science and Resource Engineering) * * * * * @source $URL$ * http://svn.osgeo.org/geotools/trunk/modules/unsupported/app-schema/app-schema/src/main * /java/org/geotools/filter/NestedAttributeExpression.java $ */ public class NestedAttributeExpression extends AttributeExpressionImpl { private FeatureTypeMapping mappings; private StepList fullSteps; /** * Used for polymorphism. Would be null if mappings is known. Otherwise used to determine the * FeatureTypeMapping to use, depending on the function evaluation upon a feature. */ // private Expression function; /** * First constructor * * @param xpath * Attribute XPath * @param expressions * List of broken up expressions */ public NestedAttributeExpression(String xpath, FeatureTypeMapping mappings) { super(xpath); this.mappings = mappings; fullSteps = XPath.steps(mappings.getTargetFeature(), this.attPath.toString(), mappings .getNamespaces()); } public NestedAttributeExpression(StepList xpath, FeatureTypeMapping mappings) { super(xpath.toString()); this.mappings = mappings; fullSteps = xpath; } public NestedAttributeExpression(Expression expression, FeatureTypeMapping mappings) { super(expression.toString()); this.mappings = mappings; fullSteps = XPath.steps(mappings.getTargetFeature(), this.attPath.toString(), mappings .getNamespaces()); } // private boolean isConditional() { // return function != null; // } /** * see {@link org.geotools.filter.AttributeExpressionImpl#evaluate(Object)} */ @Override public Object evaluate(Object object) { if (object == null) { return null; } // only simple/complex features are supported if (!(object instanceof Feature)) { throw new UnsupportedOperationException( "Expecting a feature to apply filter, but found: " + object); } // if (object instanceof FeatureImpl) { // AttributeExpressionImpl exp = new AttributeExpressionImpl(attPath, new Hints( // FeaturePropertyAccessorFactory.NAMESPACE_CONTEXT, namespaces)); // List<Object> values = new ArrayList<Object>(1); // values.add(exp.evaluate(object)); // return values; // } List<Feature> roots = new ArrayList<Feature>(); roots.add((Feature) object); // if (isConditional()) { // fullSteps = XPath.steps(((Feature) object).getDescriptor(), this.attPath, this.namespaceSupport); // String fTypeString = function.evaluate(object, String.class); // if (fTypeString != null) { // Name fTypeName = Types.degloseName(fTypeString, this.namespaceSupport); // try { // FeatureSource<FeatureType, Feature> fSource = DataAccessRegistry.getFeatureSource(fTypeName); // if (fSource != null && fSource instanceof MappingFeatureSource) { // mappings = ((MappingFeatureSource) fSource).getMapping(); // } else { // return null; // } // } catch (IOException e) { // return null; // } // } else { // return null; // } // } return getValues(0, 0, roots, mappings, null); } private boolean isLastStep(int index) { return index >= fullSteps.size(); } private List<Object> getValues(int startIndex, int endIndex, List<Feature> roots, FeatureTypeMapping fMapping, AttributeMapping prevMapping) { List<Object> values = new ArrayList<Object>(); if (startIndex > fullSteps.size() || endIndex > fullSteps.size()) { return values; } while (startIndex <= endIndex) { List<AttributeMapping> attMappings = new ArrayList<AttributeMapping>(); StepList steps = null; if (isLastStep(endIndex)) { // exhausted all paths return values; } while (attMappings.isEmpty() && endIndex < fullSteps.size()) { endIndex++; steps = fullSteps.subList(startIndex, endIndex); attMappings = fMapping.getAttributeMappingsIgnoreIndex(steps); if (steps.size() == 1) { if (Types.equals(fMapping.getTargetFeature().getName(), steps.get(0).getName()) && !(Types.isSimpleContentType(fMapping.getTargetFeature().getType()))) { // skip element type name, but not when it's a simple content // like gml:name because it wouldn't have the element type name in the xpath startIndex++; endIndex = startIndex; steps = fullSteps.subList(startIndex, endIndex); attMappings = fMapping.getAttributeMappingsIgnoreIndex(steps); continue; } else if (attMappings.isEmpty() && steps.get(0).isId()) { // sometimes there's no explicit attribute mapping for top element name // but id should still resolve to primary key by default // e.g. gsml:GeologicUnit/@gml:id should resolve even though there's no // AttributeMapping for gsml:GeologicUnit setIdValues(null, roots, values); return values; } } } if (attMappings.isEmpty()) { // not found here, but might be found in other nodes if multi-valued // and polymorphic continue; } for (AttributeMapping mapping : attMappings) { if (mapping instanceof NestedAttributeMapping) { if (isClientProperty(endIndex)) { // check for client properties boolean isNestedXlinkHref = isXlinkHref(mapping); boolean valueFound = false; if (!isNestedXlinkHref) { // check if client properties are set in the parent attributeMapping in root mapping file valueFound = getClientProperties(mapping, values, roots); } if (!valueFound) { // or if they're set in the attributeMapping in feature chained mapping file getNestedClientProperties((NestedAttributeMapping) mapping, roots, values, isNestedXlinkHref); } } else { boolean isSimpleContent = Types.isSimpleContent(steps, fMapping .getTargetFeature().getType()); // if simple content, then it doesn't need to increment the next starting // index // since there will be no type name in the xpath, e.g. when gml:name is // feature // chained // the path stays as gml:name.. but if it's a complex type with complex // content, // e.g. gsml:specification // the path will be gsml:specification/gsml:GeologicUnit/<some leaf // attribute to // filter by> getNestedValues((NestedAttributeMapping) mapping, roots, values, isSimpleContent ? startIndex : startIndex + 1); } } else { // normal attribute mapping if (endIndex == fullSteps.size()) { Expression exp = mapping.getSourceExpression(); for (Feature f : roots) { Object value = getValue(exp, f); if (value != null) { values.add(value); } } } else if (isClientProperty(endIndex)) { // peek at the next attribute to check for client properties if (getLastStep().isId()) { setIdValues(mapping, roots, values); } else { getClientProperties(mapping, values, roots); } } else { // increment the xpath List<Object> nestedValues = getValues(startIndex, endIndex, roots, fMapping, mapping); if (nestedValues != null) { values.addAll(nestedValues); } } } } return values; } return values; } private boolean isXlinkHref(AttributeMapping mapping) { if (fullSteps.get(fullSteps.size() - 1).getName().equals(XLINK.HREF)) { // special case for xlink:href by feature chaining // must get the value from the nested attribute mapping instead, i.e. from another table // if it's to get the values from the local table, it shouldn't be set with feature chaining return true; } return false; } private void getNestedValues(NestedAttributeMapping mapping, List<Feature> features, List<Object> values, int nextIndex) { FeatureTypeMapping nextFMapping = null; for (Feature f : features) { try { nextFMapping = mapping.getFeatureTypeMapping(f); } catch (IOException e) { nextFMapping = null; } if (nextFMapping != null && mapping.isSameSource()) { // same root/database row, different mappings, used in // polymorphism List<Feature> nestedRoots = new ArrayList<Feature>(1); nestedRoots.add(f); List<Object> nestedValues = getValues(nextIndex, nextIndex, nestedRoots, nextFMapping, mapping); if (nestedValues != null) { values.addAll(nestedValues); } continue; } try { List<Feature> nestedFeatures = getNestedFeatures(f, mapping, nextFMapping); if (nestedFeatures == null || nestedFeatures.isEmpty()) { continue; } if (nextFMapping != null) { List<Object> nestedValues = getValues(nextIndex, nextIndex, nestedFeatures, nextFMapping, mapping); if (nestedValues != null) { values.addAll(nestedValues); } } else if (!nestedFeatures.isEmpty()) { throw new UnsupportedOperationException( "FeatureTypeMapping not found for " + attPath + ". This shouldn't happen if it's set in AppSchemaDataAccess mapping file!"); } } catch (IOException e) { throw new RuntimeException("Failed evaluating filter expression: '" + attPath + "'. Caused by: " + e.getMessage()); } catch (IllegalArgumentException e) { // might be a polymorphic case where it's looking for an attribute // from another type // that doesn't match this, but might match another database row // so just continue continue; } } } private void getNestedClientProperties(NestedAttributeMapping mapping, List<Feature> features, List<Object> values, boolean isXlinkHref) { FeatureTypeMapping nextFMapping = null; for (Feature f : features) { try { nextFMapping = mapping.getFeatureTypeMapping(f); if (nextFMapping != null) { List<Feature> nestedFeatures; nestedFeatures = getNestedFeatures(f, mapping, nextFMapping); if (nestedFeatures == null || nestedFeatures.isEmpty()) { continue; } if (isXlinkHref) { // xlink:href mapping done in the root mapping file // there is no need to find attributeMapping in the nested feature type mapping getClientProperties(mapping, values, nestedFeatures); } else { List<AttributeMapping> nestedAttMappings = nextFMapping .getAttributeMappingsIgnoreIndex(mapping.getTargetXPath()); AttributeMapping attMapping = null; boolean found = false; if (!nestedAttMappings.isEmpty()) { attMapping = nestedAttMappings.get(0); found = getClientProperties(attMapping, values, nestedFeatures); } if (!found && getLastStep().isId()) { setIdValues(attMapping, nestedFeatures, values); } } } } catch (IOException e) { throw new RuntimeException("Failed evaluating filter expression: '" + attPath + "'. Caused by: " + e.getMessage()); } catch (IllegalArgumentException e) { // might be a polymorphic case where it's looking for an attribute // from another type // that doesn't match this, but might match another database row // so just continue continue; } } } private boolean isClientProperty(int endIndex) { if (endIndex == fullSteps.size() - 1) { return fullSteps.get(endIndex).isXmlAttribute(); } return false; } private boolean getClientProperties(AttributeMapping attMapping, List<Object> values, List<Feature> features) { boolean expressionFound = false; Step lastStep = getLastStep(); Expression exp = getClientPropertyExpression(attMapping, lastStep); if (exp != null) { for (Feature f : features) { Object value = getValue(exp, f); if (value != null) { values.add(value); } } expressionFound = true; } return expressionFound; } private Step getLastStep() { return fullSteps.get(fullSteps.size() - 1); } /** * Get nested features from a feature chaining attribute mapping * * @param root * Root feature being evaluated * @param nestedMapping * Attribute mapping for nested features * @param fMapping * The root feature type mapping * @return list of nested features * @throws IOException */ private List<Feature> getNestedFeatures(Feature root, NestedAttributeMapping nestedMapping, FeatureTypeMapping fMapping) throws IOException { Object fTypeName = nestedMapping.getNestedFeatureType(root); if (fTypeName == null || !(fTypeName instanceof Name)) { return null; } boolean hasSimpleFeatures = AppSchemaDataAccessRegistry.hasName((Name) fTypeName); // get foreign key Object val = getValue(nestedMapping.getSourceExpression(), root); if (val == null) { return null; } if (hasSimpleFeatures) { // normal app-schema mapping return nestedMapping.getInputFeatures(val, fMapping); } else { // app-schema with a complex feature source return nestedMapping.getFeatures(val, null, root); } } private Object getValue(Expression expression, Feature feature) { Object value = expression.evaluate(feature); return extractAttributeValue(value); } /** * Extract the value that might be wrapped in an attribute. If the value is a collection, gets * the first value. * * @param value * @return */ private Object extractAttributeValue(Object value) { if (value == null) { return null; } while (value instanceof Attribute) { // get real value value = ((Attribute) value).getValue(); } if (value == null) { return null; } if (value instanceof Collection) { if (((Collection) value).isEmpty()) { return null; } value = ((Collection) value).iterator().next(); while (value instanceof Attribute) { value = ((Attribute) value).getValue(); } } return value; } private void setIdValues(AttributeMapping mapping, List<Feature> features, List<Object> values) { Expression exp = null; if (mapping == null || mapping.getIdentifierExpression() == Expression.NIL) { // no specific attribute mapping or that idExpression is not mapped // use primary key exp = CommonFactoryFinder.getFilterFactory(null).property("@id"); } else { exp = mapping.getIdentifierExpression(); } if (exp != null) { for (Feature f : features) { Object value = getValue(exp, f); if (value != null) { values.add(value); } } } } /** * Find the source expression if the step is a client property. * * @param nextRootStep * the step * @param fMapping * feature type mapping to get namespaces from * @param mapping * attribute mapping * @param targetXPath * the full target xpath * @return */ private Expression getClientPropertyExpression(AttributeMapping mapping, Step lastStep) { if (lastStep.isXmlAttribute()) { Map<Name, Expression> clientProperties = mapping.getClientProperties(); QName lastStepQName = lastStep.getName(); Name lastStepName; if (lastStepQName.getPrefix() != null && lastStepQName.getPrefix().length() > 0 && (lastStepQName.getNamespaceURI() == null || lastStepQName.getNamespaceURI() .length() == 0)) { String prefix = lastStepQName.getPrefix(); String uri = namespaceSupport.getURI(prefix); lastStepName = Types.typeName(uri, lastStepQName.getLocalPart()); } else { lastStepName = Types.toTypeName(lastStepQName); } if (clientProperties.containsKey(lastStepName)) { // end NC - added return (Expression) clientProperties.get(lastStepName); } } return null; } }