Repository: johnzon Updated Branches: refs/heads/master f4c412647 -> d8abbf0c0
JOHNZON-171 more properties handling and configurable regex impl Project: http://git-wip-us.apache.org/repos/asf/johnzon/repo Commit: http://git-wip-us.apache.org/repos/asf/johnzon/commit/94a2179a Tree: http://git-wip-us.apache.org/repos/asf/johnzon/tree/94a2179a Diff: http://git-wip-us.apache.org/repos/asf/johnzon/diff/94a2179a Branch: refs/heads/master Commit: 94a2179a3a55acd094f63dde3c6cfd4b53601003 Parents: f4c4126 Author: Romain Manni-Bucau <[email protected]> Authored: Mon Apr 30 09:26:19 2018 +0200 Committer: Romain Manni-Bucau <[email protected]> Committed: Mon Apr 30 09:26:19 2018 +0200 ---------------------------------------------------------------------- .../jsonschema/JsonSchemaValidatorFactory.java | 109 +++++++++++++++++-- .../johnzon/jsonschema/regex/JavaRegex.java | 41 +++++++ .../jsonschema/regex/JavascriptRegex.java | 73 +++++++++++++ .../jsonschema/spi/builtin/BaseValidation.java | 3 +- .../spi/builtin/PatternValidation.java | 67 +++--------- .../jsonschema/JsonSchemaValidatorTest.java | 38 +++++++ 6 files changed, 266 insertions(+), 65 deletions(-) ---------------------------------------------------------------------- http://git-wip-us.apache.org/repos/asf/johnzon/blob/94a2179a/johnzon-jsonschema/src/main/java/org/apache/johnzon/jsonschema/JsonSchemaValidatorFactory.java ---------------------------------------------------------------------- diff --git a/johnzon-jsonschema/src/main/java/org/apache/johnzon/jsonschema/JsonSchemaValidatorFactory.java b/johnzon-jsonschema/src/main/java/org/apache/johnzon/jsonschema/JsonSchemaValidatorFactory.java index 38880ba..1eb2fbb 100644 --- a/johnzon-jsonschema/src/main/java/org/apache/johnzon/jsonschema/JsonSchemaValidatorFactory.java +++ b/johnzon-jsonschema/src/main/java/org/apache/johnzon/jsonschema/JsonSchemaValidatorFactory.java @@ -26,13 +26,17 @@ import java.util.ArrayList; import java.util.List; import java.util.Optional; import java.util.ServiceLoader; +import java.util.Set; +import java.util.concurrent.atomic.AtomicReference; import java.util.function.Function; +import java.util.function.Predicate; import java.util.stream.Stream; import java.util.stream.StreamSupport; import javax.json.JsonObject; import javax.json.JsonValue; +import org.apache.johnzon.jsonschema.regex.JavascriptRegex; import org.apache.johnzon.jsonschema.spi.ValidationContext; import org.apache.johnzon.jsonschema.spi.ValidationExtension; import org.apache.johnzon.jsonschema.spi.builtin.ContainsValidation; @@ -70,8 +74,18 @@ public class JsonSchemaValidatorFactory implements AutoCloseable { private final List<ValidationExtension> extensions = new ArrayList<>(); + // js is closer to default and actually most used in the industry + private final AtomicReference<Function<String, Predicate<CharSequence>>> regexFactory = new AtomicReference<>(JavascriptRegex::new); + public JsonSchemaValidatorFactory() { - extensions.addAll(asList( + extensions.addAll(createDefaultValidations()); + extensions.addAll(new ArrayList<>(StreamSupport.stream(ServiceLoader.load(ValidationExtension.class).spliterator(), false) + .collect(toList()))); + } + + // see http://json-schema.org/latest/json-schema-validation.html + public List<ValidationExtension> createDefaultValidations() { + return asList( new RequiredValidation(), new TypeValidation(), new EnumValidation(), @@ -82,7 +96,7 @@ public class JsonSchemaValidatorFactory implements AutoCloseable { new ExclusiveMinimumValidation(), new MaxLengthValidation(), new MinLengthValidation(), - new PatternValidation(), + new PatternValidation(regexFactory.get()), new ItemsValidation(this), new MaxItemsValidation(), new MinItemsValidation(), @@ -90,10 +104,9 @@ public class JsonSchemaValidatorFactory implements AutoCloseable { new ContainsValidation(this), new MaxPropertiesValidation(), new MinPropertiesValidation() - // todo: http://json-schema.org/latest/json-schema-validation.html#rfc.section.6.4 and following - )); - extensions.addAll(new ArrayList<>(StreamSupport.stream(ServiceLoader.load(ValidationExtension.class).spliterator(), false) - .collect(toList()))); + // TODO: dependencies, propertyNames, if/then/else, allOf/anyOf/oneOf/not, + // format validations + ); } public JsonSchemaValidatorFactory appendExtensions(final ValidationExtension... extensions) { @@ -106,6 +119,11 @@ public class JsonSchemaValidatorFactory implements AutoCloseable { return appendExtensions(extensions); } + public JsonSchemaValidatorFactory setRegexFactory(final Function<String, Predicate<CharSequence>> factory) { + regexFactory.set(factory); + return this; + } + public JsonSchemaValidator newInstance(final JsonObject schema) { return new JsonSchemaValidator(buildValidator(ROOT_PATH, schema, null)); } @@ -119,8 +137,14 @@ public class JsonSchemaValidatorFactory implements AutoCloseable { final JsonObject schema, final Function<JsonValue, JsonValue> valueProvider) { final List<Function<JsonValue, Stream<ValidationResult.ValidationError>>> directValidations = buildDirectValidations(path, schema, valueProvider).collect(toList()); - final Function<JsonValue, Stream<ValidationResult.ValidationError>> nestedValidations = buildNestedValidations(path, schema, valueProvider); - return new ValidationsFunction(Stream.concat(directValidations.stream(), Stream.of(nestedValidations)).collect(toList())); + final Function<JsonValue, Stream<ValidationResult.ValidationError>> nestedValidations = buildPropertiesValidations(path, schema, valueProvider); + final Function<JsonValue, Stream<ValidationResult.ValidationError>> dynamicNestedValidations = buildPatternPropertiesValidations(path, schema, valueProvider); + final Function<JsonValue, Stream<ValidationResult.ValidationError>> fallbackNestedValidations = buildAdditionalPropertiesValidations(path, schema, valueProvider); + return new ValidationsFunction( + Stream.concat( + directValidations.stream(), + Stream.of(nestedValidations, dynamicNestedValidations, fallbackNestedValidations)) + .collect(toList())); } private Stream<Function<JsonValue, Stream<ValidationResult.ValidationError>>> buildDirectValidations(final String[] path, @@ -133,9 +157,9 @@ public class JsonSchemaValidatorFactory implements AutoCloseable { .map(Optional::get); } - private Function<JsonValue, Stream<ValidationResult.ValidationError>> buildNestedValidations(final String[] path, - final JsonObject schema, - final Function<JsonValue, JsonValue> valueProvider) { + private Function<JsonValue, Stream<ValidationResult.ValidationError>> buildPropertiesValidations(final String[] path, + final JsonObject schema, + final Function<JsonValue, JsonValue> valueProvider) { return ofNullable(schema.get("properties")) .filter(it -> it.getValueType() == JsonValue.ValueType.OBJECT) .map(it -> it.asJsonObject().entrySet().stream() @@ -150,6 +174,69 @@ public class JsonSchemaValidatorFactory implements AutoCloseable { .orElse(NO_VALIDATION); } + // not the best impl but is it really an important case? + private Function<JsonValue, Stream<ValidationResult.ValidationError>> buildPatternPropertiesValidations(final String[] path, + final JsonObject schema, + final Function<JsonValue, JsonValue> valueProvider) { + return ofNullable(schema.get("patternProperties")) + .filter(it -> it.getValueType() == JsonValue.ValueType.OBJECT) + .map(it -> it.asJsonObject().entrySet().stream() + .filter(obj -> obj.getValue().getValueType() == JsonValue.ValueType.OBJECT) + .map(obj -> { + final Predicate<CharSequence> pattern = regexFactory.get().apply(obj.getKey()); + final JsonObject currentSchema = obj.getValue().asJsonObject(); + // no cache cause otherwise it could be in properties + return (Function<JsonValue, Stream<ValidationResult.ValidationError>>) validable -> { + if (validable.getValueType() != JsonValue.ValueType.OBJECT) { + return Stream.empty(); + } + return validable.asJsonObject().entrySet().stream() + .filter(e -> pattern.test(e.getKey())) + .flatMap(e -> buildValidator( + Stream.concat(Stream.of(path), Stream.of(e.getKey())).toArray(String[]::new), + currentSchema, + new ChainedValueAccessor(valueProvider, e.getKey())).apply(e.getValue())); + }; + }) + .collect(toList())) + .map(this::toFunction) + .orElse(NO_VALIDATION); + } + + private Function<JsonValue, Stream<ValidationResult.ValidationError>> buildAdditionalPropertiesValidations(final String[] path, + final JsonObject schema, + final Function<JsonValue, JsonValue> valueProvider) { + return ofNullable(schema.get("additionalProperties")) + .filter(it -> it.getValueType() == JsonValue.ValueType.OBJECT) + .map(it -> { + Predicate<String> excluded = s -> false; + if (schema.containsKey("properties")) { + final Set<String> properties = schema.getJsonObject("properties").keySet(); + excluded = excluded.and(s -> !properties.contains(s)); + } + if (schema.containsKey("patternProperties")) { + final List<Predicate<CharSequence>> properties = schema.getJsonObject("patternProperties").keySet().stream() + .map(regexFactory.get()) + .collect(toList()); + excluded = excluded.and(s -> properties.stream().noneMatch(p -> p.test(s))); + } + final Predicate<String> excludeAttrRef = excluded; + final JsonObject currentSchema = it.asJsonObject(); + return (Function<JsonValue, Stream<ValidationResult.ValidationError>>) validable -> { + if (validable.getValueType() != JsonValue.ValueType.OBJECT) { + return Stream.empty(); + } + return validable.asJsonObject().entrySet().stream() + .filter(e -> excludeAttrRef.test(e.getKey())) + .flatMap(e -> buildValidator( + Stream.concat(Stream.of(path), Stream.of(e.getKey())).toArray(String[]::new), + currentSchema, + new ChainedValueAccessor(valueProvider, e.getKey())).apply(e.getValue())); + }; + }) + .orElse(NO_VALIDATION); + } + private Function<JsonValue, Stream<ValidationResult.ValidationError>> toFunction( final List<Function<JsonValue, Stream<ValidationResult.ValidationError>>> validations) { return new ValidationsFunction(validations); http://git-wip-us.apache.org/repos/asf/johnzon/blob/94a2179a/johnzon-jsonschema/src/main/java/org/apache/johnzon/jsonschema/regex/JavaRegex.java ---------------------------------------------------------------------- diff --git a/johnzon-jsonschema/src/main/java/org/apache/johnzon/jsonschema/regex/JavaRegex.java b/johnzon-jsonschema/src/main/java/org/apache/johnzon/jsonschema/regex/JavaRegex.java new file mode 100644 index 0000000..85524e4 --- /dev/null +++ b/johnzon-jsonschema/src/main/java/org/apache/johnzon/jsonschema/regex/JavaRegex.java @@ -0,0 +1,41 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +package org.apache.johnzon.jsonschema.regex; + +import java.util.function.Predicate; +import java.util.regex.Pattern; + +public class JavaRegex implements Predicate<CharSequence> { + + private final Pattern pattern; + + public JavaRegex(final String pattern) { + this.pattern = Pattern.compile(pattern); + } + + @Override + public boolean test(final CharSequence charSequence) { + return pattern.matcher(charSequence).matches(); + } + + @Override + public String toString() { + return "JavaRegex{" + pattern + '}'; + } +} http://git-wip-us.apache.org/repos/asf/johnzon/blob/94a2179a/johnzon-jsonschema/src/main/java/org/apache/johnzon/jsonschema/regex/JavascriptRegex.java ---------------------------------------------------------------------- diff --git a/johnzon-jsonschema/src/main/java/org/apache/johnzon/jsonschema/regex/JavascriptRegex.java b/johnzon-jsonschema/src/main/java/org/apache/johnzon/jsonschema/regex/JavascriptRegex.java new file mode 100644 index 0000000..9b83389 --- /dev/null +++ b/johnzon-jsonschema/src/main/java/org/apache/johnzon/jsonschema/regex/JavascriptRegex.java @@ -0,0 +1,73 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +package org.apache.johnzon.jsonschema.regex; + +import java.util.function.Predicate; + +import javax.script.Bindings; +import javax.script.ScriptEngine; +import javax.script.ScriptEngineManager; +import javax.script.ScriptException; + +public class JavascriptRegex implements Predicate<CharSequence> { + + private static final ScriptEngine ENGINE; + + static { + ENGINE = new ScriptEngineManager().getEngineByName("javascript"); + } + + private final String regex; + + private final String indicators; + + public JavascriptRegex(final String regex) { + if (regex.startsWith("/") && regex.length() > 1) { + final int end = regex.lastIndexOf('/'); + if (end < 0) { + this.regex = regex; + this.indicators = ""; + } else { + this.regex = regex.substring(1, end); + this.indicators = regex.substring(end + 1); + } + } else { + this.regex = regex; + this.indicators = ""; + } + } + + @Override + public boolean test(final CharSequence string) { + final Bindings bindings = ENGINE.createBindings(); + bindings.put("text", string); + bindings.put("regex", regex); + bindings.put("indicators", indicators); + try { + return Boolean.class.cast(ENGINE.eval("new RegExp(regex, indicators).test(text)", bindings)); + } catch (final ScriptException e) { + return false; + } + } + + @Override + public String toString() { + return "JavascriptRegex{/" + regex + "/" + indicators + '}'; + } +} http://git-wip-us.apache.org/repos/asf/johnzon/blob/94a2179a/johnzon-jsonschema/src/main/java/org/apache/johnzon/jsonschema/spi/builtin/BaseValidation.java ---------------------------------------------------------------------- diff --git a/johnzon-jsonschema/src/main/java/org/apache/johnzon/jsonschema/spi/builtin/BaseValidation.java b/johnzon-jsonschema/src/main/java/org/apache/johnzon/jsonschema/spi/builtin/BaseValidation.java index 5e0b9ed..0b4e569 100644 --- a/johnzon-jsonschema/src/main/java/org/apache/johnzon/jsonschema/spi/builtin/BaseValidation.java +++ b/johnzon-jsonschema/src/main/java/org/apache/johnzon/jsonschema/spi/builtin/BaseValidation.java @@ -67,8 +67,9 @@ abstract class BaseValidation implements Function<JsonValue, Stream<ValidationRe return onArray(value.asJsonArray()); case NULL: return Stream.empty(); + default: + throw new IllegalArgumentException("Unsupported value type: " + value); } - throw new IllegalArgumentException("Unsupported value type: " + value); } protected boolean isNull(final JsonValue obj) { http://git-wip-us.apache.org/repos/asf/johnzon/blob/94a2179a/johnzon-jsonschema/src/main/java/org/apache/johnzon/jsonschema/spi/builtin/PatternValidation.java ---------------------------------------------------------------------- diff --git a/johnzon-jsonschema/src/main/java/org/apache/johnzon/jsonschema/spi/builtin/PatternValidation.java b/johnzon-jsonschema/src/main/java/org/apache/johnzon/jsonschema/spi/builtin/PatternValidation.java index 7085225..8778444 100644 --- a/johnzon-jsonschema/src/main/java/org/apache/johnzon/jsonschema/spi/builtin/PatternValidation.java +++ b/johnzon-jsonschema/src/main/java/org/apache/johnzon/jsonschema/spi/builtin/PatternValidation.java @@ -25,38 +25,41 @@ import java.util.stream.Stream; import javax.json.JsonString; import javax.json.JsonValue; -import javax.script.Bindings; -import javax.script.ScriptEngine; -import javax.script.ScriptEngineManager; -import javax.script.ScriptException; import org.apache.johnzon.jsonschema.ValidationResult; import org.apache.johnzon.jsonschema.spi.ValidationContext; import org.apache.johnzon.jsonschema.spi.ValidationExtension; public class PatternValidation implements ValidationExtension { + private final Function<String, Predicate<CharSequence>> predicateFactory; + + public PatternValidation(final Function<String, Predicate<CharSequence>> predicateFactory) { + this.predicateFactory = predicateFactory; + } + @Override public Optional<Function<JsonValue, Stream<ValidationResult.ValidationError>>> create(final ValidationContext model) { if (model.getSchema().getString("type", "object").equals("string")) { return Optional.ofNullable(model.getSchema().get("pattern")) .filter(val -> val.getValueType() == JsonValue.ValueType.STRING) - .map(pattern -> new Impl(model.toPointer(), model.getValueProvider(), JsonString.class.cast(pattern).getString())); + .map(pattern -> new Impl(model.toPointer(), model.getValueProvider(), predicateFactory.apply(JsonString.class.cast(pattern).getString()))); } return Optional.empty(); } private static class Impl extends BaseValidation { - private final JsRegex jsRegex; + private final Predicate<CharSequence> matcher; - private Impl(final String pointer, final Function<JsonValue, JsonValue> valueProvider, final String pattern) { + private Impl(final String pointer, final Function<JsonValue, JsonValue> valueProvider, + final Predicate<CharSequence> matcher) { super(pointer, valueProvider, JsonValue.ValueType.STRING); - this.jsRegex = new JsRegex(pattern); + this.matcher = matcher; } @Override public Stream<ValidationResult.ValidationError> onString(final JsonString value) { - if (!jsRegex.test(value.getString())) { - return Stream.of(new ValidationResult.ValidationError(pointer, value + " doesn't match " + jsRegex)); + if (!matcher.test(value.getString())) { + return Stream.of(new ValidationResult.ValidationError(pointer, value + " doesn't match " + matcher)); } return Stream.empty(); } @@ -64,51 +67,9 @@ public class PatternValidation implements ValidationExtension { @Override public String toString() { return "Pattern{" + - "regex=" + jsRegex + + "regex=" + matcher + ", pointer='" + pointer + '\'' + '}'; } } - - private static class JsRegex implements Predicate<CharSequence> { - - private static final ScriptEngine ENGINE; - - static { - ENGINE = new ScriptEngineManager().getEngineByName("javascript"); - } - - private final String regex; - - private final String indicators; - - private JsRegex(final String regex) { - if (regex.startsWith("/") && regex.length() > 1) { - final int end = regex.lastIndexOf('/'); - if (end < 0) { - this.regex = regex; - this.indicators = ""; - } else { - this.regex = regex.substring(1, end); - this.indicators = regex.substring(end + 1); - } - } else { - this.regex = regex; - this.indicators = ""; - } - } - - @Override - public boolean test(final CharSequence string) { - final Bindings bindings = ENGINE.createBindings(); - bindings.put("text", string); - bindings.put("regex", regex); - bindings.put("indicators", indicators); - try { - return Boolean.class.cast(ENGINE.eval("new RegExp(regex, indicators).test(text)", bindings)); - } catch (final ScriptException e) { - return false; - } - } - } } http://git-wip-us.apache.org/repos/asf/johnzon/blob/94a2179a/johnzon-jsonschema/src/test/java/org/apache/johnzon/jsonschema/JsonSchemaValidatorTest.java ---------------------------------------------------------------------- diff --git a/johnzon-jsonschema/src/test/java/org/apache/johnzon/jsonschema/JsonSchemaValidatorTest.java b/johnzon-jsonschema/src/test/java/org/apache/johnzon/jsonschema/JsonSchemaValidatorTest.java index bde29c9..ff101c6 100644 --- a/johnzon-jsonschema/src/test/java/org/apache/johnzon/jsonschema/JsonSchemaValidatorTest.java +++ b/johnzon-jsonschema/src/test/java/org/apache/johnzon/jsonschema/JsonSchemaValidatorTest.java @@ -555,4 +555,42 @@ public class JsonSchemaValidatorTest { validator.close(); } + + @Test + public void patternProperties() { + final JsonSchemaValidator validator = factory.newInstance(jsonFactory.createObjectBuilder() + .add("type", "object") + .add("patternProperties", jsonFactory.createObjectBuilder() + .add("[0-9]+", jsonFactory.createObjectBuilder().add("type", "number")) + .build()) + .build()); + + assertTrue(validator.apply(jsonFactory.createObjectBuilder().build()).isSuccess()); + assertTrue(validator.apply(jsonFactory.createObjectBuilder() + .add("1", 1) + .build()).isSuccess()); + assertTrue(validator.apply(jsonFactory.createObjectBuilder() + .add("1", "test") + .build()).isSuccess()); + + validator.close(); + } + + @Test + public void additionalProperties() { + final JsonSchemaValidator validator = factory.newInstance(jsonFactory.createObjectBuilder() + .add("type", "object") + .add("additionalProperties", jsonFactory.createObjectBuilder().add("type", "number")) + .build()); + + assertTrue(validator.apply(jsonFactory.createObjectBuilder().build()).isSuccess()); + assertTrue(validator.apply(jsonFactory.createObjectBuilder() + .add("1", 1) + .build()).isSuccess()); + assertTrue(validator.apply(jsonFactory.createObjectBuilder() + .add("1", "test") + .build()).isSuccess()); + + validator.close(); + } }
