/* * GeoTools - The Open Source Java GIS Toolkit * http://geotools.org * * (C) 2016, 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.io.IOException; import java.util.ArrayList; import java.util.List; import java.util.logging.Level; import java.util.logging.Logger; import javax.xml.namespace.QName; 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.XPathUtil.Step; import org.geotools.data.complex.filter.XPathUtil.StepList; import org.geotools.data.joining.JoiningNestedAttributeMapping; import org.geotools.filter.visitor.DefaultExpressionVisitor; import org.geotools.util.logging.Logging; import org.geotools.xlink.XLINK; import org.opengis.feature.Feature; import org.opengis.feature.type.AttributeType; import org.opengis.feature.type.Name; import org.opengis.filter.expression.Expression; import org.opengis.filter.expression.PropertyName; import org.xml.sax.helpers.NamespaceSupport; /** * Expression visitor that uses the attribute and mapping information provided by a {@link FeatureTypeMapping} object to determine which nested * feature types / attributes must be traversed to reach the attribute identified by the provided {@link PropertyName} expression. * * <p> * The provided {@link FeatureTypeMapping} object is regarded as the root mapping against which the expression is evaluated. * </p> * * <p> * The nested attribute mappings are returned as a list of {@link FeatureChainLink} objects; the first one in the list always refers to the root * mapping. * </p> * * @author Stefano Costa, GeoSolutions * */ public class FeatureChainedAttributeVisitor extends DefaultExpressionVisitor { private static final Logger LOGGER = Logging.getLogger(FeatureChainedAttributeVisitor.class); private FeatureTypeMapping rootMapping; private List<FeatureChainedAttributeDescriptor> attributes; public FeatureChainedAttributeVisitor(FeatureTypeMapping root) { if (root == null) { throw new NullPointerException("root mapping is null"); } this.attributes = new ArrayList<>(); this.rootMapping = root; } @Override public Object visit(PropertyName expression, Object data) { if (expression == null) { throw new NullPointerException("expression is null"); } Feature feature = null; if (data != null && !(data instanceof Feature)) { feature = (Feature) data; } // reset outcome of the visit attributes = new ArrayList<>(); try { walkXPath(expression.getPropertyName(), feature); } catch (IOException e) { throw new RuntimeException( "Exception occurred splitting XPath expression into mapping steps", e); } return getFeatureChainedAttributes(); } void walkXPath(String xpath, Feature feature) throws IOException { FeatureTypeMapping currentType = rootMapping; StepList currentXPath = XPath.steps(rootMapping.getTargetFeature(), xpath, rootMapping.getNamespaces()); FeatureChainedAttributeDescriptor attrDescr = new FeatureChainedAttributeDescriptor(); walkXPathRecursive(currentXPath, currentType, attrDescr, feature); } private void walkXPathRecursive(StepList currentXPath, FeatureTypeMapping currentType, FeatureChainedAttributeDescriptor attrDescr, Feature feature) throws IOException { List<NestedAttributeMapping> currentAttributes = currentType.getNestedMappings(); boolean searchIsOver = true; for (NestedAttributeMapping nestedAttr : currentAttributes) { StepList targetXPath = nestedAttr.getTargetXPath(); if (currentXPath.startsWith(targetXPath)) { if (nestedAttr.isConditional() && feature == null) { logConditionalMappingFound(currentType, targetXPath); // quit the search return; } else { FeatureTypeMapping nestedType = nestedAttr.getFeatureTypeMapping(feature); if (nestedType != null) { AttributeType nestedPropertyType = nestedType.getTargetFeature().getType(); QName nestedTypeQName = getFeatureTypeQName(nestedType); Step nestedTypeStep = new Step(nestedTypeQName, 1); StepList nestedTypeXPath = targetXPath.clone(); nestedTypeXPath.add(nestedTypeStep); boolean xpathContainsNestedType = currentXPath.startsWith(nestedTypeXPath); boolean hasSimpleContent = Types.isSimpleContentType(nestedPropertyType); // if this is feature chaining for simple content, the name of the nested type // may not be present in the XPath, as it was already specified as the container // property (e.g. see mappings doing chaining for gml:name) if (xpathContainsNestedType || hasSimpleContent) { LOGGER.finer("Nested feature type found: " + nestedTypeQName); FeatureChainedAttributeDescriptor copy = attrDescr.shallowCopy(); copy.addLink(new FeatureChainLink(currentType, nestedAttr)); // new root mapping to search FeatureTypeMapping newType = nestedType; // new xpath StepList newXPath = currentXPath.clone(); int startIdx = (xpathContainsNestedType) ? nestedTypeXPath.size() : currentXPath.size(); newXPath = newXPath.subList(startIdx, currentXPath.size()); // if nested type has simple content, XPath expression may point directly // to the type, and not to one of its attributes (which, BTW, can only // be client properties, or it wouldn't be simple content) if (newXPath.isEmpty() && hasSimpleContent) { newXPath.add(nestedTypeStep); } // recursive call walkXPathRecursive(newXPath, newType, copy, feature); // I'm not done yet searchIsOver = false; } } else { logNestedFeatureTypeNotFound(currentType, targetXPath); } } } } // add last attribute mapping, which is a direct child of the last nested feature found if (searchIsOver && currentXPath != null && !currentXPath.isEmpty()) { StepList lastAttrPath = currentXPath; List<Expression> lastAttrExpressions = currentType.findMappingsFor(lastAttrPath, false); if (lastAttrExpressions != null && lastAttrExpressions.size() > 0) { attrDescr.setAttributePath(lastAttrPath); // check whether this is a case of feature chaining by reference if (isClientProperty(lastAttrPath) && isXlinkHref(lastAttrPath)) { StepList parentAttrPath = lastAttrPath.subList(0, lastAttrPath.size() - 1); AttributeMapping parentAttr = currentType.getAttributeMapping(parentAttrPath); if (parentAttr != null && parentAttr instanceof NestedAttributeMapping) { // yes, it's feature chaining by reference: add another step to the chain NestedAttributeMapping nestedAttr = (NestedAttributeMapping) parentAttr; attrDescr.addLink(new FeatureChainLink(currentType, nestedAttr)); // add last step if (nestedAttr.isConditional() && feature == null) { logConditionalMappingFound(currentType, nestedAttr.getTargetXPath()); // abort search return; } else { FeatureTypeMapping nestedType = nestedAttr .getFeatureTypeMapping(feature); if (nestedType != null) { FeatureChainLink lastLink = new FeatureChainLink(nestedType, true); attrDescr.addLink(lastLink); // search was successful, add attribute to collection attributes.add(attrDescr); } else { logNestedFeatureTypeNotFound(currentType, nestedAttr.getTargetXPath()); } } } } else { attrDescr.addLink(new FeatureChainLink(currentType)); // search was successful, add attribute to collection attributes.add(attrDescr); } } } } private void logConditionalMappingFound(FeatureTypeMapping containerType, StepList xpath) { if (LOGGER.isLoggable(Level.FINE)) { QName qname = getFeatureTypeQName(containerType); String prefixedName = qname.getPrefix() + ":" + qname.getLocalPart(); LOGGER.fine("Conditional nested mapping found, but no feature to evaluate " + "against was provided: nested feature type cannot be determined for " + "container type \"" + prefixedName + "\" and target attribute \"" + xpath); } } private void logNestedFeatureTypeNotFound(FeatureTypeMapping containerType, StepList xpath) { if (LOGGER.isLoggable(Level.FINE)) { QName qname = getFeatureTypeQName(containerType); String prefixedName = qname.getPrefix() + ":" + qname.getLocalPart(); LOGGER.fine("Nested type could not be determined for container type \"" + prefixedName + "\" and target attribute \"" + xpath); } } private QName getFeatureTypeQName(FeatureTypeMapping featureTypeMapping) { NamespaceSupport nsSupport = featureTypeMapping.getNamespaces(); Name featureTypeName = featureTypeMapping.getTargetFeature().getName(); String uri = featureTypeName.getNamespaceURI(); String localPart = featureTypeName.getLocalPart(); String prefix = nsSupport.getPrefix(uri); return new QName(uri, localPart, prefix); } private boolean isClientProperty(StepList steps) { if (steps.isEmpty()) { return false; } return steps.get(steps.size() - 1).isXmlAttribute(); } 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); } /** * Returns an object describing the sequence of feature chaining links that must be traversed to reach the attribute specified by the visited * expression. * * @return a feature chained attribute descriptor, or <code>null</code> if none was found */ public List<FeatureChainedAttributeDescriptor> getFeatureChainedAttributes() { return attributes; } /** * Descriptor class holding information about a feature chained attribute, i.e. an attribute belonging to a feature type that is linked to a root * feature type via feature chaining. * * <p> * In more detail, purpose of this class is to store: * <ol> * <li>the sequence of nested attribute mappings describing how from top to bottom in the feature types chain</li> * <li>the path of the attribute, relative to the last linked feature type in the chain (except when the last chaining is done by reference, in * which case the path refers to the second last feature type)</li> * </ol> * </p> * * @author Stefano Costa, GeoSolutions * */ public static class FeatureChainedAttributeDescriptor { private List<FeatureChainLink> featureChain; private StepList attributePath; private FeatureChainedAttributeDescriptor() { featureChain = new ArrayList<>(); } /** * Returns the list of links in the feature types chain. * * @return a copy of the internal feature chain links list */ public List<FeatureChainLink> getFeatureChain() { return new ArrayList<>(featureChain); } /** * Adds a new link in the feature types chain. * * @param chainLink the link to add */ public void addLink(FeatureChainLink chainLink) { if (chainLink == null) { throw new NullPointerException("chainLink is null"); } featureChain.add(chainLink); int size = featureChain.size(); String alias = (size == 1) ? "chain_root" : "chain_link_" + (size - 1); chainLink.setAlias(alias); if (size > 1) { FeatureChainLink previousStep = featureChain.get(size - 2); previousStep.nextStep = chainLink; chainLink.previousStep = previousStep; } } /** * Gets a link in the feature types chain by its index. * * @param linkIdx the link index (0-based) * @return the feature chain link corresponding to the provided index * @throws IllegalArgumentException if <code>linkIdx</code> is negative * @throws IndexOutOfBoundsException if <code>linkIdx</code> is >= than the chain size */ public FeatureChainLink getLink(int linkIdx) { if (linkIdx < 0) { throw new IllegalArgumentException("linkIdx must be > 0"); } if (linkIdx >= featureChain.size()) { throw new IndexOutOfBoundsException("linkIdx " + linkIdx + " is not present"); } return featureChain.get(linkIdx); } /** * Gets the first link in the feature types chain. * * @return the first feature chain link * @throws IndexOutOfBoundsException if the feature types chain is empty */ public FeatureChainLink getFirstLink() { if (featureChain.size() == 0) { throw new IndexOutOfBoundsException("the list is empty"); } return featureChain.get(0); } /** * Gets the last link in the feature types chain. * * @return the last feature chain link * @throws IndexOutOfBoundsException if the feature types chain is empty */ public FeatureChainLink getLastLink() { if (featureChain.size() == 0) { throw new IndexOutOfBoundsException("the list is empty"); } return featureChain.get(featureChain.size() - 1); } /** * Checks whether all nested attribute mappings are instances of {@link JoiningNestedAttributeMapping}. * * @return <code>true</code> if all nested attribute mappings in the chain support joining, <code>false</code> otherwise */ public boolean isJoiningEnabled() { boolean joiningEnabled = true; for (FeatureChainLink mappingStep : featureChain) { joiningEnabled = joiningEnabled && (!mappingStep.hasNestedFeature() || mappingStep.isJoiningNestedMapping()); } return joiningEnabled; } /** * Removes all elements in the feature types chain. */ void clearChain() { featureChain.clear(); } /** * Gets the size of the feature types chain. * * @return the number of links in the chain */ public int chainSize() { return featureChain.size(); } /** * Gets the path of the feature chained attribute. * * @return the attribute path */ public StepList getAttributePath() { return attributePath; } /** * Sets the path of the feature chained attribute. * * @param attributePath the attribute path to set */ public void setAttributePath(StepList attributePath) { this.attributePath = attributePath; } /** * Returns the feature type where the mapping configuration of the nested attribute is defined. * * <p> * In practice, this is the last linked feature type in the chain, except when the last chaining is done by reference (via an xlink:href * attribute), in which case the second last feature type is returned. * </p> * * @return */ public FeatureTypeMapping getFeatureTypeOwningAttribute() { FeatureChainLink lastLink = getLastLink(); FeatureTypeMapping featureMapping = lastLink.getFeatureTypeMapping(); if (lastLink.isChainingByReference()) { // last attribute xpath should be resolved against the parent feature if (lastLink.previous() != null) { featureMapping = lastLink.previous().getFeatureTypeMapping(); } } return featureMapping; } /** * Perform a shallow copy of this {@link FeatureChainedAttributeDescriptor} instance. * * @return a shallow copy of the instance */ public FeatureChainedAttributeDescriptor shallowCopy() { FeatureChainedAttributeDescriptor copy = new FeatureChainedAttributeDescriptor(); copy.featureChain.addAll(featureChain); copy.attributePath = attributePath; return copy; } } /** * Represents a single link in the "chain" of feature types that need to be linked to go from the root type to a nested attribute. * * <p> * The class is <code>public</code> as its purpose is to convey information to clients, but instantiation and manipulation of its internal state * is <code>private</code>. * </p> * * @author Stefano Costa, GeoSolutions * */ public static class FeatureChainLink { private FeatureTypeMapping featureTypeMapping; private NestedAttributeMapping nestedFeatureAttribute; private boolean chainingByReference; private String alias; private FeatureChainLink nextStep; private FeatureChainLink previousStep; private FeatureChainLink(FeatureTypeMapping featureType) { if (featureType == null) { throw new NullPointerException("featureType is null"); } this.featureTypeMapping = featureType; this.nestedFeatureAttribute = null; this.chainingByReference = false; this.alias = featureType.getSource().getSchema().getName().getLocalPart(); this.nextStep = null; this.previousStep = null; } private FeatureChainLink(FeatureTypeMapping featureType, NestedAttributeMapping nestedFeatureAttribute) { this(featureType); if (nestedFeatureAttribute == null) { throw new NullPointerException("nestedFeatureAttribute is null"); } this.nestedFeatureAttribute = nestedFeatureAttribute; } private FeatureChainLink(FeatureTypeMapping featureType, boolean chainingByReference) { this(featureType); this.chainingByReference = chainingByReference; } /** * Gets the mapping configuration of the linked feature type. * * @return the linked feature type mapping */ public FeatureTypeMapping getFeatureTypeMapping() { return featureTypeMapping; } /** * Gets the mapping configuration of the attribute holding the next nested feature in the chain. * * @return the nested attribute mapping, or <code>null</code> if there are no more nested features in the chain */ public NestedAttributeMapping getNestedFeatureAttribute() { return nestedFeatureAttribute; } /** * Gets the mapping configuration of the attribute holding the next nested feature in the chain, cast to the specified * {@link NestedAttributeMapping} subclass. * * @see #getNestedFeatureAttribute() * @param attributeMappingClass the {@link NestedAttributeMapping} subclass to cast to */ public <T extends NestedAttributeMapping> T getNestedFeatureAttribute( Class<T> attributeMappingClass) { return attributeMappingClass.cast(nestedFeatureAttribute); } /** * Returns <code>true</code> if this {@link FeatureChainLink} instance represents a chaining-by-reference mapping, i.e. nested feature is not * fully encoded inline, only <code>xlink:href</code> attribute is set. * * @return <code>true</code> if this is chaining by reference, <code>false</code> otherwise */ public boolean isChainingByReference() { return chainingByReference; } /** * Returns <code>true</code> if joining support is enabled for the nested attribute mapping. * * @return <code>true</code> if joining support is enabled for this chain link, <code>false</code> otherwise */ public boolean isJoiningNestedMapping() { return nestedFeatureAttribute != null && nestedFeatureAttribute instanceof JoiningNestedAttributeMapping; } /** * Returns <code>true</code> if this link refers to a nested feature type which in turn contains another nested feature. * * @return <code>true</code> if there is another nested feature in the chain, <code>false</code> otherwise */ public boolean hasNestedFeature() { return nestedFeatureAttribute != null; } /** * Unique identifier of a link in the chain; mainly useful when SQL encoding the feature chained attribute. * * @return a unique identifier of the link */ public String getAlias() { return alias; } private void setAlias(String alias) { this.alias = alias; } /** * Returns the next link in the chain. * * @return the next link, or <code>null</code> if none exists */ public FeatureChainLink next() { return nextStep; } /** * Returns the previous link in the chain. * * @return the previous link, or <code>null</code> if none exists */ public FeatureChainLink previous() { return previousStep; } } }