-
Notifications
You must be signed in to change notification settings - Fork 76
Working with SCIM Paths
Path objects are used pervasively when working with GenericScimResource
objects or PATCH requests. Paths are used to target specific data in a SCIM resource.
A path is most often specified as an attribute name or as an attribute with sub-attribute (using the form "attribute.sub-attribute"), though it may even include a value filter.
Bear in mind that a SCIM resource is a JSON document. Once resolved, a valid path specifies one or more nodes in the JSON document, which will be one of the following types:
- String
- Boolean
- Decimal
- Integer
- DateTime
- Binary
- Reference
- Complex
- Array
Note the last two types — a path may not necessarily resolve to a node representing a primitive type.
Consider the following JSON object:
{
"firstName": "Bill",
"lastName": "Smith",
"shoeSize": 13,
"addresses": [
{
"street": "123 1st Street",
"city": "Austin",
"state": "TX"
},
{
"street": "234 2nd Street",
"city": "Round Rock",
"state": "TX"
},
{
"street": "345 3rd Street",
"city": "Cedar Park",
"state": "TX"
}
],
"phoneNumber":
{
"areaCode": "512",
"prefix": "555",
"number": "1212"
},
"colors": [
"red",
"green",
"blue"
]
}
Given this example and a GenericScimResource
called scimResource
:
- A path of
firstName
would match theTextNode
in the document with the value of "Bill".
assert(scimResource.getValue("firstName").textValue().equals("Bill"));
- A path of
shoeSize
would match theIntNode
in the document with the value of 13.
assert(scimResource.getValue("shoeSize").intValue() == 13);
- A path of
addresses
would match a list of complex values.
assert(scimResource.getValue("addresses").isArray());
A path may have a sub-attribute component, which specifies a sub-attribute of a complex object.
For example:
-
phoneNumber.areacode
would match aTextNode
containing "512".
assert(scimResource.getValue("phoneNumber.areacode").textValue().equals("512"));
Such a path could match multiple nodes:
-
address.city
would match all of theTextNode
s in the addresses list that have a sub-attribute named "city":TextNode
"Austin",TextNode
"Cedar Park", andTextNode
"Round Rock".
List<JsonNode> addressesWithCities =
JsonUtils.findMatchingPaths(Path.fromString("addresses.city"),
scimResource.getObjectNode());
assert(addressesWithCities.size() == 3);
An additional component of the path is a value filter. A value filter may be used with or without the sub-attribute component. Without the attribute component, the filter returns the complete complex object that was matched.
Example with sub-attribute component:
-
addresses[city ne "Austin"].city
would select twoTextNode
s:TextNode
"Round Rock" andTextNode
"Cedar Park".
List<JsonNode> notAustin =
JsonUtils.findMatchingPaths(
Path.fromString("addresses[city ne \"Austin\"].city"),
scimResource.getObjectNode());
assert(notAustin.size() == 2);
Example without sub-attribute component:
-
addresses[city ne "Austin"]
would select two address complex objects. These are the two addresses where the city is not equal to "Austin".
assert(scimResource.getValue("addresses[city ne \"Austin\"]").isArray());
assert(scimResource.getValue("addresses[city ne \"Austin\"]").size() == 2);
Notice how the two previous examples differ. We'll discuss that below in the ArrayNode or List<JsonNode> section.
Simple multivalued attributes, such as "colors" in this example, have a special implicit sub-attribute called "value". This can be used to form value filters that select specific members of a simple multivalued attribute.
For example:
-
colors[value eq "green"]
would select a single-element array[ "green" ]
from the simple multivalued attribute in the example.
assert(scimResource.getValue("colors[value eq \"green\"]").isArray());
assert(scimResource.getValue("colors[value eq \"green\"]").size() == 1);
assert(scimResource.getValue("colors[value eq \"green\"]").get(0).textValue().equals("green"));
You might be wondering why an ArrayNode
([ "green" ]
) is returned instead of a TextNode
("green"
). To see why this is desirable and consistent, it may help to consider the previous example using the addresses[city ne "Austin"]
path. In that case, it should have seemed very intuitive that the path returned an array. Why? Because it matched more than one element. So consider this example:
-
colors[value ne "green"]
will select an array with two elements:[ "red", "blue" ]
assert(scimResource.getValue("colors[value ne \"green\"]").isArray());
assert(scimResource.getValue("colors[value ne \"green\"]").size() == 2);
// The following assertion uses Guava
assert(Iterables.all(
scimResource.getValue("colors[value ne \"green\"]"),
new Predicate<JsonNode>()
{
public boolean apply(JsonNode element)
{
return !element.textValue().equals("green");
}
}
));
The methods GenericScimResource.getValue(String)
and GenericScimResource.getValue(Path)
have a behavior that is convenient but which may take you by surprise: They return exactly one JsonNode
for the given path, the first matching node. That might be a TextNode
, a BooleanNode
, an ArrayNode
, and so on. It might even be a NullNode
.
This can trip you up if you don't consider the kind of JsonNode
that will be targeted by your path.
For example, consider the path addresses[city ne "Austin"]
.
System.out.println(scimResource.getValue("addresses[city ne \"Austin\"]"));
The output of this code is:
[{"street":"234 2nd Street","city":"Round Rock","state":"TX"},{"street":"345 3rd Street","city":"Cedar Park","state":"TX"}]
But what about the path addresses[city ne "Austin"].city
? Notice the sub-attribute.
System.out.println(scimResource.getValue("addresses[city ne \"Austin\"].city"));
The output of this code is quite different:
"Round Rock"
Why the difference? The first path targets an entire multivalued attribute, so an ArrayNode
is returned. The second path targets every "city" sub-attribute of that multivalued attribute, but getValue(…)
always returns exactly one JsonNode
. So it returns a single TextNode
.
This is consistent behavior, but it may not be what you want. What if you specifically need to get back all matching nodes, not just the first matching node? In that case, you can call JsonUtils.findMatchingPaths(…)
, which is intended to handle precisely this case.
List<JsonNode> notAustin =
JsonUtils.findMatchingPaths(
Path.fromString("addresses[city ne \"Austin\"].city"),
scimResource.getObjectNode());
assert(notAustin() == 2);
for (JsonNode city : notAustin)
{
System.out.println(city);
}
The output of this code is:
"Round Rock"
"Cedar Park"