/*
* 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.config.Types;
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.factory.CommonFactoryFinder;
import org.geotools.filter.AttributeExpressionImpl;
import org.geotools.filter.visitor.DuplicatingFilterVisitor;
import org.geotools.jdbc.NamespaceAwareAttributeRenameVisitor;
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;
import org.opengis.filter.expression.ExpressionVisitor;
/**
* 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 NestedAttributeMapping rootMapping;
private StepList fullSteps;
/**
* First constructor
*
* @param xpath
* Attribute XPath
* @param expressions
* List of broken up expressions
*/
public NestedAttributeExpression(StepList xpath, NestedAttributeMapping nestedMapping) {
super(xpath.toString());
this.rootMapping = nestedMapping;
this.fullSteps = xpath;
}
/**
* 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)) {
return null;
}
return getValues(((Feature)object), rootMapping, fullSteps);
}
private List<Object> getValues(Feature feature, NestedAttributeMapping nestedMapping,
StepList steps) {
List<Object> values = new ArrayList<Object>();
FeatureTypeMapping nextFMapping;
try {
nextFMapping = nestedMapping.getFeatureTypeMapping(feature);
} catch (IOException e) {
nextFMapping = null;
}
if (nextFMapping == null) {
// throw error unless this is polymorphism
if (nestedMapping.isConditional()) {
return values;
}
throw new UnsupportedOperationException("FeatureTypeMapping not found for " + attPath
+ ". Please revise PropertyName in your filter!");
}
List<Feature> nestedFeatures = new ArrayList<Feature>();
if (nestedMapping.isSameSource()) {
// same root/database row, different mappings, used in
// polymorphism
nestedFeatures = new ArrayList<Feature>();
nestedFeatures.add(feature);
} else {
// get nested features
try {
nestedFeatures = getNestedFeatures(feature, nestedMapping, nextFMapping);
} 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
return values;
}
}
boolean isClientProperty = isClientProperty(steps);
StepList newSteps = null;
if (isClientProperty) {
// check for client properties for this mapping
newSteps = steps.subList(0, steps.size() - 1);
if (newSteps.size() == 1) {
// special case for client property for this NestedAttributeMapping
for (Feature f : nestedFeatures) {
values.addAll(getClientProperties(nestedMapping, f));
}
}
}
// skip element name that is mapped at the next FeatureTypeMapping
// except when it's a simple content
// if simple content, then 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>
Name nextElementName = nextFMapping.getTargetFeature().getName();
// starting index for the next search
int startPos = -1;
if (Types.equals(nextElementName, steps.get(0).getName())) {
// simple contents where nested element name is the same as the nesting element
startPos = 0;
} else {
Step elementNameStep = steps.get(nestedMapping.getTargetXPath().size());
// support polymorphism
// check that element type matches the steps
if (Types.equals(nextElementName, elementNameStep.getName())) {
startPos = nestedMapping.getTargetXPath().size();
if (steps.size() > startPos + 1 && !steps.get(startPos + 1).isXmlAttribute()) {
// skip next element name for next evaluation
// except if the next step is a client property for that element name
// since we'd need the AttributeMapping for the client property
startPos++;
}
}
}
if (startPos > -1) {
newSteps = steps.subList(startPos, steps.size());
if (!newSteps.isEmpty()) {
List<NestedAttributeMapping> nestedMappings = nextFMapping.getNestedMappings();
if (!nestedMappings.isEmpty()) {
for (NestedAttributeMapping mapping : nestedMappings) {
if (newSteps.startsWith(mapping.getTargetXPath())) {
for (Feature f : nestedFeatures) {
// loop to this method
values.addAll(getValues(f, mapping, newSteps));
}
}
}
}
if (isClientProperty) {
// check for client properties
newSteps = newSteps.subList(0, newSteps.size() - 1);
}
boolean isXlinkHref = isClientProperty && isXlinkHref(steps);
List<AttributeMapping> attMappings = nextFMapping
.getAttributeMappingsIgnoreIndex(newSteps);
for (AttributeMapping attMapping : attMappings) {
if (isClientProperty) {
if (!(isXlinkHref && attMapping instanceof NestedAttributeMapping)) {
// if it's an xlink href,
// ignore nested attribute mappings as the values should come from
// nested features.. so should be evaluated at the next call
for (Feature f : nestedFeatures) {
values.addAll(getClientProperties(attMapping, f));
}
}
} else {
for (Feature f : nestedFeatures) {
values.add(getValue(attMapping.getSourceExpression(), f));
}
}
}
}
}
return values;
}
private boolean isXlinkHref(StepList steps) {
if (steps.isEmpty()) {
return false;
}
// 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 steps.get(steps.size() - 1).getName().equals(XLINK.HREF);
}
private boolean isClientProperty(StepList steps) {
if (steps.isEmpty()) {
return false;
}
return steps.get(steps.size() - 1).isXmlAttribute();
}
private List<Object> getClientProperties(AttributeMapping attMapping, Feature f) {
List<Object> values = new ArrayList<Object>();
Step lastStep = getLastStep();
Expression exp = getClientPropertyExpression(attMapping, lastStep);
if (exp != null) {
Object value = getValue(exp, f);
if (value != null) {
values.add(value);
}
}
return values;
}
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, 0, null);
}
}
private Object getValue(Expression expression, Feature feature) {
try {
Object value = expression.evaluate(feature);
return extractAttributeValue(value);
} catch (IllegalArgumentException e) {
// if the field doesn't exist in the feature
// i.e. if it's polymorphic
return null;
}
}
/**
* Extract the value that might be wrapped in an attribute. If the value is a collection, gets
* the first value.
*
* @param value
* @return
*/
@SuppressWarnings("rawtypes")
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;
}
/**
* 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) {
Expression exp = null;
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
exp = (Expression) clientProperties.get(lastStepName);
} else if (XPath.isId(lastStep)) {
if (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();
}
}
}
return exp;
}
public Object accept(ExpressionVisitor visitor, Object extraData) {
//Workaround for GEOT-4981: NestedAttributeExpresionImpl is incompatible with DuplicatingFilterVisitor
if (visitor instanceof DuplicatingFilterVisitor && !(visitor instanceof NamespaceAwareAttributeRenameVisitor)) {
return new NestedAttributeExpression(fullSteps, rootMapping);
}
return visitor.visit(this,extraData);
}
}