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
Original file line number Diff line number Diff line change
Expand Up @@ -411,6 +411,27 @@ default IBoundLoader newBoundLoader() {
return new DefaultBoundLoader(this);
}

/**
* Get a new {@link IBoundLoader} instance configured for permissive loading.
* <p>
* This loader has
* {@link DeserializationFeature#DESERIALIZE_VALIDATE_REQUIRED_FIELDS} disabled,
* making it suitable for use with Metapath functions like {@code fn:doc()}
* where documents may be incomplete or under construction.
* <p>
* Use this method when setting up a {@link DynamicContext} for Metapath
* evaluation to ensure that referenced documents can be loaded without strict
* required field validation.
*
* @return a permissive loader instance
*/
@NonNull
default IBoundLoader newPermissiveBoundLoader() {
IBoundLoader loader = newBoundLoader();
loader.disableFeature(DeserializationFeature.DESERIALIZE_VALIDATE_REQUIRED_FIELDS);
return loader;
}

/**
* Create a deep copy of the provided bound object.
*
Expand Down Expand Up @@ -445,10 +466,10 @@ <CLASS extends IBoundObject> CLASS deepCopy(@NonNull CLASS other, IBoundObject p
default IConstraintValidator newValidator(
@NonNull IConstraintValidationHandler handler,
@Nullable IConfiguration<ValidationFeature<?>> config) {
IBoundLoader loader = newBoundLoader();
// Use permissive loader for referenced documents
IBoundLoader loader = newPermissiveBoundLoader();
// Also disable constraint validation for referenced documents
loader.disableFeature(DeserializationFeature.DESERIALIZE_VALIDATE_CONSTRAINTS);
// Disable required field validation since schema validation handles this
loader.disableFeature(DeserializationFeature.DESERIALIZE_VALIDATE_REQUIRED_FIELDS);

DynamicContext context = new DynamicContext();
context.setDocumentLoader(loader);
Expand Down Expand Up @@ -570,10 +591,10 @@ default IValidationResult validateWithConstraints(
@NonNull URI target,
@Nullable IConfiguration<ValidationFeature<?>> config)
throws IOException, ConstraintValidationException {
IBoundLoader loader = newBoundLoader();
// Use permissive loader for the target document and any referenced documents
IBoundLoader loader = newPermissiveBoundLoader();
// Also disable constraint validation during loading
loader.disableFeature(DeserializationFeature.DESERIALIZE_VALIDATE_CONSTRAINTS);
// Disable required field validation since schema validation handles this
loader.disableFeature(DeserializationFeature.DESERIALIZE_VALIDATE_REQUIRED_FIELDS);
IDocumentNodeItem nodeItem = loader.loadAsNodeItem(target);

return validate(nodeItem, loader, config);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -147,7 +147,8 @@ private void validate(@NonNull INodeItem nodeItem) throws ConstraintValidationEx
}

DynamicContext dynamicContext = new DynamicContext(nodeItem.getStaticContext());
dynamicContext.setDocumentLoader(getBindingContext().newBoundLoader());
// Use permissive loader for documents referenced in constraint expressions
dynamicContext.setDocumentLoader(getBindingContext().newPermissiveBoundLoader());
DefaultConstraintValidator validator = new DefaultConstraintValidator(getConstraintValidationHandler());
validator.validate(definitionNodeItem, dynamicContext);
validator.finalizeValidation(dynamicContext);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -211,17 +211,17 @@ private String formatMissingPropertiesMessage(
IBoundProperty<?> missing = !missingFlags.isEmpty() ? missingFlags.get(0)
: !missingFields.isEmpty() ? missingFields.get(0)
: missingAssemblies.get(0);
String type = getPropertyTypeName(missing, format);
String type = getPropertyTypeName(missing, format, false);
String name = getInstanceName(missing, context);
message.append(String.format("Missing required %s '%s' in '%s'", type, name, parentName));
} else if (hasSingleType(missingFlags, missingFields, missingAssemblies)) {
// Multiple properties of single type
List<IBoundProperty<?>> list = !missingFlags.isEmpty() ? missingFlags
: !missingFields.isEmpty() ? missingFields
: missingAssemblies;
String type = getPropertyTypeName(list.get(0), format);
String type = getPropertyTypeName(list.get(0), format, true);
String names = formatNameList(list, context);
message.append(String.format("Missing required %ss in '%s': %s", type, parentName, names));
message.append(String.format("Missing required %s in '%s': %s", type, parentName, names));
} else {
// Multiple properties of different types
message.append(String.format("Missing required properties in '%s':", parentName));
Expand Down Expand Up @@ -268,26 +268,33 @@ private String formatMissingPropertiesMessage(
* {@code true} if the property is a flag instance
* @param format
* the format being parsed
* @param plural
* {@code true} for plural form, {@code false} for singular
* @return the user-friendly type name
*/
@NonNull
private static String getFormatPropertyTypeName(boolean isFlag, @NonNull Format format) {
private static String getFormatPropertyTypeName(boolean isFlag, @NonNull Format format, boolean plural) {
switch (format) {
case XML:
return isFlag ? "attribute" : "element";
if (isFlag) {
return plural ? "attributes" : "attribute";
}
return plural ? "elements" : "element";
case JSON:
case YAML:
return "property";
return plural ? "properties" : "property";
default:
// Fallback for any future formats - use generic "property"
return "property";
return plural ? "properties" : "property";
}
}

/**
* Get the user-friendly label for a group of properties in the given format.
* <p>
* Used when listing multiple properties of the same type in error messages.
* Delegates to {@link #getFormatPropertyTypeName(boolean, Format, boolean)} and
* capitalizes the result.
*
* @param isFlags
* {@code true} if listing flag instances
Expand All @@ -297,16 +304,8 @@ private static String getFormatPropertyTypeName(boolean isFlag, @NonNull Format
*/
@NonNull
private static String getFormatPropertyGroupLabel(boolean isFlags, @NonNull Format format) {
switch (format) {
case XML:
return isFlags ? "Attributes" : "Elements";
case JSON:
case YAML:
return "Properties";
default:
// Fallback for any future formats
return "Properties";
}
String typeName = getFormatPropertyTypeName(isFlags, format, true);
return Character.toUpperCase(typeName.charAt(0)) + typeName.substring(1);
}

/**
Expand All @@ -332,19 +331,24 @@ private static boolean hasSingleType(
/**
* Get the property type name for error messages in format-appropriate terms.
* <p>
* Delegates to {@link #getFormatPropertyTypeName(boolean, Format)} based on
* whether the instance is a flag.
* Delegates to {@link #getFormatPropertyTypeName(boolean, Format, boolean)}
* based on whether the instance is a flag.
*
* @param instance
* the property instance
* @param format
* the format being parsed
* @param plural
* {@code true} for plural form, {@code false} for singular
* @return the user-friendly type name appropriate for the format
*/
@NonNull
private static String getPropertyTypeName(@NonNull IBoundProperty<?> instance, @NonNull Format format) {
private static String getPropertyTypeName(
@NonNull IBoundProperty<?> instance,
@NonNull Format format,
boolean plural) {
boolean isFlag = instance instanceof IFlagInstance;
return getFormatPropertyTypeName(isFlag, format);
return getFormatPropertyTypeName(isFlag, format, plural);
}

/**
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -460,4 +460,89 @@ void testNullFieldValueDoesNotThrowNpe() throws IOException {
});
}
}

/**
* Tests for permissive document loading via newPermissiveBoundLoader.
* <p>
* These tests verify that permissive loaders skip required field validation,
* which is useful for the fn:doc() function when loading incomplete documents.
*/
@Nested
class PermissiveLoadingTest {

@Test
void testPermissiveLoaderSkipsRequiredFieldValidation() throws IOException {
// Document missing all required fields
String xml = "<root xmlns='http://csrc.nist.gov/ns/metaschema/testing/validation-errors'>"
+ "</root>";

IBindingContext bindingContext = newBindingContext();
// Create deserializer and apply permissive configuration
IDeserializer<?> deserializer = bindingContext.newDeserializer(Format.XML, rootClass);
deserializer.disableFeature(DeserializationFeature.DESERIALIZE_VALIDATE_REQUIRED_FIELDS);

// Should not throw - permissive loading skips required field validation
assertDoesNotThrow(() -> {
deserializer.deserialize(new StringReader(xml), URI.create("test://incomplete.xml"));
});
}

@Test
void testStrictLoaderValidatesRequiredFields() throws IOException {
// Document missing all required fields
String xml = "<root xmlns='http://csrc.nist.gov/ns/metaschema/testing/validation-errors'>"
+ "</root>";

IBindingContext bindingContext = newBindingContext();
// Use deserializer with explicit strict validation enabled
IDeserializer<?> deserializer = bindingContext.newDeserializer(Format.XML, rootClass);
deserializer.enableFeature(DeserializationFeature.DESERIALIZE_VALIDATE_REQUIRED_FIELDS);

// Should throw because required fields are missing
assertThrows(IOException.class, () -> {
deserializer.deserialize(new StringReader(xml), URI.create("test://incomplete.xml"));
});
}

@Test
void testPermissiveLoaderAllowsPartialDocuments() throws IOException {
// Document with some fields but not all required ones
String xml = "<root xmlns='http://csrc.nist.gov/ns/metaschema/testing/validation-errors'>"
+ "<optional-field>some value</optional-field>"
+ "</root>";

IBindingContext bindingContext = newBindingContext();
IDeserializer<?> deserializer = bindingContext.newDeserializer(Format.XML, rootClass);
deserializer.disableFeature(DeserializationFeature.DESERIALIZE_VALIDATE_REQUIRED_FIELDS);

// Should load successfully despite missing required-flag, required-field, and
// required-assembly
Object result = assertDoesNotThrow(
() -> deserializer.deserialize(new StringReader(xml), URI.create("test://partial.xml")));

assertNotNull(result, "Permissive loading should return a result even for partial documents");
}

@Test
void testNewPermissiveBoundLoaderHasValidationDisabled() throws IOException {
IBindingContext bindingContext = newBindingContext();
IBoundLoader permissiveLoader = bindingContext.newPermissiveBoundLoader();

// Verify that the permissive loader has DESERIALIZE_VALIDATE_REQUIRED_FIELDS
// disabled
assertFalse(permissiveLoader.isFeatureEnabled(DeserializationFeature.DESERIALIZE_VALIDATE_REQUIRED_FIELDS),
"Permissive loader should have DESERIALIZE_VALIDATE_REQUIRED_FIELDS disabled");
}

@Test
void testNewBoundLoaderHasValidationEnabledByDefault() throws IOException {
IBindingContext bindingContext = newBindingContext();
IBoundLoader regularLoader = bindingContext.newBoundLoader();

// Verify that the regular loader has DESERIALIZE_VALIDATE_REQUIRED_FIELDS
// enabled by default
assertTrue(regularLoader.isFeatureEnabled(DeserializationFeature.DESERIALIZE_VALIDATE_REQUIRED_FIELDS),
"Regular loader should have DESERIALIZE_VALIDATE_REQUIRED_FIELDS enabled by default");
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -127,7 +127,8 @@ public void execute() throws CommandExecutionException {

try {
IBindingContext bindingContext = getBindingContext();
IBoundLoader loader = bindingContext.newBoundLoader();
// Use permissive loader since convert is not a validation command
IBoundLoader loader = bindingContext.newPermissiveBoundLoader();
if (LOGGER.isInfoEnabled()) {
LOGGER.info("Converting '{}'.", source);
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -126,9 +126,13 @@ private void executeCommand(

IModule module = null;
INodeItem item = null;
IBoundLoader loader = null;
if (cmdLine.hasOption(MetaschemaCommands.METASCHEMA_OPTIONAL_OPTION)) {
IBindingContext bindingContext = MetaschemaCommands.newBindingContextWithDynamicCompilation();

// Use permissive loader since eval is not a validation command
loader = bindingContext.newPermissiveBoundLoader();

try {
module = bindingContext.registerModule(MetaschemaCommands.loadModule(
cmdLine,
Expand All @@ -142,9 +146,6 @@ private void executeCommand(
// determine if the query is evaluated against the module or the instance
if (cmdLine.hasOption(CONTENT_OPTION)) {
// load the content

IBoundLoader loader = bindingContext.newBoundLoader();

String contentLocation = ObjectUtils.requireNonNull(cmdLine.getOptionValue(CONTENT_OPTION));
URI contentResource;
try {
Expand Down Expand Up @@ -196,10 +197,16 @@ private void executeCommand(
String.format("Must use '%s' to specify the Metapath expression.", EXPRESSION_OPTION.getArgName()));
}

// Setup dynamic context with document loader for doc() function support
DynamicContext dynamicContext = new DynamicContext(staticContext);
if (loader != null) {
dynamicContext.setDocumentLoader(loader);
}

try {
// Parse and compile the Metapath expression
ISequence<?> sequence = IMetapathExpression.compile(expression, staticContext)
.evaluate(item, new DynamicContext(staticContext));
.evaluate(item, dynamicContext);

// handle the metapath results
try (Writer stringWriter = new StringWriter()) {
Expand Down
Loading