All Downloads are FREE. Search and download functionalities are using the official Maven repository.

dev.harrel.jsonschema.FormatEvaluatorFactory Maven / Gradle / Ivy

The newest version!
package dev.harrel.jsonschema;

import com.sanctionco.jmail.JMail;
import com.sanctionco.jmail.net.InternetProtocolAddress;

import java.net.URI;
import java.time.format.DateTimeFormatter;
import java.util.*;
import java.util.function.Consumer;
import java.util.function.Predicate;
import java.util.function.UnaryOperator;
import java.util.regex.Pattern;

import static java.util.Collections.unmodifiableSet;

/**
 * {@code EvaluatorFactory} implementation that provides format validation capabilities.
 * It should not be used as a standalone factory.
 * It is intended to be used as a supplementary factory to a full dialect-compatible factory (like {@link Draft2020EvaluatorFactory}).
 * 
 *     new ValidatorFactory().withEvaluatorFactory(new FormatEvaluatorFactory());
 * 
* May also be used in conjunction with custom factories: *
 *     new ValidatorFactory().withEvaluatorFactory(EvaluatorFactory.compose(customFactory, new FormatEvaluatorFactory()));
 * 
*

* It may not be fully compatible with JSON Schema specification. It is mostly based on tools already present in JDK itself. * Supported formats: *

    *
  • * date, date-time, time - uses {@link DateTimeFormatter} with standard ISO formatters, *
  • *
  • * duration - regex based validation as it may be combination of {@link java.time.Duration} and {@link java.time.Period}, *
  • *
  • * email, idn-email - uses {@link JMail#isValid(String)}, *
  • *
  • * hostname - regex based validation, *
  • *
  • * idn-hostname - not supported - performs same validation as hostname, *
  • *
  • * ipv4, ipv6 - uses {@link InternetProtocolAddress}, *
  • *
  • * uri, uri-reference, iri, iri-reference - uses {@link URI}, *
  • *
  • * uuid - uses {@link UUID}, *
  • *
  • * uri-template - lenient checking of unclosed braces (should be compatible with Spring's implementation), *
  • *
  • * json-pointer, relative-json-pointer - manual validation, *
  • *
  • * regex - uses {@link Pattern}. *
  • *
* * @implNote Default constructor provides instance without vocabulary support. This means the validation will * always occur regardless of currently active vocabularies (determined based on meta-schema). * If more specification compliant instance is needed, please explicitly provide vocabularies to the constructor: *
 *     new FormatEvaluatorFactory(Set.of(Vocabulary.Draft2020.FORMAT_ASSERTION, Vocabulary.Draft2019.FORMAT));
 * 
* Then the validation will only be run if at least one of provided vocabularies is active. */ @SuppressWarnings("ResultOfMethodCallIgnored") public final class FormatEvaluatorFactory implements EvaluatorFactory { private static final class FormatEvaluator implements Evaluator { private final UnaryOperator operator; private FormatEvaluator(UnaryOperator operator) { this.operator = operator; } @Override public Result evaluate(EvaluationContext ctx, JsonNode node) { if (!node.isString()) { return Result.success(); } String value = node.asString(); String err = operator.apply(value); return err == null ? Result.success(value) : Result.failure(err); } } private static final Pattern DURATION_PATTERN = Pattern.compile( "P(?:\\d+W|T(?:\\d+H(?:\\d+M(?:\\d+S)?)?|\\d+M(?:\\d+S)?|\\d+S)|(?:\\d+D|\\d+M(?:\\d+D)?|\\d+Y(?:\\d+M(?:\\d+D)?)?)(?:T(?:\\d+H(?:\\d+M(?:\\d+S)?)?|\\d+M(?:\\d+S)?|\\d+S))?)", Pattern.CASE_INSENSITIVE ); private static final Pattern HOSTNAME_PATTERN = Pattern.compile( "([a-zA-Z0-9]|[a-zA-Z0-9][a-zA-Z0-9\\-]{0,61}[a-zA-Z0-9])(\\.([a-zA-Z0-9]|[a-zA-Z0-9][a-zA-Z0-9\\-]{0,61}[a-zA-Z0-9]))*", Pattern.CASE_INSENSITIVE ); private final Predicate vocabPredicate; /** * Creates a default instance without vocabularies support. */ public FormatEvaluatorFactory() { this.vocabPredicate = ctx -> true; } /** * Creates a customized instance with vocabularies support. * Validation will only be run when at least one of provided vocabularies is active during validation process. */ public FormatEvaluatorFactory(Set vocabularies) { Set vocabsCopy = unmodifiableSet(new HashSet<>(vocabularies)); this.vocabPredicate = ctx -> !Collections.disjoint(vocabsCopy, ctx.getMetaValidationData().activeVocabularies); } @Override public Optional create(SchemaParsingContext ctx, String fieldName, JsonNode fieldNode) { if (!"format".equals(fieldName) || !fieldNode.isString() || !vocabPredicate.test(ctx)) { return Optional.empty(); } return Optional.ofNullable(getOperator(fieldNode.asString())) .map(FormatEvaluator::new); } private UnaryOperator getOperator(String format) { switch (format) { case "date": return tryOf(DateTimeFormatter.ISO_DATE::parse); case "date-time": return tryOf(DateTimeFormatter.ISO_DATE_TIME::parse); case "time": return tryOf(DateTimeFormatter.ISO_TIME::parse); case "duration": return v -> DURATION_PATTERN.matcher(v).matches() ? null : String.format("\"%s\" is not a valid duration string", v); case "email": case "idn-email": return v -> JMail.isValid(v) ? null : String.format("\"%s\" is not a valid email address", v); case "hostname": case "idn-hostname": return v -> HOSTNAME_PATTERN.matcher(v).matches() ? null : String.format("\"%s\" is not a valid hostname", v); case "ipv4": return v -> InternetProtocolAddress.validateIpv4(v).isPresent() ? null : String.format("\"%s\" is not a valid IPv4 address", v); case "ipv6": return v -> InternetProtocolAddress.validateIpv6(v).isPresent() ? null : String.format("\"%s\" is not a valid IPv6 address", v); case "uri": case "iri": return FormatEvaluatorFactory::uriOperator; case "uri-reference": case "iri-reference": return tryOf(URI::create); case "uuid": return FormatEvaluatorFactory::uuidOperator; case "uri-template": return FormatEvaluatorFactory::uriTemplateOperator; case "json-pointer": return v -> validateJsonPointer(v) ? null : String.format("\"%s\" is not a valid json-pointer", v); case "relative-json-pointer": return FormatEvaluatorFactory::rjpOperator; case "regex": return tryOf(Pattern::compile); default: return null; } } private static UnaryOperator tryOf(Consumer op) { return v -> { try { op.accept(v); return null; } catch (Exception e) { return e.getMessage(); } }; } private static boolean validateJsonPointer(String pointer) { if (pointer.isEmpty()) { return true; } if (!pointer.startsWith("/")) { return false; } String decoded = pointer.replace("~0", "").replace("~1", ""); return !decoded.contains("~"); } private static String uriOperator(String value) { try { URI uri = URI.create(value); return uri.isAbsolute() ? null : String.format("\"%s\" is a relative URI", uri); } catch (Exception e) { return e.getMessage(); } } private static String uuidOperator(String value) { try { UUID.fromString(value); return value.length() == 36 ? null : String.format("\"%s\" UUID has invalid length", value); } catch (Exception e) { return e.getMessage(); } } private static String uriTemplateOperator(String value) { int level = 0; for (int i = 0; i < value.length(); i++) { char c = value.charAt(i); if (c == '{') { level++; } else if (c == '}' && level > 0) { level--; } } return level == 0 ? null : String.format("\"%s\" is not a valid URI template", value); } private static String rjpOperator(String value) { int firstSegmentEndIdx = value.length(); for (int i = 0; i < value.length(); i++) { char c = value.charAt(i); if (c == '#' || c == '/') { firstSegmentEndIdx = i; break; } if (c < '0' || c > '9') { return invalidRjpMessage(value); } } String firstSegment = value.substring(0, firstSegmentEndIdx); String secondSegment = value.substring(firstSegmentEndIdx); if (firstSegment.isEmpty() || firstSegment.length() > 1 && firstSegment.startsWith("0") || // no leading zeros !"#".equals(secondSegment) && !validateJsonPointer(secondSegment)) { return invalidRjpMessage(value); } return null; } private static String invalidRjpMessage(String value) { return String.format("\"%s\" is not a valid relative-json-pointer", value); } }




© 2015 - 2024 Weber Informatics LLC | Privacy Policy