/*
* GeoTools - The Open Source Java GIS Toolkit
* http://geotools.org
*
* (C) 2005-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.filter;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Iterator;
import java.util.List;
import java.util.Scanner;
import java.util.logging.Level;
import java.util.logging.Logger;
import javax.xml.XMLConstants;
import javax.xml.namespace.QName;
import org.geotools.data.complex.ComplexFeatureConstants;
import org.geotools.factory.CommonFactoryFinder;
import org.geotools.feature.AttributeBuilder;
import org.geotools.feature.AttributeImpl;
import org.geotools.feature.NameImpl;
import org.geotools.feature.Types;
import org.geotools.feature.ValidatingFeatureFactoryImpl;
import org.geotools.feature.type.AttributeDescriptorImpl;
import org.geotools.feature.type.GeometryTypeImpl;
import org.geotools.feature.type.UniqueNameFeatureTypeFactoryImpl;
import org.geotools.util.CheckedArrayList;
import org.geotools.xs.XSSchema;
import org.opengis.feature.Attribute;
import org.opengis.feature.ComplexAttribute;
import org.opengis.feature.FeatureFactory;
import org.opengis.feature.Property;
import org.opengis.feature.type.AttributeDescriptor;
import org.opengis.feature.type.AttributeType;
import org.opengis.feature.type.ComplexType;
import org.opengis.feature.type.FeatureTypeFactory;
import org.opengis.feature.type.GeometryDescriptor;
import org.opengis.feature.type.GeometryType;
import org.opengis.feature.type.Name;
import org.opengis.feature.type.PropertyDescriptor;
import org.opengis.filter.FilterFactory;
import org.opengis.filter.expression.Expression;
import org.opengis.filter.expression.PropertyName;
import org.opengis.referencing.crs.CoordinateReferenceSystem;
import org.opengis.util.Cloneable;
import org.xml.sax.helpers.NamespaceSupport;
import com.vividsolutions.jts.geom.Geometry;
/**
* Utility class to evaluate XPath expressions against an Attribute instance, which may be any
* Attribute, whether it is simple, complex, a feature, etc.
* <p>
* At the difference of the Filter subsystem, which works against Attribute contents (for example to
* evaluate a comparison filter), the XPath subsystem, for which this class is the single entry
* point, works against Attribute instances. That is, the result of an XPath expression, if a single
* value, is an Attribtue, not the attribute content, or a List of Attributes, for instance.
* </p>
*
* @author Gabriel Roldan (Axios Engineering)
* @author Rini Angreani (CSIRO Earth Science and Resource Engineering)
* @version $Id$
*
*
* @source $URL$
* http://svn.osgeo.org/geotools/trunk/modules/unsupported/app-schema/app-schema/src/main
* /java/org/geotools/data/complex/filter/XPath.java $
* @since 2.4
*/
public class XPath {
private static final Logger LOGGER = org.geotools.util.logging.Logging.getLogger(XPath.class
.getPackage().getName());
private FilterFactory FF;
private FeatureFactory featureFactory;
private CoordinateReferenceSystem crs;
/**
* Used to create specific attribute descriptors for
* {@link #set(Attribute, String, Object, String, AttributeType)} when the actual attribute
* instance is of a derived type of the corresponding one declared in the feature type.
*/
private FeatureTypeFactory descriptorFactory;
public XPath() {
this.FF = CommonFactoryFinder.getFilterFactory(null);
this.featureFactory = new ValidatingFeatureFactoryImpl();
this.descriptorFactory = new UniqueNameFeatureTypeFactoryImpl();
}
public XPath(FilterFactory ff, FeatureFactory featureFactory) {
setFilterFactory(ff);
setFeatureFactory(featureFactory);
// this.descriptorFactory = new TypeFactoryImpl();
}
public void setFilterFactory(FilterFactory ff) {
this.FF = ff;
}
public void setCRS(CoordinateReferenceSystem crs) {
this.crs = crs;
}
public void setFeatureFactory(FeatureFactory featureFactory) {
this.featureFactory = featureFactory;
}
public static class StepList extends CheckedArrayList<Step> {
private static final long serialVersionUID = -5612786286175355862L;
private StepList() {
super(XPath.Step.class);
}
public StepList(StepList steps) {
super(XPath.Step.class);
addAll(steps);
}
public String toString() {
StringBuffer sb = new StringBuffer();
for (Iterator<Step> it = iterator(); it.hasNext();) {
Step s = (Step) it.next();
sb.append(s.toString());
if (it.hasNext()) {
sb.append("/");
}
}
return sb.toString();
}
public boolean containsPredicate() {
for (int i=0; i< size(); i++) {
if (get(i).getPredicate() != null) {
return true;
}
}
return false;
}
public StepList subList(int fromIndex, int toIndex) {
if (fromIndex < 0)
throw new IndexOutOfBoundsException("fromIndex = " + fromIndex);
if (toIndex > size())
throw new IndexOutOfBoundsException("toIndex = " + toIndex);
if (fromIndex > toIndex)
throw new IllegalArgumentException("fromIndex(" + fromIndex + ") > toIndex("
+ toIndex + ")");
StepList subList = new StepList();
for (int i = fromIndex; i < toIndex; i++) {
subList.add(this.get(i));
}
return subList;
}
public StepList clone() {
StepList copy = new StepList();
for (Step step : this) {
copy.add((Step) step.clone());
}
return copy;
}
/**
* Compares this StepList with another for equivalence regardless of the indexes of each
* Step.
*
* @param propertyName
* @return <code>true</code> if this step list has the same location paths than
* <code>propertyName</code> ignoring the indexes in each step. <code>false</code>
* otherwise.
*/
public boolean equalsIgnoreIndex(final StepList propertyName) {
if (propertyName == null) {
return false;
}
if (propertyName == this) {
return true;
}
if (size() != propertyName.size()) {
return false;
}
Iterator mine = iterator();
Iterator him = propertyName.iterator();
Step myStep;
Step hisStep;
while (mine.hasNext()) {
myStep = (Step) mine.next();
hisStep = (Step) him.next();
if (myStep.isIndexed()) {
if (!myStep.equals(hisStep)) {
return false;
}
} else {
if (!myStep.equalsIgnoreIndex(hisStep)) {
return false;
}
}
}
return true;
}
/**
* Find the first steps matching the xpath within this list, and set an index to it.
*
* @param index
* the new index for the matching steps
* @param xpath
* the xpath to be searched
*/
public void setIndex(int index, String xpath, String separator) {
if (this.toString().contains(xpath)) {
for (int i = 0; i < size() - 1; i++) {
String firstString = get(i).toString();
if (xpath.equals(firstString)) {
get(i).setIndex(index);
return;
}
if (xpath.startsWith(firstString)) {
StringBuffer buf = new StringBuffer(firstString);
buf.append(separator);
for (int j = i + 1; j < size() - 1; j++) {
buf.append(get(j).toString());
if (buf.toString().equals(xpath)) {
get(j).setIndex(index);
return;
}
buf.append(separator);
}
}
}
}
}
}
/**
*
* @author Gabriel Roldan
*
*/
public static class Step implements Cloneable {
private int index;
private String predicate = null;
private QName attributeName;
private boolean isXmlAttribute;
private boolean isIndexed;
/**
* Creates a "property" xpath step (i.e. isXmlAttribute() == false).
*
* @param name
* @param index
*/
public Step(final QName name, final int index) {
this(name, index, false, false);
}
/**
* Creates an xpath step for the given qualified name and index; and the given flag to
* indicate if it it an "attribute" or "property" step.
*
* @param name
* the qualified name of the step (name should include prefix to be reflected in
* toString())
* @param index
* the index (indexing starts at 1 for Xpath) of the step
* @param isXmlAttribute
* whether the step referers to an "attribute" or a "property" (like for
* attributes and elements in xml)
* @throws NullPointerException
* if <code>name==null</code>
* @throws IllegalArgumentException
* if <code>index < 1</code>
*/
public Step(final QName name, final int index, boolean isXmlAttribute) {
this(name, index, isXmlAttribute, false);
}
/**
* Creates an xpath step for the given qualified name and index; and the given flag to
* indicate if it it an "attribute" or "property" step.
*
* @param name
* the qualified name of the step (name should include prefix to be reflected in
* toString())
* @param index
* the index (indexing starts at 1 for Xpath) of the step
* @param isXmlAttribute
* whether the step referers to an "attribute" or a "property" (like for
* attributes and elements in xml)
* @param isIndexed
* whether or not the index is to be shown in the string representation even if
* index = 1
* @throws NullPointerException
* if <code>name==null</code>
* @throws IllegalArgumentException
* if <code>index < 1</code>
*/
public Step(final QName name, final int index, boolean isXmlAttribute, boolean isIndexed) {
if (name == null) {
throw new NullPointerException("name");
}
if (index < 1) {
throw new IllegalArgumentException("index shall be >= 1");
}
this.attributeName = name;
this.index = index;
this.isXmlAttribute = isXmlAttribute;
this.isIndexed = isIndexed;
}
public Step(final QName name, boolean isXmlAttribute, final String predicate) {
if (name == null) {
throw new NullPointerException("name");
}
this.attributeName = name;
this.index = 1;
this.isIndexed = false;
this.isXmlAttribute = isXmlAttribute;
this.predicate = predicate;
}
/**
* Compares this Step with another for equivalence ignoring the steps indexes.
*
* @param hisStep
* @return
*/
public boolean equalsIgnoreIndex(Step other) {
if (other == null) {
return false;
}
if (other == this) {
return true;
}
return attributeName.equals(other.attributeName)
&& isXmlAttribute == other.isXmlAttribute;
}
public int getIndex() {
return index;
}
public String getPredicate() {
return predicate;
}
public boolean isIndexed() {
return isIndexed;
}
public QName getName() {
return attributeName;
}
public String toString() {
StringBuffer sb = new StringBuffer(isXmlAttribute ? "@" : "");
if (XMLConstants.DEFAULT_NS_PREFIX != attributeName.getPrefix()) {
sb.append(attributeName.getPrefix()).append(':');
}
sb.append(attributeName.getLocalPart());
if (isIndexed) {
// we want to print index = 1 as well if specified
// so filtering on the first index doesn't return all
// e.g. gml:name[1] doesn't get translated to
// gml:name i.e. all gml:name instances
sb.append("[").append(index).append("]");
} else if (predicate != null) {
sb.append("[").append(predicate).append("]");
}
return sb.toString();
}
public boolean equals(Object o) {
if (!(o instanceof Step)) {
return false;
}
Step s = (Step) o;
return attributeName.equals(s.attributeName) && index == s.index
&& isXmlAttribute == s.isXmlAttribute
&& predicate == s.predicate;
}
public int hashCode() {
return 17 * attributeName.hashCode() + 37 * index;
}
public Step clone() {
return predicate==null?
new Step(this.attributeName, this.index, this.isXmlAttribute, this.isIndexed):
new Step(this.attributeName, this.isXmlAttribute, this.predicate);
}
/**
* Flag that indicates that this single step refers to an "attribute" rather than a
* "property".
* <p>
* I.e. it was created from the last step of an expression like
* <code>foo/bar@attribute</code>.
* </p>
*
* @return
*/
public boolean isXmlAttribute() {
return isXmlAttribute;
}
public void setIndex(int index) {
this.index = index;
isIndexed = true;
}
}
/**
* Split X-path string in to string steps, ignoring / characters inside []
*
* @param s x-path string
* @return list of string steps
*/
private static List<String> splitPath(String s) {
ArrayList<String> parts = new ArrayList<String>();
StringBuffer b = new StringBuffer();
int insideIndex = 0;
for (int pos = 0 ; pos < s.length() ; pos++) {
if (s.charAt(pos) == '/' && insideIndex==0) {
parts.add(b.toString());
b = new StringBuffer();
} else {
if (s.charAt(pos) == '[') {
insideIndex++;
} else if (s.charAt(pos) == ']') {
insideIndex--;
}
b.append(s.charAt(pos));
}
}
parts.add(b.toString());
return parts;
}
/**
* Returns the list of stepts in <code>xpathExpression</code> by cleaning it up removing
* unnecessary elements.
* <p>
* </p>
*
* @param root
* non null descriptor of the root attribute, generally the Feature descriptor. Used
* to ignore the first step in xpathExpression if the expression's first step is
* named as rootName.
*
* @param xpathExpression
* @return
* @throws IllegalArgumentException
* if <code>xpathExpression</code> has no steps or it isn't a valid XPath expression
* against <code>type</code>.
*/
public static StepList steps(final AttributeDescriptor root, final String xpathExpression,
final NamespaceSupport namespaces) throws IllegalArgumentException {
if (root == null) {
throw new NullPointerException("root");
}
if (xpathExpression == null) {
throw new NullPointerException("xpathExpression");
}
String expression = xpathExpression.trim();
if ("".equals(expression)) {
throw new IllegalArgumentException("expression is empty");
}
StepList steps = new StepList();
if ("/".equals(expression)) {
expression = root.getName().getLocalPart();
}
if (expression.startsWith("/")) {
expression = expression.substring(1);
}
final List<String> partialSteps = splitPath(expression);
if (partialSteps.size() == 0) {
throw new IllegalArgumentException("no steps provided");
}
int startIndex = 0;
for (int i = startIndex; i < partialSteps.size(); i++) {
String step = partialSteps.get(i);
if ("..".equals(step)) {
steps.remove(steps.size() - 1);
} else if (".".equals(step)) {
continue;
} else {
int index = 1;
boolean isXmlAttribute = false;
boolean isIndexed = false;
String predicate = null;
String stepName = step;
if (step.indexOf('[') != -1) {
int start = step.indexOf('[');
int end = step.indexOf(']');
stepName = step.substring(0, start);
String s = step.substring(start + 1, end);
Scanner scanner = new Scanner(s);
if (scanner.hasNextInt()) {
index = scanner.nextInt();
isIndexed = true;
} else {
predicate = s;
}
}
if (step.charAt(0) == '@') {
isXmlAttribute = true;
stepName = stepName.substring(1);
}
QName qName = deglose(stepName, root, namespaces, isXmlAttribute);
if (predicate == null) {
steps.add(new Step(qName, index, isXmlAttribute, isIndexed));
} else {
steps.add(new Step(qName, isXmlAttribute, predicate));
}
}
//
// if (step.indexOf('[') != -1) {
// int start = step.indexOf('[');
// int end = step.indexOf(']');
// String stepName = step.substring(0, start);
// int stepIndex = Integer.parseInt(step.substring(start + 1, end));
// QName qName = deglose(stepName, root, namespaces);
// steps.add(new Step(qName, stepIndex));
// } else if ("..".equals(step)) {
// steps.remove(steps.size() - 1);
// } else if (".".equals(step)) {
// continue;
// } else {
// QName qName = deglose(step, root, namespaces);
// steps.add(new Step(qName, 1));
// }
}
// XPath simplification phase: if the xpath expression contains more
// nodes
// than the root node itself, and the root node is present, remove the
// root
// node as it is redundant
if (root != null && steps.size() > 1) {
Step step = (Step) steps.get(0);
Name rootName = root.getName();
QName stepName = step.getName();
if (Types.equals(rootName, stepName)) {
LOGGER.fine("removing root name from xpath " + steps + " as it is redundant");
steps.remove(0);
}
}
return steps;
}
private static QName deglose(final String prefixedName, final AttributeDescriptor root,
final NamespaceSupport namespaces, boolean isXmlAttribute) {
if (prefixedName == null) {
throw new NullPointerException("prefixedName");
}
QName name = null;
String prefix;
final String namespaceUri;
final String localName;
int prefixIdx = prefixedName.indexOf(':');
if (prefixIdx == -1) {
localName = prefixedName;
final Name rootName = root.getName();
// don't use default namespace for client properties (xml attribute), and FEATURE_LINK
final String defaultNamespace = (isXmlAttribute
|| localName.equals(ComplexFeatureConstants.FEATURE_CHAINING_LINK_NAME
.getLocalPart()) || rootName.getNamespaceURI() == null) ? XMLConstants.NULL_NS_URI
: namespaces.getURI("") == null ? rootName.getNamespaceURI() : namespaces
.getURI("");
namespaceUri = defaultNamespace;
if (XMLConstants.NULL_NS_URI.equals(defaultNamespace)) {
prefix = XMLConstants.DEFAULT_NS_PREFIX;
} else {
if (!localName.equals(rootName.getLocalPart())) {
LOGGER.warning("Using root's namespace " + defaultNamespace
+ " for step named '" + localName + "', as no prefix was stated");
}
prefix = namespaces.getPrefix(defaultNamespace);
if (prefix == null) {
//throw new IllegalStateException("Default namespace is not mapped to a prefix: "
// + defaultNamespace);
prefix = "";
}
}
} else {
prefix = prefixedName.substring(0, prefixIdx);
localName = prefixedName.substring(prefixIdx + 1);
namespaceUri = namespaces.getURI(prefix);
}
name = new QName(namespaceUri, localName, prefix);
return name;
}
/**
* Sets the value of the attribute of <code>att</code> addressed by <code>xpath</code> and of
* type <code>targetNodeType</code> to be <code>value</code> with id <code>id</code>.
*
* @param att
* the root attribute for which to set the child attribute value
* @param xpath
* the xpath expression that addresses the <code>att</code> child whose value is to
* be set
* @param value
* the value of the attribute addressed by <code>xpath</code>
* @param id
* the identifier of the attribute addressed by <code>xpath</code>, might be
* <code>null</code>
* @param targetNodeType
* the expected type of the attribute addressed by <code>xpath</code>, or
* <code>null</code> if unknown
* @param isXlinkRef
* true if the attribute would only contain xlink:href client property
* @return
*/
public Attribute set(final Attribute att, final StepList xpath, Object value, String id,
AttributeType targetNodeType, boolean isXlinkRef, Expression sourceExpression) {
return set(att, xpath, value, id, targetNodeType, isXlinkRef, null, sourceExpression);
}
public Attribute set(final Attribute att, final StepList xpath, Object value, String id,
AttributeType targetNodeType, boolean isXlinkRef, AttributeDescriptor targetDescriptor,
Expression sourceExpression) {
if (XPath.LOGGER.isLoggable(Level.CONFIG)) {
XPath.LOGGER.entering("XPath", "set", new Object[] { att, xpath, value, id,
targetNodeType });
}
final StepList steps = new StepList(xpath);
// if (steps.size() < 2) {
// throw new IllegalArgumentException("parent not yet built for " +
// xpath);
// }
Attribute parent = att;
Name rootName = null;
if (parent.getDescriptor() != null) {
rootName = parent.getDescriptor().getName();
Step rootStep = (Step) steps.get(0);
QName stepName = rootStep.getName();
if (stepName.getLocalPart().equals(rootName.getLocalPart())) {
if (XMLConstants.NULL_NS_URI.equals(stepName.getNamespaceURI())
|| stepName.getNamespaceURI().equals(rootName.getNamespaceURI())) {
// first step is the self reference to att, so skip it
steps.remove(0);
}
}
}
Iterator stepsIterator = steps.iterator();
for (; stepsIterator.hasNext();) {
final XPath.Step currStep = (Step) stepsIterator.next();
AttributeDescriptor currStepDescriptor = null;
final boolean isLastStep = !stepsIterator.hasNext();
final QName stepName = currStep.getName();
final Name attributeName = Types.toName(stepName);
final AttributeType _parentType = parent.getType();
if (_parentType.equals(XSSchema.ANYTYPE_TYPE) && targetDescriptor != null) {
// this needs to be passed on if casting anyType to something else, since it won't
// exist in the schema
currStepDescriptor = targetDescriptor;
} else {
ComplexType parentType = (ComplexType) _parentType;
if (!isLastStep || targetNodeType == null) {
if (null == attributeName.getNamespaceURI()) {
currStepDescriptor = (AttributeDescriptor) Types.findDescriptor(parentType, attributeName.getLocalPart());
} else {
currStepDescriptor = (AttributeDescriptor) Types.findDescriptor(parentType, attributeName);
}
if (currStepDescriptor == null) {
// need to take the non easy way, may be the instance has a
// value for this step with a different name, of a derived
// type of the one declared in the parent type
String prefixedStepName = currStep.toString();
PropertyName name = FF.property(prefixedStepName);
Attribute child = (Attribute) name.evaluate(parent);
if (child != null) {
currStepDescriptor = child.getDescriptor();
}
}
} else {
AttributeDescriptor actualDescriptor;
if (null == attributeName.getNamespaceURI()) {
actualDescriptor = (AttributeDescriptor) Types.findDescriptor(parentType, attributeName.getLocalPart());
} else {
actualDescriptor = (AttributeDescriptor) Types.findDescriptor(parentType, attributeName);
}
if (actualDescriptor != null) {
int minOccurs = actualDescriptor.getMinOccurs();
int maxOccurs = actualDescriptor.getMaxOccurs();
boolean nillable = actualDescriptor.isNillable();
if (actualDescriptor instanceof GeometryDescriptor) {
// important to maintain CRS information encoding
if (Geometry.class.isAssignableFrom(targetNodeType.getBinding())) {
if (!(targetNodeType instanceof GeometryType)) {
targetNodeType = new GeometryTypeImpl(targetNodeType.getName(),
targetNodeType.getBinding(), crs != null ? crs
: ((GeometryDescriptor) actualDescriptor)
.getCoordinateReferenceSystem(),
targetNodeType.isIdentified(), targetNodeType
.isAbstract(),
targetNodeType.getRestrictions(), targetNodeType
.getSuper(), targetNodeType.getDescription());
}
currStepDescriptor = descriptorFactory.createGeometryDescriptor(
(GeometryType) targetNodeType, attributeName, minOccurs,
maxOccurs, nillable, null);
} else {
throw new IllegalArgumentException("Can't set targetNodeType: "
+ targetNodeType.toString() + " for attribute mapping: "
+ attributeName + " as it is not a Geometry type!");
}
} else {
currStepDescriptor = descriptorFactory.createAttributeDescriptor(
targetNodeType, attributeName, minOccurs, maxOccurs, nillable,
null);
}
}
}
if (currStepDescriptor == null) {
StringBuffer parentAtts = new StringBuffer();
Collection properties = parentType.getDescriptors();
for (Iterator it = properties.iterator(); it.hasNext();) {
PropertyDescriptor desc = (PropertyDescriptor) it.next();
Name name = desc.getName();
parentAtts.append(name.getNamespaceURI());
parentAtts.append("#");
parentAtts.append(name.getLocalPart());
if (it.hasNext()) {
parentAtts.append(", ");
}
}
throw new IllegalArgumentException(currStep
+ " is not a valid location path for type " + parentType.getName()
+ ". " + currStep + " ns: " + currStep.getName().getNamespaceURI()
+ ", " + parentType.getName().getLocalPart() + " properties: "
+ parentAtts);
}
}
if (isLastStep) {
// reached the leaf
if (currStepDescriptor == null) {
throw new IllegalArgumentException(currStep
+ " is not a valid location path for type " + _parentType.getName());
}
if (value == null && !currStepDescriptor.isNillable() && sourceExpression != null
&& !sourceExpression.equals(Expression.NIL)) {
if (currStepDescriptor.getMinOccurs() == 0) {
return null;
}
}
int index = currStep.isIndexed ? currStep.getIndex() : -1;
Attribute attribute = setValue(currStepDescriptor, id, value, index, parent,
targetNodeType, isXlinkRef);
return attribute;
} else {
// parent = appendComplexProperty(parent, currStep,
// currStepDescriptor);
int index = currStep.isIndexed ? currStep.getIndex() : -1;
parent = setValue(currStepDescriptor, null, new ArrayList<Property>(), index,
parent, null, isXlinkRef);
}
}
throw new IllegalStateException();
}
private Attribute setValue(final AttributeDescriptor descriptor, final String id,
final Object value, final int index, final Attribute parent,
final AttributeType targetNodeType, boolean isXlinkRef) {
// adapt value to context
Object convertedValue = convertValue(descriptor, value);
Attribute leafAttribute = null;
final Name attributeName = descriptor.getName();
if (!isXlinkRef) {
// skip this process if the attribute would only contain xlink:ref
// that is chained, because it won't contain any values, and we
// want to create a new empty leaf attribute
if (parent instanceof ComplexAttribute) {
Object currStepValue = ((ComplexAttribute) parent).getProperties(attributeName);
if (currStepValue instanceof Collection) {
List<Attribute> values = new ArrayList((Collection) currStepValue);
if (!values.isEmpty()) {
if (isEmpty(convertedValue)) {
// when attribute is empty, it is probably just a parent of a leaf
// attribute
// it could already exist from another attribute mapping for a different
// leaf
// e.g. 2 different attribute mappings:
// sa:relatedObservation/om:Observation/om:parameter[2]/swe:Time/swe:uom
// sa:relatedObservation/om:Observation/om:parameter[2]/swe:Time/swe:value
// and this could be processing om:parameter[2] the second time for
// swe:value
// so we need to find it if it already exists
if (index > -1) {
// get the attribute of specified index
int valueIndex = 1;
for (Attribute stepValue : values) {
Object mappedIndex = stepValue.getUserData().get(
ComplexFeatureConstants.MAPPED_ATTRIBUTE_INDEX);
if (mappedIndex == null) {
mappedIndex = valueIndex;
}
if (index == Integer.parseInt(String.valueOf(mappedIndex))) {
return stepValue;
}
valueIndex++;
}
} else {
// get the last existing node
return values.get(values.size() - 1);
}
} else {
for (Attribute stepValue : values) {
// eliminate duplicates in case the values come from denormalized
// view..
boolean sameIndex = true;
if (index > -1) {
if (stepValue.getUserData().containsKey(
ComplexFeatureConstants.MAPPED_ATTRIBUTE_INDEX)) {
sameIndex = (index == Integer.parseInt(
String.valueOf(stepValue.getUserData().get(
ComplexFeatureConstants.MAPPED_ATTRIBUTE_INDEX))));
}
}
if (sameIndex && stepValue.getValue().equals(convertedValue)) {
return stepValue;
}
}
}
}
} else if (currStepValue instanceof Attribute) {
leafAttribute = (Attribute) currStepValue;
} else if (currStepValue != null) {
throw new IllegalStateException("Unknown addressed object. Xpath:"
+ attributeName + ", addressed: " + currStepValue.getClass().getName()
+ " [" + currStepValue.toString() + "]");
}
}
}
if (leafAttribute == null) {
AttributeBuilder builder = new AttributeBuilder(featureFactory);
if (crs != null) {
builder.setCRS(crs);
}
builder.setDescriptor(parent.getDescriptor());
// check for mapped type override
builder.setType(parent.getType());
if (targetNodeType != null) {
if (parent.getType().equals(XSSchema.ANYTYPE_TYPE)) {
// special handling for casting any type since there's no attributes in its
// schema
leafAttribute = builder.addAnyTypeValue(convertedValue, targetNodeType,
descriptor, id);
} else {
leafAttribute = builder.add(id, convertedValue, attributeName, targetNodeType);
}
} else if (value == null && descriptor.getType().equals(XSSchema.ANYTYPE_TYPE)) {
// casting anyType as a complex attribute so we can set xlink:href
leafAttribute = builder.addComplexAnyTypeAttribute(convertedValue, descriptor, id);
} else {
leafAttribute = builder.add(id, convertedValue, attributeName);
}
if (index > -1) {
// set attribute index if specified so it can be retrieved later for grouping
leafAttribute.getUserData().put(ComplexFeatureConstants.MAPPED_ATTRIBUTE_INDEX,
index);
}
List newValue = new ArrayList();
newValue.addAll((Collection) parent.getValue());
newValue.add(leafAttribute);
parent.setValue(newValue);
}
if (!isEmpty(convertedValue)) {
leafAttribute.setValue(convertedValue);
}
return leafAttribute;
}
private boolean isEmpty(Object convertedValue) {
if (convertedValue == null) {
return true;
} else if (convertedValue instanceof Collection && ((Collection) convertedValue).isEmpty()) {
return true;
} else {
return false;
}
}
/**
* Return value converted into a type suitable for this descriptor.
*
* @param descriptor
* @param value
* @return
*/
@SuppressWarnings("serial")
private Object convertValue(final AttributeDescriptor descriptor, final Object value) {
final AttributeType type = descriptor.getType();
Class<?> binding = type.getBinding();
if (type instanceof ComplexType && binding == Collection.class) {
if (!(value instanceof Collection) && isSimpleContentType(type)) {
ArrayList<Property> list = new ArrayList<Property>();
if (value == null && !descriptor.isNillable()) {
return list;
}
list.add(buildSimpleContent(type, value));
return list;
}
}
if (binding == String.class && value instanceof Collection) {
// if it's a single value in a collection, strip the square brackets
String collectionString = value.toString();
return collectionString.substring(1, collectionString.length() - 1);
}
return FF.literal(value).evaluate(value, binding);
}
/**
* Return true if the type is either a simple type or has a simple type as its supertype. In
* particular, complex types with simple content will return true.
*
* @param type
* @return
*/
static boolean isSimpleContentType(AttributeType type) {
if (type == XSSchema.ANYSIMPLETYPE_TYPE) {
// should never happen as this type is abstract
throw new RuntimeException("Unexpected simple type");
}
AttributeType superType = type.getSuper();
if (superType == XSSchema.ANYSIMPLETYPE_TYPE) {
return true;
} else if (superType == null) {
return false;
} else {
return isSimpleContentType(superType);
}
}
/**
* Get base (non-collection) type of simple content.
*
* @param type
* @return
*/
static AttributeType getSimpleContentType(AttributeType type) {
Class<?> binding = type.getBinding();
if (binding == Collection.class) {
return getSimpleContentType(type.getSuper());
} else {
return type;
}
}
/**
* Create a fake property for simple content of a complex type.
*
* @param type
* @param value
* @return
*/
Attribute buildSimpleContent(AttributeType type, Object value) {
AttributeType simpleContentType = getSimpleContentType(type);
Object convertedValue = FF.literal(value).evaluate(value,
getSimpleContentType(type).getBinding());
Name name = new NameImpl(null, "simpleContent");
AttributeDescriptor descriptor = new AttributeDescriptorImpl(simpleContentType, name, 1, 1,
true, (Object) null);
return new AttributeImpl(convertedValue, descriptor, null);
}
public boolean isComplexType(final StepList attrXPath, final AttributeDescriptor featureType) {
PropertyName attExp = FF.property(attrXPath.toString());
Object type = attExp.evaluate(featureType);
if (type == null) {
type = attExp.evaluate(featureType);
throw new IllegalArgumentException("path not found: " + attrXPath);
}
AttributeDescriptor node = (AttributeDescriptor) type;
return node.getType() instanceof ComplexType;
}
}