/* (c) 2014 Open Source Geospatial Foundation - all rights reserved
* (c) 2001 - 2013 OpenPlans
* This code is licensed under the GPL 2.0 license, available at the root
* application directory.
*/
package org.geoserver.wfs;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import org.geoserver.catalog.FeatureTypeInfo;
import org.geotools.data.Join;
import org.geotools.factory.CommonFactoryFinder;
import org.geotools.filter.FilterAttributeExtractor;
import org.geotools.filter.visitor.DuplicatingFilterVisitor;
import org.geotools.filter.visitor.FilterVisitorSupport;
import org.opengis.filter.And;
import org.opengis.filter.BinaryComparisonOperator;
import org.opengis.filter.BinaryLogicOperator;
import org.opengis.filter.ExcludeFilter;
import org.opengis.filter.Filter;
import org.opengis.filter.FilterFactory2;
import org.opengis.filter.Id;
import org.opengis.filter.IncludeFilter;
import org.opengis.filter.Not;
import org.opengis.filter.PropertyIsBetween;
import org.opengis.filter.PropertyIsLike;
import org.opengis.filter.PropertyIsNil;
import org.opengis.filter.PropertyIsNull;
import org.opengis.filter.expression.Expression;
import org.opengis.filter.expression.PropertyName;
import org.opengis.filter.spatial.BinarySpatialOperator;
import org.opengis.filter.temporal.BinaryTemporalOperator;
public class JoinExtractingVisitor extends FilterVisitorSupport {
static FilterFactory2 ff = CommonFactoryFinder.getFilterFactory2(null);
FeatureTypeInfo primaryFeatureType;
String primaryAlias;
List<FeatureTypeInfo> featureTypes;
List<String> aliases;
boolean hadAliases;
List<Filter> joinFilters = new ArrayList<Filter>();
List<Filter> filters = new ArrayList<Filter>();
public JoinExtractingVisitor(List<FeatureTypeInfo> featureTypes, List<String> aliases) {
this.primaryFeatureType = null;
this.featureTypes = new ArrayList<>(featureTypes);
if (aliases == null || aliases.isEmpty()) {
hadAliases = false;
// assign prefixes
aliases = new ArrayList<String>();
for (int j = 0, i = 0; i < featureTypes.size(); i++) {
String alias;
boolean conflictFound;
do {
conflictFound = false;
alias = String.valueOf((char) ('a' + (j++)));
for (FeatureTypeInfo ft : featureTypes) {
if(alias.equals(ft.getName()) || alias.equals(ft.prefixedName())) {
conflictFound = true;
break;
}
}
} while (conflictFound);
aliases.add(alias);
}
} else {
hadAliases = true;
}
this.aliases = new ArrayList(aliases);
}
public Object visitNullFilter(Object extraData) {
return null;
}
public Object visit(ExcludeFilter filter, Object extraData) {
return handleOther(filter, extraData);
}
public Object visit(IncludeFilter filter, Object extraData) {
return handleOther(filter, extraData);
}
public Object visit(Id filter, Object extraData) {
return handleOther(filter, extraData);
}
public Object visit(Not filter, Object extraData) {
if (isJoinFilter(filter.getFilter(), extraData)) {
checkValidJoinFilter(filter);
joinFilters.add(filter);
} else {
handleOther(filter, extraData);
}
return extraData;
}
private void checkValidJoinFilter(Filter filter) {
Set<String> prefixes = getFilterPrefixes(filter);
if (prefixes.size() > 2) {
throw new WFSException("Not subfilter joins against more than one table "
+ prefixes + ", this kind of filter is not supported: " + filter);
}
}
public Object visit(PropertyIsBetween filter, Object extraData) {
return handle(filter, extraData, filter.getLowerBoundary(), filter.getUpperBoundary(),
filter.getUpperBoundary());
}
public Object visit(PropertyIsLike filter, Object extraData) {
return handleOther(filter, extraData);
}
public Object visit(PropertyIsNull filter, Object extraData) {
return handleOther(filter, extraData);
}
public Object visit(PropertyIsNil filter, Object extraData) {
return handleOther(filter, extraData);
}
@Override
protected Object visit(BinaryLogicOperator op, Object extraData) {
if (op instanceof And) {
for (Filter f : op.getChildren()) {
f.accept(this, extraData);
}
} else {
boolean joinFilter = false;
for (Filter child : op.getChildren()) {
if (isJoinFilter(child, extraData)) {
joinFilter = true;
break;
}
}
if (joinFilter) {
checkValidJoinFilter(op);
joinFilters.add(op);
} else {
handleOther(op, extraData);
}
return extraData;
}
return extraData;
}
@Override
protected Object visit(BinaryComparisonOperator op, Object extraData) {
return handle(op, extraData, op.getExpression1(), op.getExpression2());
}
@Override
protected Object visit(BinarySpatialOperator op, Object extraData) {
return handle(op, extraData, op.getExpression1(), op.getExpression2());
}
@Override
protected Object visit(BinaryTemporalOperator op, Object extraData) {
return handle(op, extraData, op.getExpression1(), op.getExpression2());
}
Object handle(Filter f, Object extraData, Expression... expressions) {
if (isJoinFilter(expressions)) {
joinFilters.add(f);
}
else {
handleOther(f, extraData);
}
return null;
}
Object handleOther(Filter f, Object extraData) {
filters.add(f);
return null;
}
boolean isJoinFilter(Expression... expressions) {
// Used to check if the expressions were all property names
// however, f(t1.x) = t2.y is also a join filter, and t1.x + 2 = t2.y too
// So, generalized it a bit, it's still not fully correct though,
// as it can be fooled by a.x = a.y, which is not a join filter... but we
// can have the full name (no alias) twice in a self join, and that would be a valid join... uff!
Set<String> prefixes = new HashSet<>();
for (Expression ex : expressions) {
FilterAttributeExtractor fae = new FilterAttributeExtractor();
ex.accept(fae, null);
Set<String> localAttributes = fae.getAttributeNameSet();
Set<String> localPrefixes = getPrefixes(localAttributes);
if (!localPrefixes.isEmpty()) {
if (prefixes.size() == 0) {
// accumulate the prefixes, to see how many tables we're joining
prefixes.addAll(localPrefixes);
} else if (prefixes.size() > 1) {
// e.g. f(a.x,b.y)=b.z
return true;
} else {
// is it a comparison among attributes of the same table, or not?
// e.g., a.x = a.y is not a join filter (a self join would use two different
// aliases)
localPrefixes.removeAll(prefixes);
if (!localPrefixes.isEmpty()) {
return true;
}
}
}
}
return false;
}
private boolean isJoinFilter(Filter filter, Object extraData) {
JoinExtractingVisitor visitor = new JoinExtractingVisitor(featureTypes, aliases);
filter.accept(visitor, extraData);
return !visitor.joinFilters.isEmpty();
}
private Set<String> getPrefixes(Set<String> attributes) {
Set<String> result = new HashSet<>();
for (String attribute : attributes) {
int idx = attribute.indexOf('/');
if (idx > 0) {
String prefix = attribute.substring(0, idx);
result.add(prefix);
}
}
return result;
}
public List<Join> getJoins() {
List<Join> joins = new ArrayList();
setupPrimary();
//unroll the contents of the join filters and rewrite them and and assign to correct
//feature type
List<Filter> joinFilters = rewriteAndSortJoinFilters(this.joinFilters);
//do same for other secondary filters
List<Filter> otherFilters = rewriteAndSortOtherFilters(this.filters);
for (int i = 0; i < featureTypes.size(); i++) {
String nativeName = featureTypes.get(i).getNativeName();
Join join = new Join(nativeName, joinFilters.get(i+1));
if (aliases != null) {
join.setAlias(aliases.get(i));
}
if (otherFilters.get(i+1) != null) {
join.setFilter(otherFilters.get(i+1));
}
joins.add(join);
}
return joins;
}
/**
* Returns the joined feature types. If called past join extraction, it will return the types in
* the same order as the joins (which might have been reordered to locate the center of the star
* join)
*
*
*/
public List<FeatureTypeInfo> getFeatureTypes() {
if (primaryFeatureType == null) {
return featureTypes;
} else {
List<FeatureTypeInfo> result = new ArrayList<>();
result.add(primaryFeatureType);
result.addAll(featureTypes);
return result;
}
}
/**
* Find the center of the star join, and remove it from the feature types and aliases arrays the
* rest of the algorithm is setup to have only the secondary types in these arrays
*/
private void setupPrimary() {
if (primaryFeatureType == null) {
int idx = getPrimaryFeatureTypeIndex(this.joinFilters);
primaryFeatureType = featureTypes.get(idx);
primaryAlias = aliases.get(idx);
featureTypes.remove(idx);
aliases.remove(idx);
}
}
public Filter getPrimaryFilter() {
setupPrimary();
List<Filter> otherFilters = rewriteAndSortOtherFilters(filters);
return otherFilters.get(0);
}
public String getPrimaryAlias() {
setupPrimary();
return primaryAlias;
}
public FeatureTypeInfo getPrimaryFeatureType() {
setupPrimary();
return primaryFeatureType;
}
List<Filter> rewriteAndSortJoinFilters(List<Filter> filters) {
Map<String, FeatureTypeInfo> typeMap = buildTypeMap();
Map<String, String> nameToAlias = buildNameToAlias();
String primaryName = primaryFeatureType.prefixedName();
String primaryUnqualifiedName = primaryFeatureType.getName();
PropertyNameRewriter rewriter = new PropertyNameRewriter(nameToAlias, true);
Filter[] sorted = new Filter[featureTypes.size() + 1];
for (Filter filter : filters) {
Set<String> prefixes = getFilterPrefixes(filter);
prefixes.remove(primaryAlias);
prefixes.remove(primaryName);
prefixes.remove(primaryUnqualifiedName);
if (prefixes.size() != 1) {
throw new WFSException("Extracted invalid join filter " + filter
+ ", it joins more than " + "one secondary feature type + " + prefixes
+ " with the central join feature type " + primaryAlias + "/" + primaryName);
}
Filter rewritten = (Filter) filter.accept(rewriter, null);
String alias = prefixes.iterator().next();
FeatureTypeInfo ft = typeMap.get(alias);
int idx = featureTypes.indexOf(ft);
if (idx == -1) {
throw new WFSException("Extracted invalid join filter " + filter
+ ", it uses the unkonwn alias/typename " + alias);
}
updateFilter(sorted, idx + 1, rewritten);
}
return Arrays.asList(sorted);
}
private Set<String> getFilterPrefixes(Filter filter) {
FilterAttributeExtractor extractor = new FilterAttributeExtractor();
filter.accept(extractor, null);
Set<String> attributeNames = extractor.getAttributeNameSet();
Set<String> prefixes = getPrefixes(attributeNames);
return prefixes;
}
List<Filter> rewriteAndSortOtherFilters(List<Filter> filters) {
String primaryName = primaryFeatureType.prefixedName();
Map<String, FeatureTypeInfo> typeMap = buildTypeMap();
Map<String, String> nameToAlias = buildNameToAlias();
PropertyNameRewriter rewriter = new PropertyNameRewriter(nameToAlias, false);
Filter[] sorted = new Filter[featureTypes.size() + 1];
for (Filter filter : filters) {
Set<String> prefixes = getFilterPrefixes(filter);
prefixes.remove(primaryName);
if (prefixes.size() != 1) {
throw new WFSException("Extracted invalid join sub-filter " + filter
+ ", it users more than one feature type + " + prefixes);
}
Filter rewritten = (Filter) filter.accept(rewriter, null);
String alias = prefixes.iterator().next();
FeatureTypeInfo ft = typeMap.get(alias);
if (primaryFeatureType.equals(ft)) {
updateFilter(sorted, 0, rewritten);
} else {
int idx = featureTypes.indexOf(ft);
if (idx == -1) {
throw new WFSException("Extracted invalid join filter " + filter
+ ", it uses the unkonwn alias/typename " + alias);
}
updateFilter(sorted, idx + 1, rewritten);
}
}
return Arrays.asList(sorted);
}
/**
* Builds a map going from alias, prefixed type name and simple type name to FeatureTypeInfo. In
* case of conflicts aliases will override the type names
*
*
*/
private Map<String, FeatureTypeInfo> buildTypeMap() {
Map<String, FeatureTypeInfo> typeMap = new HashMap<>();
typeMap.put(primaryFeatureType.prefixedName(), primaryFeatureType);
typeMap.put(primaryFeatureType.getName(), primaryFeatureType);
typeMap.put(primaryAlias, primaryFeatureType);
for (int i = 0; i < aliases.size(); i++) {
String alias = aliases.get(i);
FeatureTypeInfo ft = featureTypes.get(i);
typeMap.put(ft.getName(), ft);
typeMap.put(ft.prefixedName(), ft);
typeMap.put(alias, ft);
}
return typeMap;
}
/**
* Builds a map going from type name, qualified or unqualified, to alias
*
*
*/
private Map<String, String> buildNameToAlias() {
Map<String, String> nameToAlias = new HashMap<>();
nameToAlias.put(primaryFeatureType.prefixedName(), primaryAlias);
nameToAlias.put(primaryFeatureType.getName(), primaryAlias);
for (int i = 0; i < aliases.size(); i++) {
String alias = aliases.get(i);
FeatureTypeInfo ft = featureTypes.get(i);
nameToAlias.put(ft.getName(), alias);
nameToAlias.put(ft.prefixedName(), alias);
}
return nameToAlias;
}
/**
* Geotools only support "star" joins with a primary being the center of the join. Figure out if
* we have one feature type that is acting as the center of the star, or throw an exception if
* we don't have one.
*
* @param filters2
*
*/
private int getPrimaryFeatureTypeIndex(List<Filter> filters) {
if (featureTypes.size() == 2) {
return 0;
}
List<Integer> connecteds = new ArrayList<>();
for (int i = 0; i < featureTypes.size(); i++) {
connecteds.add(i);
}
for (Filter filter : filters) {
Set<String> filterPrefixes = getFilterPrefixes(filter);
Set<Integer> nameTypes = getPropertyNameTypeIndexes(filterPrefixes);
connecteds.retainAll(nameTypes);
}
if (connecteds.isEmpty()) {
throw new WFSException(
"Cannot run this type of join, at the moment GeoServer only supports "
+ "joins having a single central feature type joined to all others");
} else {
return connecteds.iterator().next();
}
}
private Set<Integer> getPropertyNameTypeIndexes(Set<String> filterPrefixes) {
Set<Integer> nameTypes = new HashSet<>();
for (String prefix : filterPrefixes) {
int aliasIdx = aliases.indexOf(prefix);
if (aliasIdx >= 0) {
nameTypes.add(aliasIdx);
} else {
for (int i = 0; i < featureTypes.size(); i++) {
FeatureTypeInfo ft = featureTypes.get(i);
if (prefix.equals(ft.prefixedName()) || prefix.equals(ft.getName())) {
nameTypes.add(i);
break;
}
}
}
}
return nameTypes;
}
void updateFilter(Filter[] filters, int i, Filter filter) {
if (filters[i] == null) {
filters[i] = filter;
}
else {
filters[i] = ff.and(filters[i], filter);
}
}
/**
* Rewrites property names to either remove the join prefixes (for local filters) or replace the
* <code>alias/attribute</code> or <code>typename/attribute</code> syntax with a
* <code>alias.attribute</code> syntax
*
* @author Andrea Aime - GeoSolutions
*
*/
class PropertyNameRewriter extends DuplicatingFilterVisitor {
Map<String, String> nameToAlias;
private Set<String> prefixes;
boolean addPrefix;
public PropertyNameRewriter(Map<String, String> nameToAlias, boolean prefix) {
super();
this.prefixes = new HashSet<>();
this.prefixes.addAll(nameToAlias.keySet());
this.prefixes.addAll(nameToAlias.values());
this.prefixes.add(primaryAlias);
this.nameToAlias = nameToAlias;
this.addPrefix = prefix;
}
@Override
public Object visit(PropertyName expression, Object extraData) {
String n = expression.getPropertyName();
int idx = n.indexOf('/');
String prefix = null;
if (idx > 0) {
prefix = n.substring(0, idx);
if (prefixes.contains(prefix)) {
if (nameToAlias.get(prefix) != null) {
prefix = nameToAlias.get(prefix);
}
n = n.substring(idx + 1);
} else {
n = null;
}
} else {
n = null;
}
if (n != null) {
if (addPrefix) {
n = (prefix != null ? prefix : "") + "." + n;
}
return ff.property(n);
}
return null;
}
}
}