Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
493 changes: 493 additions & 0 deletions PRDs/20251215-path-formatters/plan.md

Large diffs are not rendered by default.

Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,32 @@ public interface IPathFormatter {
@NonNull
IPathFormatter METAPATH_PATH_FORMATER = new MetapathFormatter();

/**
* A path formatter that produces XPath 3.1 paths with EQName-qualified names.
* <p>
* This formatter generates namespace-qualified paths using the EQName format
* (e.g., {@code Q{http://example.com}element[1]}), suitable for use with XML
* tooling that requires namespace qualification.
*
* @see XPathFormatter
*/
@NonNull
IPathFormatter XPATH_PATH_FORMATTER = new XPathFormatter();

/**
* A path formatter that produces RFC 6901 JSON Pointer paths.
* <p>
* This formatter generates JSON Pointer paths suitable for use with JSON
* tooling and JSON-based error reporting. Uses JSON property names, 0-based
* array indices, and proper RFC 6901 escaping.
*
* @see JsonPointerFormatter
* @see <a href="https://www.rfc-editor.org/rfc/rfc6901">RFC 6901 - JSON
* Pointer</a>
*/
@NonNull
IPathFormatter JSON_POINTER_PATH_FORMATTER = new JsonPointerFormatter();

/**
* Format the path represented by the provided path segment. The provided
* segment is expected to be the last node in this path. A call to
Expand Down

This file was deleted.

Original file line number Diff line number Diff line change
@@ -0,0 +1,202 @@
/*
* SPDX-FileCopyrightText: none
* SPDX-License-Identifier: CC0-1.0
*/

package gov.nist.secauto.metaschema.core.metapath.format;

import gov.nist.secauto.metaschema.core.metapath.item.node.IAssemblyInstanceGroupedNodeItem;
import gov.nist.secauto.metaschema.core.metapath.item.node.IAssemblyNodeItem;
import gov.nist.secauto.metaschema.core.metapath.item.node.IDocumentNodeItem;
import gov.nist.secauto.metaschema.core.metapath.item.node.IFieldNodeItem;
import gov.nist.secauto.metaschema.core.metapath.item.node.IFlagNodeItem;
import gov.nist.secauto.metaschema.core.metapath.item.node.IModelNodeItem;
import gov.nist.secauto.metaschema.core.metapath.item.node.IModuleNodeItem;
import gov.nist.secauto.metaschema.core.metapath.item.node.IRootAssemblyNodeItem;
import gov.nist.secauto.metaschema.core.model.IFlagInstance;
import gov.nist.secauto.metaschema.core.model.INamedModelInstance;
import gov.nist.secauto.metaschema.core.model.JsonGroupAsBehavior;
import gov.nist.secauto.metaschema.core.qname.IEnhancedQName;
import gov.nist.secauto.metaschema.core.util.ObjectUtils;

import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;

import java.util.List;

import edu.umd.cs.findbugs.annotations.NonNull;
import edu.umd.cs.findbugs.annotations.Nullable;

/**
* An {@link IPathFormatter} that produces RFC 6901 compliant JSON Pointer
* paths.
* <p>
* This formatter produces paths suitable for use with JSON tooling and
* JSON-based error reporting. The format follows the JSON Pointer specification
* (RFC 6901).
* <p>
* Example output: {@code /catalog/controls/0/id}
* <p>
* Key characteristics:
* <ul>
* <li>Uses JSON property names (not XML element names)</li>
* <li>Uses 0-based array indices for LIST grouping</li>
* <li>Uses key values for KEYED grouping</li>
* <li>Handles SINGLETON_OR_LIST by checking sibling count</li>
* <li>Escapes special characters per RFC 6901 (~ as ~0, / as ~1)</li>
* <li>No @ prefix for flags (unlike XPath)</li>
* </ul>
*
* @see <a href="https://www.rfc-editor.org/rfc/rfc6901">RFC 6901 - JSON
* Pointer</a>
*/
public class JsonPointerFormatter implements IPathFormatter {
private static final Logger LOGGER = LogManager.getLogger(JsonPointerFormatter.class);

@Override
@NonNull
public String formatMetaschema(IModuleNodeItem metaschema) {
// Returns empty string to produce leading "/" via join in format method
return "";
}

@Override
@NonNull
public String formatDocument(IDocumentNodeItem document) {
// Returns empty string to produce leading "/" via join in format method
return "";
}

@Override
@NonNull
public String formatRootAssembly(IRootAssemblyNodeItem root) {
String jsonName = root.getDefinition().getJsonName();
return escapeJsonPointer(jsonName);
}

@Override
@NonNull
public String formatAssembly(IAssemblyNodeItem assembly) {
return formatModelItem(assembly);
}

@Override
@NonNull
public String formatAssembly(IAssemblyInstanceGroupedNodeItem assembly) {
return formatModelItem(assembly);
}

@Override
@NonNull
public String formatField(IFieldNodeItem field) {
return formatModelItem(field);
}

@Override
@NonNull
public String formatFlag(IFlagNodeItem flag) {
// JSON Pointer does not use @ prefix for attributes
return escapeJsonPointer(flag.getQName().getLocalName());
}
Comment thread
david-waltermire marked this conversation as resolved.

/**
* Format a model node item (assembly or field) based on its JSON grouping
* behavior.
*
* @param item
* the model node item to format
* @return the formatted path segment
*/
@NonNull
private String formatModelItem(@NonNull IModelNodeItem<?, ?> item) {
INamedModelInstance instance = item.getInstance();
if (instance == null) {
// No instance - use local name only
return escapeJsonPointer(item.getQName().getLocalName());
}

String jsonName = escapeJsonPointer(instance.getJsonName());
JsonGroupAsBehavior behavior = instance.getJsonGroupAsBehavior();

switch (behavior) {
case KEYED:
String keyValue = getJsonKeyValue(item, instance);
return jsonName + "/" + escapeJsonPointer(keyValue);
case LIST:
// 0-based index
return jsonName + "/" + (item.getPosition() - 1);
case SINGLETON_OR_LIST:
int siblingCount = countSiblings(item);
if (siblingCount > 1) {
// Multiple siblings - use array notation
return jsonName + "/" + (item.getPosition() - 1);
}
// Single sibling - no index
return jsonName;
case NONE:
default:
return jsonName;
}
}

/**
* Get the JSON key value for a KEYED collection item.
*
* @param item
* the model node item
* @param instance
* the model instance
* @return the key value, or falls back to 0-based index if not available
*/
@NonNull
private static String getJsonKeyValue(
@NonNull IModelNodeItem<?, ?> item,
@NonNull INamedModelInstance instance) {
IFlagInstance keyFlag = instance.getEffectiveJsonKey();
if (keyFlag != null) {
IEnhancedQName keyFlagQName = keyFlag.getQName();
IFlagNodeItem flagItem = item.getFlagByName(keyFlagQName);
if (flagItem != null) {
return flagItem.toAtomicItem().asString();
}
}
// Fallback to 0-based index - this indicates a potential issue with the model
// or data
if (LOGGER.isWarnEnabled()) {
LOGGER.warn("Unable to resolve JSON key for KEYED collection item '{}', falling back to numeric index",
item.getQName().getLocalName());
}
return String.valueOf(item.getPosition() - 1);
}

/**
* Count the number of siblings with the same name as the given item.
*
* @param item
* the model node item
* @return the sibling count (including the item itself)
*/
private static int countSiblings(@NonNull IModelNodeItem<?, ?> item) {
IAssemblyNodeItem parent = item.getParentContentNodeItem();
if (parent == null) {
return 1;
}
List<? extends IModelNodeItem<?, ?>> siblings = parent.getModelItemsByName(item.getQName());
return siblings.size();
}

/**
* Escape a string value according to RFC 6901.
* <p>
* The order of escaping is important: ~ must be escaped first, then /.
*
* @param value
* the value to escape
* @return the escaped value
*/
@NonNull
private static String escapeJsonPointer(@NonNull String value) {
// Order matters: escape ~ first, then /
return ObjectUtils.notNull(value.replace("~", "~0").replace("/", "~1"));
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
/*
* SPDX-FileCopyrightText: none
* SPDX-License-Identifier: CC0-1.0
*/

package gov.nist.secauto.metaschema.core.metapath.format;

/**
* Enumeration of path format selection options for validation output.
* <p>
* This enum allows users to control how node paths are formatted in validation
* messages and other output. The selection can be explicit (forcing a specific
* format) or automatic (selecting based on document format).
*
* @see IPathFormatter#resolveFormatter(PathFormatSelection,
* gov.nist.secauto.metaschema.databind.io.Format)
*/
public enum PathFormatSelection {
/**
* Auto-select the path format based on document format.
* <p>
* When this option is selected:
* <ul>
* <li>JSON documents use JSON Pointer format (RFC 6901)</li>
* <li>YAML documents use JSON Pointer format (RFC 6901)</li>
* <li>XML documents use XPath 3.1 EQName format</li>
* </ul>
* <p>
* This is the default selection when no explicit format is specified.
*/
AUTO,

/**
* Always use Metapath format regardless of document type.
* <p>
* Produces paths like: {@code /root/assembly[1]/field[1]/@flag}
*/
METAPATH,

/**
* Always use XPath 3.1 EQName format regardless of document type.
* <p>
* Produces namespace-qualified paths like:
* {@code /Q{http://example.com}root/Q{http://example.com}assembly[1]}
*/
XPATH,

/**
* Always use RFC 6901 JSON Pointer format regardless of document type.
* <p>
* Produces paths like: {@code /root/assemblies/0/id}
*/
JSON_POINTER
}
Loading
Loading