Fork me on GitHub

Executing Metapath

This guide explains how to use Metapath expressions to query OSCAL documents programmatically.

Metapath is an expression language for querying Metaschema-based documents. It's similar to XPath but works across all formats (XML, JSON, YAML).

import dev.metaschema.oscal.lib.OscalBindingContext;
import dev.metaschema.oscal.lib.model.Catalog;
import dev.metaschema.core.metapath.MetapathExpression;
import dev.metaschema.core.metapath.IMetapathExpression;
import dev.metaschema.core.metapath.item.IItem;
import dev.metaschema.core.metapath.item.ISequence;
import dev.metaschema.databind.io.Format;
import dev.metaschema.databind.io.IDeserializer;
import java.nio.file.Path;

OscalBindingContext context = OscalBindingContext.instance();

// Load a catalog
IDeserializer<Catalog> deserializer = context.newDeserializer(
    Format.JSON, Catalog.class);
Catalog catalog = deserializer.deserialize(Path.of("catalog.json"));

// Compile an expression
IMetapathExpression expression = MetapathExpression.compile(
    "//control", context.getStaticContext());

// Evaluate against the catalog
ISequence<?> results = expression.evaluate(catalog);

// Process results
for (IItem item : results) {
    System.out.println("Found: " + item.toAtomicItem().asString());
}
IMetapathExpression expr = MetapathExpression.compile(
    "//control", context.getStaticContext());
ISequence<?> controls = expr.evaluate(catalog);
IMetapathExpression expr = MetapathExpression.compile(
    "//control[@id='ac-1']", context.getStaticContext());
ISequence<?> result = expr.evaluate(catalog);
IMetapathExpression expr = MetapathExpression.compile(
    "//control/title", context.getStaticContext());
ISequence<?> titles = expr.evaluate(catalog);

titles.forEach(title ->
    System.out.println(title.toAtomicItem().asString()));
IMetapathExpression expr = MetapathExpression.compile(
    "count(//control)", context.getStaticContext());
ISequence<?> result = expr.evaluate(catalog);

int count = result.getFirstItem(true)
    .toAtomicItem()
    .asInteger()
    .intValue();
System.out.println("Control count: " + count);
// Find controls with status=withdrawn
IMetapathExpression expr = MetapathExpression.compile(
    "//control[prop[@name='status'][@value='withdrawn']]",
    context.getStaticContext());
import dev.metaschema.databind.model.IBoundObject;
import dev.metaschema.oscal.lib.model.Control;

ISequence<?> results = expression.evaluate(catalog);

for (IItem item : results) {
    if (item instanceof IBoundObject) {
        Object value = ((IBoundObject) item).getValue();
        if (value instanceof Control) {
            Control control = (Control) value;
            System.out.println("Control ID: " + control.getId());
            System.out.println("Title: " + control.getTitle());
        }
    }
}
IMetapathExpression expr = MetapathExpression.compile(
    "//control/@id", context.getStaticContext());
ISequence<?> ids = expr.evaluate(catalog);

ids.forEach(item -> {
    String id = item.toAtomicItem().asString();
    System.out.println("ID: " + id);
});

liboscal-java provides OSCAL-specific Metapath functions:

Check if a property has an OSCAL namespace:

IMetapathExpression expr = MetapathExpression.compile(
    "//prop[has-oscal-namespace(., 'https://fedramp.gov/ns/oscal')]",
    context.getStaticContext());

Resolve a profile import (within Metapath):

IMetapathExpression expr = MetapathExpression.compile(
    "resolve-profile(import)",
    context.getStaticContext());
// All control groups
"//group"

// Controls in a specific group
"//group[@id='access-control']/control"

// Control enhancements
"//control[contains(@id, '.')]"

// All parameters
"//param"
// All imports
"//import"

// Included controls
"//import/include-controls/with-id"

// Parameter modifications
"//modify/set-parameter"
// System name
"/system-security-plan/system-characteristics/system-name"

// Implemented requirements
"//implemented-requirement"

// Components
"//component"
import dev.metaschema.core.metapath.MetapathException;

try {
    IMetapathExpression expr = MetapathExpression.compile(
        "//control[invalid syntax", context.getStaticContext());
} catch (MetapathException e) {
    System.err.println("Invalid expression: " + e.getMessage());
}

For repeated queries, cache compiled expressions:

import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;

public class MetapathQueryCache {
    private final Map<String, IMetapathExpression> cache =
        new ConcurrentHashMap<>();
    private final StaticContext staticContext;

    public MetapathQueryCache(OscalBindingContext context) {
        this.staticContext = context.getStaticContext();
    }

    public IMetapathExpression getExpression(String path) {
        return cache.computeIfAbsent(path, p ->
            MetapathExpression.compile(p, staticContext));
    }

    public ISequence<?> query(Object document, String path) {
        return getExpression(path).evaluate(document);
    }
}
Expression Description
/catalog Root catalog
//control All controls at any depth
control[@id='ac-1'] Control with specific ID
control[1] First control
control[last()] Last control
control/title Titles of direct child controls
.. Parent element
. Current element
Function Description
count(seq) Count items
string-length(str) String length
starts-with(str, prefix) Check prefix
contains(str, substr) Check substring
not(expr) Logical negation
exists(seq) Check if non-empty
  1. Compile once, use many - Cache compiled expressions
  2. Be specific - Narrow paths are faster than //
  3. Handle empty results - Check sequence length before accessing
  4. Use predicates - Filter in the expression, not in Java
  5. Test expressions - Validate with CLI before embedding

Continue learning about liboscal-java with these related guides: