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

com.github.fge.jsonschema.examples.Example9 Maven / Gradle / Ivy

There is a newer version: 2.2.6
Show newest version
/*
 * Copyright (c) 2014, Francis Galiegue ([email protected])
 *
 * This software is dual-licensed under:
 *
 * - the Lesser General Public License (LGPL) version 3.0 or, at your option, any
 *   later version;
 * - the Apache Software License (ASL) version 2.0.
 *
 * The text of both licenses is available under the src/resources/ directory of
 * this project (under the names LGPL-3.0.txt and ASL-2.0.txt respectively).
 *
 * Direct link to the sources:
 *
 * - LGPL 3.0: https://www.gnu.org/licenses/lgpl-3.0.txt
 * - ASL 2.0: http://www.apache.org/licenses/LICENSE-2.0.txt
 */

package com.github.fge.jsonschema.examples;

import com.fasterxml.jackson.databind.JsonNode;
import com.github.fge.jackson.NodeType;
import com.github.fge.jackson.jsonpointer.JsonPointer;
import com.github.fge.jsonschema.cfg.ValidationConfiguration;
import com.github.fge.jsonschema.core.exceptions.ProcessingException;
import com.github.fge.jsonschema.core.keyword.syntax.checkers.AbstractSyntaxChecker;
import com.github.fge.jsonschema.core.keyword.syntax.checkers.SyntaxChecker;
import com.github.fge.jsonschema.core.processing.Processor;
import com.github.fge.jsonschema.core.report.ProcessingReport;
import com.github.fge.jsonschema.core.tree.SchemaTree;
import com.github.fge.jsonschema.keyword.digest.AbstractDigester;
import com.github.fge.jsonschema.keyword.digest.Digester;
import com.github.fge.jsonschema.keyword.digest.helpers.IdentityDigester;
import com.github.fge.jsonschema.keyword.digest.helpers.SimpleDigester;
import com.github.fge.jsonschema.keyword.validator.AbstractKeywordValidator;
import com.github.fge.jsonschema.keyword.validator.KeywordValidator;
import com.github.fge.jsonschema.library.DraftV4Library;
import com.github.fge.jsonschema.library.Keyword;
import com.github.fge.jsonschema.library.KeywordBuilder;
import com.github.fge.jsonschema.library.Library;
import com.github.fge.jsonschema.library.LibraryBuilder;
import com.github.fge.jsonschema.main.JsonSchema;
import com.github.fge.jsonschema.main.JsonSchemaFactory;
import com.github.fge.jsonschema.messages.JsonSchemaValidationBundle;
import com.github.fge.jsonschema.processors.data.FullData;
import com.github.fge.msgsimple.bundle.MessageBundle;
import com.github.fge.msgsimple.load.MessageBundles;
import com.github.fge.msgsimple.source.MapMessageSource;
import com.github.fge.msgsimple.source.MessageSource;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.Lists;
import com.google.common.collect.Sets;

import java.io.IOException;
import java.math.BigInteger;
import java.util.Collection;
import java.util.Comparator;
import java.util.List;
import java.util.Set;
import java.util.SortedSet;

