Skip to content

Working with SCIM Paths

Jacob Childress edited this page Jul 6, 2016 · 5 revisions

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.

Examples

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 the TextNode in the document with the value of "Bill".
assert(scimResource.getValue("firstName").textValue().equals("Bill"));
  • A path of shoeSize would match the IntNode 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());

Sub-attributes

A path may have a sub-attribute component, which specifies a sub-attribute of a complex object.

For example:

  • phoneNumber.areacode would match a TextNode containing "512".
assert(scimResource.getValue("phoneNumber.areacode").textValue().equals("512"));

Such a path could match multiple nodes:

  • address.city would match all of the TextNodes in the addresses list that have a sub-attribute named "city": TextNode "Austin", TextNode "Cedar Park", and TextNode "Round Rock".
List<JsonNode> addressesWithCities =
    JsonUtils.findMatchingPaths(Path.fromString("addresses.city"),
                                scimResource.getObjectNode());
assert(addressesWithCities.size() == 3);

Value filters

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 two TextNodes: TextNode "Round Rock" and TextNode "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.

The value sub-attribute

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");
      }
    }
));

Selecting multiple nodes: ArrayNode vs. List<JsonNode>

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"