-
Notifications
You must be signed in to change notification settings - Fork 39
How to use the FilterNode
SCIM defines a filter-language that is used to filter for resources. This chapter will explain how the FilterNode
-implementation that is passed to the ResourceHandler
can be utilized.
The FilterNode
is a built in a tree-like structure that determines how the expression should be resolved and it has exactly 5 relevant usecase implementations:
- AndExpressionNode
- OrExpressionNode
- NotExpressionNode
- AttributePathRoot
- AttirubteExpressionLeaf
First of all we will discuss the AttributeExpressionLeaf
and the AttributePathRoot
.
AttributeExpressionLeaf
This node represents a simple expression like the following:
userName eq "chuck"
externalId sw "1"
name.givenName ew "lo"
meta.created lt "2022-10-17T01:07:00Z"
name pr
emails.display pr
etc.
AttributePathRoot
An AttributePathRoot
is used on complex types in combination with a nested filter expression:
name[givenName eq "larry" and familyName sw "ha"]
emails[value eq "max@mustermann.de"]
The AttributePathRoot
will then contain a child-element:
FilterNode currentNode = filterNode;
if (filterNode instanceof AttributePathRoot)
{
AttributePathRoot attributePathRoot = (AttributePathRoot)filterNode;
currentNode = attributePathRoot.getChild();
}
and this child will then be one of the other 4 implementations listed above [AndExpressionNode
, OrExpressionNode
, NotExpressionNode
, AttirubteExpressionLeaf
].
AndExpressionNode
Represents a simple and expression:
userName sw "u" and userName ew "a"
name[givenName sw "u" and familyName ew "d"] // (will be an AttributeExpressionPath but with an AndExpressionNode as child)
OrExpressionNode
Represents a simple or expression:
userName sw "u" or userName ew "a"
name[givenName sw "u" or familyName ew "d"] // (will be an AttributeExpressionPath but with an OrExpressionNode as child)
NotExpressionNode
Any expression but negated. The NotExpressionNode
contains only a right-node that might be any other node-type:
not(userName sw "a")
not(name[givenName sw "u" or familyName ew "a"])
here is tiny part of a real world implementation that is running in production:
/**
* parses the SCIM filter node into a JPQL where-clause representation. The filternode is built in a logical
* tree-like structure that makes it very easy to translate it into a valid JPQL where-expression
*
* @param filterNode the SCIM filter expression in a tree-structure
* @return the JPQL where filter-expression
*/
protected String getFilterExpression(FilterNode filterNode)
{
if (filterNode == null)
{
return "";
}
FilterNode currentNode = filterNode;
if (filterNode instanceof AttributePathRoot)
{
AttributePathRoot attributePathRoot = (AttributePathRoot)filterNode;
currentNode = attributePathRoot.getChild();
}
if (currentNode instanceof AndExpressionNode)
{
AndExpressionNode andExpressionNode = (AndExpressionNode)currentNode;
return "(" + getFilterExpression(andExpressionNode.getLeftNode()) + " AND "
+ getFilterExpression(andExpressionNode.getRightNode()) + ")";
}
else if (currentNode instanceof OrExpressionNode)
{
OrExpressionNode orExpressionNode = (OrExpressionNode)currentNode;
return "(" + getFilterExpression(orExpressionNode.getLeftNode()) + " OR "
+ getFilterExpression(orExpressionNode.getRightNode()) + ")";
}
else if (currentNode instanceof NotExpressionNode)
{
NotExpressionNode notExpressionNode = (NotExpressionNode)currentNode;
return "NOT (" + getFilterExpression(notExpressionNode.getRightNode()) + ")";
}
else
{
AttributeExpressionLeaf attributeExpressionLeaf = (AttributeExpressionLeaf)currentNode;
boolean isCaseExact = attributeExpressionLeaf.getSchemaAttribute().isCaseExact();
final String fullResourceName = attributeExpressionLeaf.getSchemaAttribute().getFullResourceName();
String expression = ...;
...
final String comparisonExpression = resolveComparator(jpqlAttribute, attributeExpressionLeaf);
...
return expression;
}
}
...
/**
* translates the current attribute comparison into its JPQL representation and adds parameters instead of
* direct values into the JPQL query. The parameters will be added into the {@link #parameterResolverList}
* which will then later be added as Query-parameter with JPA in order to prevent SQL-injections.
*
* @param jpqlAttribute
* @param attributeExpressionLeaf the SCIM attribute-filter-expression that should resolve to something like
*
* <pre>
* u.userName = :aac0c224621adc44a29f6ddd619b5b12a6
* </pre>
* <p>
* where the string "ac0c224621adc44a29f6ddd619b5b12a6" represents the parameter name
* @return the JPQL attribute comparison string
*/
private String resolveComparator(String jpqlAttribute, AttributeExpressionLeaf attributeExpressionLeaf)
{
SchemaAttribute schemaAttribute = attributeExpressionLeaf.getSchemaAttribute();
final Comparator comparator = attributeExpressionLeaf.getComparator();
final String parameterName = "a" + UUID.randomUUID().toString().replaceAll("-", "");
boolean isCaseExact = schemaAttribute.isCaseExact();
final String jpqlParameter = toCaseCheckedValue(attributeExpressionLeaf.getType(),
isCaseExact,
":" + parameterName);
switch (comparator)
{
case EQ: // equals
setParameterValue(attributeExpressionLeaf, parameterName);
return jpqlAttribute + " = " + jpqlParameter;
case NE: // not equals
setParameterValue(attributeExpressionLeaf, parameterName);
return String.format("%1$s != %2$s or %1$s is null", jpqlAttribute, jpqlParameter);
case CO: // contains
setParameterValue(attributeExpressionLeaf, parameterName);
return jpqlAttribute + " like concat('%', " + jpqlParameter + ", '%')";
case SW: // start with
setParameterValue(attributeExpressionLeaf, parameterName);
return jpqlAttribute + " like concat(" + jpqlParameter + ", '%')";
case EW: // ends with
setParameterValue(attributeExpressionLeaf, parameterName);
return jpqlAttribute + " like concat('%', " + jpqlParameter + ")";
case GE: // greater equals
setParameterValue(attributeExpressionLeaf, parameterName);
return jpqlAttribute + " >= " + jpqlParameter;
case LE: // lower equals
setParameterValue(attributeExpressionLeaf, parameterName);
return jpqlAttribute + " <= " + jpqlParameter;
case GT: // greater than
setParameterValue(attributeExpressionLeaf, parameterName);
return jpqlAttribute + " > " + jpqlParameter;
case LT: // lower than
setParameterValue(attributeExpressionLeaf, parameterName);
return jpqlAttribute + " < " + jpqlParameter;
default: // is "PR" = present
return jpqlAttribute + " is not null";
}
}
/**
* if a SCIM attribute is defined as not case exact the database attributes will be converted into lower case
* to enable a case-insensitive search
*
* @param type lower case makes only sense for string-type values
* @param isCaseExact if the attribute should be compared case-insensitive or not
* @param jpqlParameterName the name of the jpql-parameter e.g. "u.userName"
* @return the unchanged parameter if a case-sensitive check is required and the parameter surrounded by
* "lower(...)" if the check should be case-insensitive
*/
private String toCaseCheckedValue(Type type, boolean isCaseExact, String jpqlParameterName)
{
final boolean isNotStringType = !Type.STRING.equals(type) && !Type.REFERENCE.equals(type);
if (isCaseExact || isNotStringType)
{
return jpqlParameterName;
}
else
{
return String.format("lower(%s)", jpqlParameterName);
}
}