/**
 * Ninth example: augmenting schemas with custom keywords
 *
 * 

link to source code

* *

link to schema

* *

This example adds a custom keyword with syntax checking, digesting and * keyword validation. The chosen keyword is {@code divisors}: it applies to * integer values and takes an array of (unique) integers as an argument.

* *

The validation is the same as for {@code multipleOf} except that it is * restricted to integer values and the instance must be a multiple of all * divisors. For instance, if the value of this keyword is {@code [2, 3]}, then * 6 validates successfully but 14 does not (it is divisible by 2 but not 3). *

* *

For this, you need to create your own keyword. This is done using {@link * Keyword#newBuilder(String)}, where the argument is the name of your keyword, * and then add the following elements:

* *
    *
  • a {@link SyntaxChecker} (using {@link * KeywordBuilder#withSyntaxChecker(SyntaxChecker)};
  • *
  • a {@link Digester} (using {@link * KeywordBuilder#withDigester(Digester)};
  • *
  • and finally, a {@link KeywordValidator} (using {@link * KeywordBuilder#withValidatorClass(Class)}.
  • *
* *

Then, as in {@link Example8}, you need to get hold of a {@link Library} * (we choose again to extend the draft v4 library) and add the (frozen) * keyword to it using {@link LibraryBuilder#addKeyword(Keyword)}.

* *

The keyword validator must have a single constructor taking a * {@link JsonNode} as an argument (which will be the result of the {@link * Digester}). Note that you may omit to write a digester and choose instead to * use an {@link IdentityDigester} or a {@link SimpleDigester} (which you inject * into a keyword using {@link * KeywordBuilder#withIdentityDigester(NodeType, NodeType...)} and {@link * KeywordBuilder#withSimpleDigester(NodeType, NodeType...)} respectively).

* *

Two sample files are given: the first (link) is valid, the other (link) isn't (the first and third * elements fail to divide by one or more factors).

*/ public final class Example9 { public static void main(final String... args) throws IOException, ProcessingException { final JsonNode customSchema = Utils.loadResource("/custom-keyword.json"); final JsonNode good = Utils.loadResource("/custom-keyword-good.json"); final JsonNode bad = Utils.loadResource("/custom-keyword-bad.json"); /* * Build the new keyword */ final Keyword keyword = Keyword.newBuilder("divisors") .withSyntaxChecker(DivisorsSyntaxChecker.getInstance()) .withDigester(DivisorsDigesters.getInstance()) .withValidatorClass(DivisorsKeywordValidator.class).freeze(); /* * Build a library, based on the v4 library, with this new keyword */ final Library library = DraftV4Library.get().thaw() .addKeyword(keyword).freeze(); /* * Complement the validation message bundle with a dedicated message * for our keyword validator */ final String key = "missingDivisors"; final String value = "integer value is not a multiple of all divisors"; final MessageSource source = MapMessageSource.newBuilder() .put(key, value).build(); final MessageBundle bundle = MessageBundles.getBundle(JsonSchemaValidationBundle.class) .thaw().appendSource(source).freeze(); /* * Build a custom validation configuration: add our custom library and * message bundle */ final ValidationConfiguration cfg = ValidationConfiguration.newBuilder() .setDefaultLibrary("http://my.site/myschema#", library) .setValidationMessages(bundle).freeze(); final JsonSchemaFactory factory = JsonSchemaFactory.newBuilder() .setValidationConfiguration(cfg).freeze(); final JsonSchema schema = factory.getJsonSchema(customSchema); ProcessingReport report; report = schema.validate(good); System.out.println(report); report = schema.validate(bad); System.out.println(report); } /* * Our custom syntax checker */ private static final class DivisorsSyntaxChecker extends AbstractSyntaxChecker { private static final SyntaxChecker INSTANCE = new DivisorsSyntaxChecker(); public static SyntaxChecker getInstance() { return INSTANCE; } private DivisorsSyntaxChecker() { /* * When constructing, the name for the keyword must be provided * along with the allowed type for the value (here, an array). */ super("divisors", NodeType.ARRAY); } @Override protected void checkValue(final Collection pointers, final MessageBundle bundle, final ProcessingReport report, final SchemaTree tree) throws ProcessingException { /* * Using AbstractSyntaxChecker as a base, we know that when we reach * this method, the value has already been validated as being of * the allowed primitive types (only array here). * * But this is not enough for this particular validator: we must * also ensure that all elements of this array are integers. Cycle * through all elements of the array and check each element. If we * encounter a non integer argument, add a message. * * We must also check that there is at lease one element, that the * array contains no duplicates and that all elements are positive * integers and strictly greater than 0. * * The getNode() method grabs the value of this keyword for us, so * use that. Note that we also reuse some messages already defined * in SyntaxMessages. */ final JsonNode node = getNode(tree); final int size = node.size(); if (size == 0) { report.error(newMsg(tree, bundle, "emptyArray")); return; } NodeType type; JsonNode element; boolean uniqueItems = true; final Set set = Sets.newHashSet(); for (int index = 0; index < size; index++) { element = node.get(index); type = NodeType.getNodeType(element); if (type != NodeType.INTEGER) report.error(newMsg(tree, bundle, "incorrectElementType") .put("expected", NodeType.INTEGER) .put("found", type)); else if (element.bigIntegerValue().compareTo(BigInteger.ONE) < 0) report.error(newMsg(tree, bundle, "integerIsNegative") .put("value", element)); uniqueItems = set.add(element); } if (!uniqueItems) report.error(newMsg(tree, bundle, "elementsNotUnique")); } } /* * Our custom digester * * We take the opportunity to build a digested form where, for instance, * [ 3, 5 ] and [ 5, 3 ] give the same digest. */ private static final class DivisorsDigesters extends AbstractDigester { private static final Digester INSTANCE = new DivisorsDigesters(); public static Digester getInstance() { return INSTANCE; } private DivisorsDigesters() { super("divisors", NodeType.INTEGER); } @Override public JsonNode digest(final JsonNode schema) { final SortedSet set = Sets.newTreeSet(COMPARATOR); for (final JsonNode element: schema.get(keyword)) set.add(element); return FACTORY.arrayNode().addAll(set); } /* * Custom Comparator. We compare BigInteger values, since all integers * are representable using this class. */ private static final Comparator COMPARATOR = new Comparator() { @Override public int compare(final JsonNode o1, final JsonNode o2) { return o1.bigIntegerValue().compareTo(o2.bigIntegerValue()); } }; } /** * Custom keyword validator for {@link Example9} * * It must be {@code public} because it is built by reflection. */ public static final class DivisorsKeywordValidator extends AbstractKeywordValidator { /* * We want to validate arbitrarily large integer values, we therefore * use BigInteger. */ private final List divisors; public DivisorsKeywordValidator(final JsonNode digest) { super("divisors"); final ImmutableList.Builder list = ImmutableList.builder(); for (final JsonNode element: digest) list.add(element.bigIntegerValue()); divisors = list.build(); } @Override public void validate(final Processor processor, final ProcessingReport report, final MessageBundle bundle, final FullData data) throws ProcessingException { final BigInteger value = data.getInstance().getNode().bigIntegerValue(); /* * We use a plain list to store failed divisors: remember that the * digested form was built with divisors in order, we therefore * only need insertion order, and a plain ArrayList guarantees that. */ final List failed = Lists.newArrayList(); for (final BigInteger divisor: divisors) if (!value.mod(divisor).equals(BigInteger.ZERO)) failed.add(divisor); if (failed.isEmpty()) return; /* * There are missed divisors: report. */ report.error(newMsg(data, bundle, "missingDivisors") .put("divisors", divisors).put("failed", failed)); } @Override public String toString() { return "divisors: " + divisors; } } }




© 2015 - 2024 Weber Informatics LLC | Privacy Policy