From df18990d92eb84fa5885fc3a814c7bd42ad509c5 Mon Sep 17 00:00:00 2001 From: Justin Tay <49700559+justin-tay@users.noreply.github.com> Date: Tue, 12 Dec 2023 14:41:01 +0800 Subject: [PATCH 1/4] Add strategy for resolving locale specific messages --- .../networknt/schema/i18n/MessageSource.java | 61 ++++++++ .../i18n/ResourceBundleMessageSource.java | 137 ++++++++++++++++++ .../i18n/ResourceBundleMessageSourceTest.java | 83 +++++++++++ .../jsv-messages-override.properties | 1 + src/test/resources/test-messages.properties | 1 + .../resources/test-messages_fr.properties | 1 + 6 files changed, 284 insertions(+) create mode 100644 src/main/java/com/networknt/schema/i18n/MessageSource.java create mode 100644 src/main/java/com/networknt/schema/i18n/ResourceBundleMessageSource.java create mode 100644 src/test/java/com/networknt/schema/i18n/ResourceBundleMessageSourceTest.java create mode 100644 src/test/resources/jsv-messages-override.properties create mode 100644 src/test/resources/test-messages.properties create mode 100644 src/test/resources/test-messages_fr.properties diff --git a/src/main/java/com/networknt/schema/i18n/MessageSource.java b/src/main/java/com/networknt/schema/i18n/MessageSource.java new file mode 100644 index 000000000..aaeb8aaa0 --- /dev/null +++ b/src/main/java/com/networknt/schema/i18n/MessageSource.java @@ -0,0 +1,61 @@ +/* + * Copyright (c) 2023 the original author or authors. + * + * Licensed 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 com.networknt.schema.i18n; + +import java.util.Locale; +import java.util.function.Supplier; + +/** + * Resolves locale specific messages. + */ +@FunctionalInterface +public interface MessageSource { + /** + * Gets the message. + * + * @param key to look up the message + * @param defaultMessageSupplier the default message + * @param locale the locale to use + * @param args the message arguments + * @return the message + */ + String getMessage(String key, Supplier defaultMessageSupplier, Locale locale, Object... args); + + /** + * Gets the message. + * + * @param key to look up the message + * @param defaultMessage the default message + * @param locale the locale to use + * @param args the message arguments + * @return the message + */ + default String getMessage(String key, String defaultMessage, Locale locale, Object... args) { + return getMessage(key, defaultMessage::toString, locale, args); + } + + /** + * Gets the message. + * + * @param key to look up the message + * @param locale the locale to use + * @param args the message arguments + * @return the message + */ + default String getMessage(String key, Locale locale, Object... args) { + return getMessage(key, (Supplier) null, locale, args); + } +} diff --git a/src/main/java/com/networknt/schema/i18n/ResourceBundleMessageSource.java b/src/main/java/com/networknt/schema/i18n/ResourceBundleMessageSource.java new file mode 100644 index 000000000..761bc927e --- /dev/null +++ b/src/main/java/com/networknt/schema/i18n/ResourceBundleMessageSource.java @@ -0,0 +1,137 @@ +/* + * Copyright (c) 2023 the original author or authors. + * + * Licensed 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 com.networknt.schema.i18n; + +import java.text.MessageFormat; +import java.util.Arrays; +import java.util.List; +import java.util.Locale; +import java.util.Map; +import java.util.MissingResourceException; +import java.util.Objects; +import java.util.Optional; +import java.util.ResourceBundle; +import java.util.concurrent.ConcurrentHashMap; +import java.util.function.Supplier; + +/** + * {@link MessageSource} that retrieves messages from a {@link ResourceBundle}. + */ +public class ResourceBundleMessageSource implements MessageSource { + /** + * Resource Bundle Cache. baseName -> locale -> resourceBundle. + */ + private Map> resourceBundleMap = new ConcurrentHashMap<>(); + + /** + * Message Cache. locale -> key -> message. + */ + private Map> messageMap = new ConcurrentHashMap<>(); + + /** + * Message Format Cache. locale -> message -> messageFormat. + *

+ * Note that Message Format is not threadsafe. + */ + private Map> messageFormatMap = new ConcurrentHashMap<>(); + + private final List baseNames; + + public ResourceBundleMessageSource(String... baseName) { + this.baseNames = Arrays.asList(baseName); + } + + @Override + public String getMessage(String key, Supplier defaultMessage, Locale locale, Object... arguments) { + String message = getMessageFromCache(locale, key); + if (message.isEmpty() && defaultMessage != null) { + message = defaultMessage.get(); + } + if (message.isEmpty()) { + // Fallback on message key + message = key; + } + if (arguments == null || arguments.length == 0) { + // When no arguments just return the message without formatting + return message; + } + MessageFormat messageFormat = getMessageFormat(locale, message); + synchronized (messageFormat) { + // Synchronized block on messageFormat as it is not threadsafe + return messageFormat.format(arguments, new StringBuffer(), null).toString(); + } + } + + protected MessageFormat getMessageFormat(Locale locale, String message) { + Map map = messageFormatMap.computeIfAbsent(locale, l -> { + return new ConcurrentHashMap<>(); + }); + return map.computeIfAbsent(message, m -> { + return new MessageFormat(m, locale); + }); + } + + /** + * Gets the message from cache or the resource bundles. Returns an empty string + * if not found. + * + * @param locale the locale + * @param key the message key + * @return the message + */ + protected String getMessageFromCache(Locale locale, String key) { + Map map = messageMap.computeIfAbsent(locale, l -> new ConcurrentHashMap<>()); + return map.computeIfAbsent(key, k -> { + return resolveMessage(locale, k); + }); + } + + /** + * Gets the message from the resource bundles. Returns an empty string if not + * found. + * + * @param locale the locale + * @param key the message key + * @return the message + */ + protected String resolveMessage(Locale locale, String key) { + Optional optionalPattern = this.baseNames.stream().map(baseName -> getResourceBundle(baseName, locale)) + .filter(Objects::nonNull).map(resourceBundle -> { + try { + return resourceBundle.getString(key); + } catch (MissingResourceException e) { + return null; + } + }).filter(Objects::nonNull).findFirst(); + return optionalPattern.orElse(""); + } + + protected Map getResourceBundle(String baseName) { + return resourceBundleMap.computeIfAbsent(baseName, key -> { + return new ConcurrentHashMap<>(); + }); + } + + protected ResourceBundle getResourceBundle(String baseName, Locale locale) { + return getResourceBundle(baseName).computeIfAbsent(locale, key -> { + try { + return ResourceBundle.getBundle(baseName, key); + } catch (MissingResourceException e) { + return null; + } + }); + } +} diff --git a/src/test/java/com/networknt/schema/i18n/ResourceBundleMessageSourceTest.java b/src/test/java/com/networknt/schema/i18n/ResourceBundleMessageSourceTest.java new file mode 100644 index 000000000..809df08dd --- /dev/null +++ b/src/test/java/com/networknt/schema/i18n/ResourceBundleMessageSourceTest.java @@ -0,0 +1,83 @@ +/* + * Copyright (c) 2023 the original author or authors. + * + * Licensed 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 com.networknt.schema.i18n; + +import static org.junit.jupiter.api.Assertions.assertEquals; + +import org.junit.jupiter.api.Test; + +import java.util.Locale; + +class ResourceBundleMessageSourceTest { + + ResourceBundleMessageSource messageSource = new ResourceBundleMessageSource("jsv-messages", "test-messages"); + + @Test + void messageNoDefault() { + String message = messageSource.getMessage("unknown.key", Locale.getDefault()); + assertEquals("unknown.key", message); + } + + @Test + void messageDefaultSupplier() { + String message = messageSource.getMessage("unknown.key", "default", Locale.getDefault()); + assertEquals("default", message); + } + + @Test + void messageDefaultSupplierArguments() { + String message = messageSource.getMessage("unknown.key", "An error {0}", Locale.getDefault(), "argument"); + assertEquals("An error argument", message); + } + + @Test + void messageFound() { + String message = messageSource.getMessage("atmostOne", Locale.getDefault()); + assertEquals("english", message); + } + + @Test + void messageFallbackOnDefaultLocale() { + String message = messageSource.getMessage("atmostOne", Locale.SIMPLIFIED_CHINESE); + assertEquals("english", message); + } + + @Test + void messageFrench() { + String message = messageSource.getMessage("atmostOne", Locale.FRANCE); + assertEquals("french", message); + } + + @Test + void messageMaxItems() { + String message = messageSource.getMessage("maxItems", Locale.getDefault(), "item", 5); + assertEquals("item: there must be a maximum of 5 items in the array", message); + } + + @Test + void missingBundleShouldNotThrow() { + MessageSource messageSource = new ResourceBundleMessageSource("missing-bundle"); + assertEquals("missing", messageSource.getMessage("missing", Locale.getDefault())); + } + + @Test + void overrideMessage() { + MessageSource messageSource = new ResourceBundleMessageSource("jsv-messages-override", "jsv-messages"); + assertEquals("path: overridden message value", messageSource.getMessage("allOf", Locale.ROOT, "path", "value")); + assertEquals("path: overridden message value", messageSource.getMessage("allOf", Locale.FRENCH, "path", "value")); + assertEquals("path: should be valid to any of the schemas value", messageSource.getMessage("anyOf", Locale.ROOT, "path", "value")); + } +} diff --git a/src/test/resources/jsv-messages-override.properties b/src/test/resources/jsv-messages-override.properties new file mode 100644 index 000000000..a9794e6ef --- /dev/null +++ b/src/test/resources/jsv-messages-override.properties @@ -0,0 +1 @@ +allOf = {0}: overridden message {1} diff --git a/src/test/resources/test-messages.properties b/src/test/resources/test-messages.properties new file mode 100644 index 000000000..6e4a5b655 --- /dev/null +++ b/src/test/resources/test-messages.properties @@ -0,0 +1 @@ +atmostOne = english diff --git a/src/test/resources/test-messages_fr.properties b/src/test/resources/test-messages_fr.properties new file mode 100644 index 000000000..2d92388f9 --- /dev/null +++ b/src/test/resources/test-messages_fr.properties @@ -0,0 +1 @@ +atmostOne = french From 1f674549a2b83225cf7187f15e370d95be574e3e Mon Sep 17 00:00:00 2001 From: Justin Tay <49700559+justin-tay@users.noreply.github.com> Date: Wed, 13 Dec 2023 10:53:18 +0800 Subject: [PATCH 2/4] Support priority list --- .../com/networknt/schema/i18n/Locales.java | 102 ++++++++++++++++++ .../networknt/schema/i18n/LocalesTest.java | 55 ++++++++++ 2 files changed, 157 insertions(+) create mode 100644 src/main/java/com/networknt/schema/i18n/Locales.java create mode 100644 src/test/java/com/networknt/schema/i18n/LocalesTest.java diff --git a/src/main/java/com/networknt/schema/i18n/Locales.java b/src/main/java/com/networknt/schema/i18n/Locales.java new file mode 100644 index 000000000..85c2cb880 --- /dev/null +++ b/src/main/java/com/networknt/schema/i18n/Locales.java @@ -0,0 +1,102 @@ +/* + * Copyright (c) 2023 the original author or authors. + * + * Licensed 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 com.networknt.schema.i18n; + +import java.util.Arrays; +import java.util.Collection; +import java.util.List; +import java.util.Locale; +import java.util.Locale.FilteringMode; +import java.util.Locale.LanguageRange; +import java.util.stream.Collectors; + +/** + * Functions for working with Locales. + */ +public class Locales { + /** + * The list of locale resource bundles. + */ + public static final String[] SUPPORTED_LANGUAGE_TAGS = new String[] { "ar-EG", "cs-CZ", "da-DK", "de", "fa-IR", + "fi-FI", "fr-CA", "fr", "he-IL", "hr-HR", "hu-HU", "it", "ja-JP", "ko-KR", "nb-NO", "nl-NL", "pl-PL", + "pt-BR", "ro-RO", "ru-RU", "sk-SK", "sv-SE", "th-TH", "tr-TR", "uk-UA", "vi-VN", "zh-CN", "zh-TW" }; + + /** + * The supported locales. + */ + public static final List SUPPORTED_LOCALES = of(SUPPORTED_LANGUAGE_TAGS); + + /** + * Gets the supported locales. + * + * @return the supported locales + */ + public static List getSupportedLocales() { + return SUPPORTED_LOCALES; + } + + /** + * Gets a list of {@link Locale} by language tags. + * + * @param languageTags for the locales + * @return the locales + */ + public static List of(String... languageTags) { + return Arrays.asList(languageTags).stream().map(Locale::forLanguageTag).collect(Collectors.toList()); + } + + /** + * Determine the best matching {@link Locale} with respect to the priority list. + * + * @param priorityList the language tag priority list + * @return the best matching locale + */ + public static Locale findSupported(String priorityList) { + return findSupported(priorityList, getSupportedLocales()); + } + + /** + * Determine the best matching {@link Locale} with respect to the priority list. + * + * @param priorityList the language tag priority list + * @param locales the supported locales + * @return the best matching locale + */ + public static Locale findSupported(String priorityList, Collection locales) { + return findSupported(LanguageRange.parse(priorityList), locales, FilteringMode.AUTOSELECT_FILTERING); + } + + /** + * Determine the best matching {@link Locale} with respect to the priority list. + * + * @param priorityList the language tag priority list + * @param locales the supported locales + * @param filteringMode the filtering mode + * @return the best matching locale + */ + public static Locale findSupported(List priorityList, Collection locales, + FilteringMode filteringMode) { + Locale result = Locale.lookup(priorityList, locales); + if (result != null) { + return result; + } + List matching = Locale.filter(priorityList, locales, filteringMode); + if (!matching.isEmpty()) { + return matching.get(0); + } + return Locale.ROOT; + } +} diff --git a/src/test/java/com/networknt/schema/i18n/LocalesTest.java b/src/test/java/com/networknt/schema/i18n/LocalesTest.java new file mode 100644 index 000000000..d88f06a26 --- /dev/null +++ b/src/test/java/com/networknt/schema/i18n/LocalesTest.java @@ -0,0 +1,55 @@ +/* + * Copyright (c) 2023 the original author or authors. + * + * Licensed 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 com.networknt.schema.i18n; + +import static org.junit.jupiter.api.Assertions.assertEquals; + +import java.util.Locale; + +import org.junit.jupiter.api.Test; + +class LocalesTest { + + @Test + void unsupportedShouldReturnLocaleRoot() { + Locale result = Locales.findSupported("en-US;q=0.9,en-GB;q=1.0"); + assertEquals("", result.getLanguage()); + } + + @Test + void shouldReturnHigherPriority() { + Locale result = Locales.findSupported("zh-CN;q=0.9,zh-TW;q=1.0"); + assertEquals("zh-TW", result.toLanguageTag()); + } + + @Test + void shouldReturnHigherPriorityToo() { + Locale result = Locales.findSupported("zh-CN;q=1.0,zh-TW;q=0.9"); + assertEquals("zh-CN", result.toLanguageTag()); + } + + @Test + void shouldReturnFound() { + Locale result = Locales.findSupported("zh-SG;q=1.0,zh-TW;q=0.9"); + assertEquals("zh-TW", result.toLanguageTag()); + } + + @Test + void shouldReturnFounds() { + Locale result = Locales.findSupported("zh;q=1.0"); + assertEquals("zh", result.getLanguage()); + } +} From 6a7847d8646f7764b1133a625b4a671f98353296 Mon Sep 17 00:00:00 2001 From: Justin Tay <49700559+justin-tay@users.noreply.github.com> Date: Wed, 13 Dec 2023 15:29:06 +0800 Subject: [PATCH 3/4] Use per execution locale --- doc/multiple-language.md | 44 ++++- .../schema/AdditionalPropertiesValidator.java | 2 +- .../com/networknt/schema/AnyOfValidator.java | 9 +- .../networknt/schema/BaseJsonValidator.java | 4 +- .../com/networknt/schema/ConstValidator.java | 10 +- .../networknt/schema/ContainsValidator.java | 11 +- .../schema/DependenciesValidator.java | 3 +- .../networknt/schema/DependentRequired.java | 3 +- .../com/networknt/schema/EnumValidator.java | 6 +- .../schema/ExclusiveMaximumValidator.java | 3 +- .../schema/ExclusiveMinimumValidator.java | 3 +- .../com/networknt/schema/ExecutionConfig.java | 36 ++++ .../networknt/schema/ExecutionContext.java | 18 ++ .../com/networknt/schema/FalseValidator.java | 5 +- .../com/networknt/schema/FormatValidator.java | 14 +- .../com/networknt/schema/I18nSupport.java | 1 + .../com/networknt/schema/ItemsValidator.java | 3 +- .../java/com/networknt/schema/JsonSchema.java | 35 ++-- .../networknt/schema/MaxItemsValidator.java | 4 +- .../networknt/schema/MaxLengthValidator.java | 3 +- .../schema/MaxPropertiesValidator.java | 3 +- .../networknt/schema/MaximumValidator.java | 3 +- .../networknt/schema/MinItemsValidator.java | 6 +- .../networknt/schema/MinLengthValidator.java | 3 +- .../schema/MinMaxContainsValidator.java | 54 ++++-- .../schema/MinPropertiesValidator.java | 3 +- .../networknt/schema/MinimumValidator.java | 3 +- .../networknt/schema/MultipleOfValidator.java | 3 +- .../networknt/schema/NotAllowedValidator.java | 2 +- .../com/networknt/schema/NotValidator.java | 6 +- .../com/networknt/schema/OneOfValidator.java | 3 +- .../networknt/schema/PatternValidator.java | 3 +- .../schema/PropertyNamesValidator.java | 3 +- .../networknt/schema/ReadOnlyValidator.java | 9 +- .../schema/RecursiveRefValidator.java | 14 +- .../com/networknt/schema/RefValidator.java | 11 +- .../networknt/schema/RequiredValidator.java | 2 +- .../schema/SchemaValidatorsConfig.java | 76 ++++---- .../com/networknt/schema/TypeValidator.java | 3 +- .../schema/UnevaluatedItemsValidator.java | 10 +- .../UnevaluatedPropertiesValidator.java | 8 +- .../networknt/schema/UnionTypeValidator.java | 3 +- .../schema/UniqueItemsValidator.java | 2 +- .../networknt/schema/ValidationMessage.java | 174 +++++++++++------- .../schema/ValidationMessageHandler.java | 30 ++- .../networknt/schema/WriteOnlyValidator.java | 7 +- .../schema/format/DateTimeValidator.java | 10 +- .../schema/i18n/DefaultMessageSource.java | 36 ++++ .../schema/i18n/MessageFormatter.java | 30 +++ .../schema/utils/CachingSupplier.java | 43 +++++ .../schema/CustomMetaSchemaTest.java | 5 +- .../com/networknt/schema/Issue686Test.java | 35 +--- .../com/networknt/schema/Issue898Test.java | 3 +- .../java/com/networknt/schema/LocaleTest.java | 68 +++++++ .../OverwritingCustomMessageBugTest.java | 4 +- 55 files changed, 609 insertions(+), 286 deletions(-) create mode 100644 src/main/java/com/networknt/schema/ExecutionConfig.java create mode 100644 src/main/java/com/networknt/schema/i18n/DefaultMessageSource.java create mode 100644 src/main/java/com/networknt/schema/i18n/MessageFormatter.java create mode 100644 src/main/java/com/networknt/schema/utils/CachingSupplier.java create mode 100644 src/test/java/com/networknt/schema/LocaleTest.java diff --git a/doc/multiple-language.md b/doc/multiple-language.md index 73d4191cf..73cb84079 100644 --- a/doc/multiple-language.md +++ b/doc/multiple-language.md @@ -24,15 +24,47 @@ JsonSchema schema = factory.getSchema(source, config); ``` Besides setting the locale and using the default resource bundle, you may also specify your own to cover any languages you -choose without adapting the library's source, or to override default messages. In doing so you however you should ensure that -your resource bundle covers all the keys defined by the default bundle. +choose without adapting the library's source, or to override default messages. In doing so you however you should ensure that your resource bundle covers all the keys defined by the default bundle. ``` -// Set the configuration with a custom resource bundle (you can create this before each validation) -ResourceBundle myBundle = ResourceBundle.getBundle("my-messages", myLocale); +// Set the configuration with a custom message source +MessageSource messageSource = new ResourceBundleMessageSource("my-messages"); SchemaValidatorsConfig config = new SchemaValidatorsConfig(); -config.setResourceBundle(myBundle); +config.setMessageSource(messageSource); JsonSchemaFactory factory = JsonSchemaFactory.getInstance(SpecVersion.VersionFlag.V201909); JsonSchema schema = factory.getSchema(source, config); ... -``` \ No newline at end of file +``` + +It is possible to override specific keys from the default resource bundle. Note however that you will need to supply all the languages for that specific key as it will not fallback on the default resource bundle. For instance the jsv-messages-override resource bundle will take precedence when resolving the message key. + +``` +// Set the configuration with a custom message source +MessageSource messageSource = new ResourceBundleMessageSource("jsv-messages-override", DefaultMessageSource.BUNDLE_BASE_NAME); +SchemaValidatorsConfig config = new SchemaValidatorsConfig(); +config.setMessageSource(messageSource); +JsonSchemaFactory factory = JsonSchemaFactory.getInstance(SpecVersion.VersionFlag.V201909); +JsonSchema schema = factory.getSchema(source, config); +... +``` + +The following approach can be used to determine the locale to use on a per user basis using a language tag priority list. + +``` +SchemaValidatorsConfig config = new SchemaValidatorsConfig(); +JsonSchemaFactory factory = JsonSchemaFactory.getInstance(SpecVersion.VersionFlag.V201909); +JsonSchema schema = factory.getSchema(source, config); + +// Uses the fr locale for this user +Locale locale = Locales.findSupported("it;q=0.9,fr;q=1.0"); +ExecutionContext executionContext = jsonSchema.createExecutionContext(); +executionContext.getExecutionConfig().setLocale(locale); +Set messages = jsonSchema.validate(executionContext, rootNode); + +// Uses the it locale for this user +locale = Locales.findSupported("it;q=1.0,fr;q=0.9"); +executionContext = jsonSchema.createExecutionContext(); +executionContext.getExecutionConfig().setLocale(locale); +messages = jsonSchema.validate(executionContext, rootNode); +... +``` diff --git a/src/main/java/com/networknt/schema/AdditionalPropertiesValidator.java b/src/main/java/com/networknt/schema/AdditionalPropertiesValidator.java index c124ea943..69660b659 100644 --- a/src/main/java/com/networknt/schema/AdditionalPropertiesValidator.java +++ b/src/main/java/com/networknt/schema/AdditionalPropertiesValidator.java @@ -97,7 +97,7 @@ public Set validate(ExecutionContext executionContext, JsonNo if (!allowedProperties.contains(pname) && !handledByPatternProperties) { if (!allowAdditionalProperties) { - errors.add(buildValidationMessage(at, pname)); + errors.add(buildValidationMessage(at, executionContext.getExecutionConfig().getLocale(), pname)); } else { if (additionalPropertiesSchema != null) { ValidatorState state = (ValidatorState) collectorContext.get(ValidatorState.VALIDATOR_STATE_KEY); diff --git a/src/main/java/com/networknt/schema/AnyOfValidator.java b/src/main/java/com/networknt/schema/AnyOfValidator.java index efd1850f7..ca63ebfcc 100644 --- a/src/main/java/com/networknt/schema/AnyOfValidator.java +++ b/src/main/java/com/networknt/schema/AnyOfValidator.java @@ -77,7 +77,8 @@ public Set validate(ExecutionContext executionContext, JsonNo //If schema has type validator and node type doesn't match with schemaType then ignore it //For union type, it is a must to call TypeValidator if (typeValidator.getSchemaType() != JsonType.UNION && !typeValidator.equalsToSchemaType(node)) { - allErrors.add(buildValidationMessage(at, typeValidator.getSchemaType().toString())); + allErrors.add(buildValidationMessage(at, executionContext.getExecutionConfig().getLocale(), + typeValidator.getSchemaType().toString())); continue; } } @@ -106,7 +107,8 @@ public Set validate(ExecutionContext executionContext, JsonNo if (this.discriminatorContext.isDiscriminatorMatchFound()) { if (!errors.isEmpty()) { allErrors.addAll(errors); - allErrors.add(buildValidationMessage(at, DISCRIMINATOR_REMARK)); + allErrors.add(buildValidationMessage(at, + executionContext.getExecutionConfig().getLocale(), DISCRIMINATOR_REMARK)); } else { // Clear all errors. allErrors.clear(); @@ -133,7 +135,8 @@ public Set validate(ExecutionContext executionContext, JsonNo if (this.validationContext.getConfig().isOpenAPI3StyleDiscriminators() && this.discriminatorContext.isActive()) { final Set errors = new HashSet<>(); - errors.add(buildValidationMessage(at, "based on the provided discriminator. No alternative could be chosen based on the discriminator property")); + errors.add(buildValidationMessage(at, executionContext.getExecutionConfig().getLocale(), + "based on the provided discriminator. No alternative could be chosen based on the discriminator property")); return Collections.unmodifiableSet(errors); } } finally { diff --git a/src/main/java/com/networknt/schema/BaseJsonValidator.java b/src/main/java/com/networknt/schema/BaseJsonValidator.java index 71a9992fb..478089911 100644 --- a/src/main/java/com/networknt/schema/BaseJsonValidator.java +++ b/src/main/java/com/networknt/schema/BaseJsonValidator.java @@ -19,6 +19,8 @@ import com.fasterxml.jackson.databind.JsonNode; import com.fasterxml.jackson.databind.node.ObjectNode; import com.networknt.schema.ValidationContext.DiscriminatorContext; +import com.networknt.schema.i18n.DefaultMessageSource; + import org.slf4j.Logger; import java.net.URI; @@ -47,7 +49,7 @@ public BaseJsonValidator(String schemaPath, ValidatorTypeCode validatorType, ValidationContext validationContext, boolean suppressSubSchemaRetrieval) { - super(validationContext != null && validationContext.getConfig() != null && validationContext.getConfig().isFailFast(), validatorType, validatorType != null ? validatorType.getCustomMessage() : null, (validationContext != null && validationContext.getConfig() != null) ? validationContext.getConfig().getResourceBundle() : I18nSupport.DEFAULT_RESOURCE_BUNDLE, validatorType, parentSchema, schemaPath); + super(validationContext != null && validationContext.getConfig() != null && validationContext.getConfig().isFailFast(), validatorType, validatorType != null ? validatorType.getCustomMessage() : null, (validationContext != null && validationContext.getConfig() != null) ? validationContext.getConfig().getMessageSource() : DefaultMessageSource.getInstance(), validatorType, parentSchema, schemaPath); this.schemaNode = schemaNode; this.suppressSubSchemaRetrieval = suppressSubSchemaRetrieval; this.applyDefaultsStrategy = (validationContext != null && validationContext.getConfig() != null && validationContext.getConfig().getApplyDefaultsStrategy() != null) ? validationContext.getConfig().getApplyDefaultsStrategy() : ApplyDefaultsStrategy.EMPTY_APPLY_DEFAULTS_STRATEGY; diff --git a/src/main/java/com/networknt/schema/ConstValidator.java b/src/main/java/com/networknt/schema/ConstValidator.java index 4e8c7196a..4c3aea4cf 100644 --- a/src/main/java/com/networknt/schema/ConstValidator.java +++ b/src/main/java/com/networknt/schema/ConstValidator.java @@ -20,7 +20,6 @@ import org.slf4j.LoggerFactory; import java.util.Collections; -import java.util.LinkedHashSet; import java.util.Set; public class ConstValidator extends BaseJsonValidator implements JsonValidator { @@ -35,14 +34,15 @@ public ConstValidator(String schemaPath, JsonNode schemaNode, JsonSchema parentS public Set validate(ExecutionContext executionContext, JsonNode node, JsonNode rootNode, String at) { debug(logger, node, rootNode, at); - Set errors = new LinkedHashSet(); if (schemaNode.isNumber() && node.isNumber()) { if (schemaNode.decimalValue().compareTo(node.decimalValue()) != 0) { - errors.add(buildValidationMessage(at, schemaNode.asText())); + return Collections.singleton(buildValidationMessage(at, + executionContext.getExecutionConfig().getLocale(), schemaNode.asText())); } } else if (!schemaNode.equals(node)) { - errors.add(buildValidationMessage(at, schemaNode.asText())); + return Collections.singleton( + buildValidationMessage(at, executionContext.getExecutionConfig().getLocale(), schemaNode.asText())); } - return Collections.unmodifiableSet(errors); + return Collections.emptySet(); } } diff --git a/src/main/java/com/networknt/schema/ContainsValidator.java b/src/main/java/com/networknt/schema/ContainsValidator.java index 737135627..abbae9818 100644 --- a/src/main/java/com/networknt/schema/ContainsValidator.java +++ b/src/main/java/com/networknt/schema/ContainsValidator.java @@ -23,6 +23,7 @@ import java.util.Collection; import java.util.Collections; +import java.util.Locale; import java.util.Optional; import java.util.Set; @@ -89,14 +90,16 @@ public Set validate(ExecutionContext executionContext, JsonNo if(isMinV201909) { updateValidatorType(ValidatorTypeCode.MIN_CONTAINS); } - return boundsViolated(isMinV201909 ? CONTAINS_MIN : ValidatorTypeCode.CONTAINS.getValue(), at, this.min); + return boundsViolated(isMinV201909 ? CONTAINS_MIN : ValidatorTypeCode.CONTAINS.getValue(), + executionContext.getExecutionConfig().getLocale(), at, this.min); } if (actual > this.max) { if(isMinV201909) { updateValidatorType(ValidatorTypeCode.MAX_CONTAINS); } - return boundsViolated(isMinV201909 ? CONTAINS_MAX : ValidatorTypeCode.CONTAINS.getValue(), at, this.max); + return boundsViolated(isMinV201909 ? CONTAINS_MAX : ValidatorTypeCode.CONTAINS.getValue(), + executionContext.getExecutionConfig().getLocale(), at, this.max); } } @@ -108,7 +111,7 @@ public void preloadJsonSchema() { Optional.ofNullable(this.schema).ifPresent(JsonSchema::initializeValidators); } - private Set boundsViolated(String messageKey, String at, int bounds) { - return Collections.singleton(constructValidationMessage(messageKey, at, String.valueOf(bounds), this.schema.getSchemaNode().toString())); + private Set boundsViolated(String messageKey, Locale locale, String at, int bounds) { + return Collections.singleton(buildValidationMessage(at, messageKey, locale, String.valueOf(bounds), this.schema.getSchemaNode().toString())); } } diff --git a/src/main/java/com/networknt/schema/DependenciesValidator.java b/src/main/java/com/networknt/schema/DependenciesValidator.java index 5f4d82657..379e1fc10 100644 --- a/src/main/java/com/networknt/schema/DependenciesValidator.java +++ b/src/main/java/com/networknt/schema/DependenciesValidator.java @@ -62,7 +62,8 @@ public Set validate(ExecutionContext executionContext, JsonNo if (deps != null && !deps.isEmpty()) { for (String field : deps) { if (node.get(field) == null) { - errors.add(buildValidationMessage(at, propertyDeps.toString())); + errors.add(buildValidationMessage(at, executionContext.getExecutionConfig().getLocale(), + propertyDeps.toString())); } } } diff --git a/src/main/java/com/networknt/schema/DependentRequired.java b/src/main/java/com/networknt/schema/DependentRequired.java index 197ddbffe..9aa6beb9f 100644 --- a/src/main/java/com/networknt/schema/DependentRequired.java +++ b/src/main/java/com/networknt/schema/DependentRequired.java @@ -56,7 +56,8 @@ public Set validate(ExecutionContext executionContext, JsonNo if (dependencies != null && !dependencies.isEmpty()) { for (String field : dependencies) { if (node.get(field) == null) { - errors.add(buildValidationMessage(at, field, pname)); + errors.add(buildValidationMessage(at, executionContext.getExecutionConfig().getLocale(), field, + pname)); } } } diff --git a/src/main/java/com/networknt/schema/EnumValidator.java b/src/main/java/com/networknt/schema/EnumValidator.java index 48d022650..a8b03d832 100644 --- a/src/main/java/com/networknt/schema/EnumValidator.java +++ b/src/main/java/com/networknt/schema/EnumValidator.java @@ -24,7 +24,6 @@ import java.util.Collections; import java.util.HashSet; -import java.util.LinkedHashSet; import java.util.Set; public class EnumValidator extends BaseJsonValidator implements JsonValidator { @@ -81,13 +80,12 @@ public EnumValidator(String schemaPath, JsonNode schemaNode, JsonSchema parentSc public Set validate(ExecutionContext executionContext, JsonNode node, JsonNode rootNode, String at) { debug(logger, node, rootNode, at); - Set errors = new LinkedHashSet(); if (node.isNumber()) node = DecimalNode.valueOf(node.decimalValue()); if (!nodes.contains(node) && !( this.validationContext.getConfig().isTypeLoose() && isTypeLooseContainsInEnum(node))) { - errors.add(buildValidationMessage(at, error)); + return Collections.singleton(buildValidationMessage(at, executionContext.getExecutionConfig().getLocale(), error)); } - return Collections.unmodifiableSet(errors); + return Collections.emptySet(); } /** diff --git a/src/main/java/com/networknt/schema/ExclusiveMaximumValidator.java b/src/main/java/com/networknt/schema/ExclusiveMaximumValidator.java index a1b8fe97b..a667d6a22 100644 --- a/src/main/java/com/networknt/schema/ExclusiveMaximumValidator.java +++ b/src/main/java/com/networknt/schema/ExclusiveMaximumValidator.java @@ -107,7 +107,8 @@ public Set validate(ExecutionContext executionContext, JsonNo } if (typedMaximum.crossesThreshold(node)) { - return Collections.singleton(buildValidationMessage(at, typedMaximum.thresholdValue())); + return Collections.singleton(buildValidationMessage(at, executionContext.getExecutionConfig().getLocale(), + typedMaximum.thresholdValue())); } return Collections.emptySet(); } diff --git a/src/main/java/com/networknt/schema/ExclusiveMinimumValidator.java b/src/main/java/com/networknt/schema/ExclusiveMinimumValidator.java index e7e8ca85b..c1c2fda58 100644 --- a/src/main/java/com/networknt/schema/ExclusiveMinimumValidator.java +++ b/src/main/java/com/networknt/schema/ExclusiveMinimumValidator.java @@ -114,7 +114,8 @@ public Set validate(ExecutionContext executionContext, JsonNo } if (typedMinimum.crossesThreshold(node)) { - return Collections.singleton(buildValidationMessage(at, typedMinimum.thresholdValue())); + return Collections.singleton(buildValidationMessage(at, executionContext.getExecutionConfig().getLocale(), + typedMinimum.thresholdValue())); } return Collections.emptySet(); } diff --git a/src/main/java/com/networknt/schema/ExecutionConfig.java b/src/main/java/com/networknt/schema/ExecutionConfig.java new file mode 100644 index 000000000..b3f1ca4e0 --- /dev/null +++ b/src/main/java/com/networknt/schema/ExecutionConfig.java @@ -0,0 +1,36 @@ +/* + * Copyright (c) 2023 the original author or authors. + * + * Licensed 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 com.networknt.schema; + +import java.util.Locale; +import java.util.Objects; + +/** + * Configuration per execution. + */ +public class ExecutionConfig { + private Locale locale = Locale.ROOT; + + public Locale getLocale() { + return locale; + } + + public void setLocale(Locale locale) { + this.locale = Objects.requireNonNull(locale, "Locale must not be null"); + } + +} diff --git a/src/main/java/com/networknt/schema/ExecutionContext.java b/src/main/java/com/networknt/schema/ExecutionContext.java index acbf69bcb..96d33fd0e 100644 --- a/src/main/java/com/networknt/schema/ExecutionContext.java +++ b/src/main/java/com/networknt/schema/ExecutionContext.java @@ -20,6 +20,7 @@ * Stores the execution context for the validation run. */ public class ExecutionContext { + private ExecutionConfig executionConfig; private CollectorContext collectorContext; public ExecutionContext() { @@ -27,7 +28,16 @@ public ExecutionContext() { } public ExecutionContext(CollectorContext collectorContext) { + this(new ExecutionConfig(), collectorContext); + } + + public ExecutionContext(ExecutionConfig executionConfig) { + this(executionConfig, new CollectorContext()); + } + + public ExecutionContext(ExecutionConfig executionConfig, CollectorContext collectorContext) { this.collectorContext = collectorContext; + this.executionConfig = executionConfig; } public CollectorContext getCollectorContext() { @@ -37,4 +47,12 @@ public CollectorContext getCollectorContext() { public void setCollectorContext(CollectorContext collectorContext) { this.collectorContext = collectorContext; } + + public ExecutionConfig getExecutionConfig() { + return executionConfig; + } + + public void setExecutionConfig(ExecutionConfig executionConfig) { + this.executionConfig = executionConfig; + } } diff --git a/src/main/java/com/networknt/schema/FalseValidator.java b/src/main/java/com/networknt/schema/FalseValidator.java index 8bde058ca..3eafa938f 100644 --- a/src/main/java/com/networknt/schema/FalseValidator.java +++ b/src/main/java/com/networknt/schema/FalseValidator.java @@ -20,7 +20,6 @@ import org.slf4j.LoggerFactory; import java.util.Collections; -import java.util.LinkedHashSet; import java.util.Set; public class FalseValidator extends BaseJsonValidator implements JsonValidator { @@ -33,8 +32,6 @@ public FalseValidator(String schemaPath, final JsonNode schemaNode, JsonSchema p public Set validate(ExecutionContext executionContext, JsonNode node, JsonNode rootNode, String at) { debug(logger, node, rootNode, at); // For the false validator, it is always not valid - Set errors = new LinkedHashSet(); - errors.add(buildValidationMessage(at)); - return Collections.unmodifiableSet(errors); + return Collections.singleton(buildValidationMessage(at, executionContext.getExecutionConfig().getLocale())); } } diff --git a/src/main/java/com/networknt/schema/FormatValidator.java b/src/main/java/com/networknt/schema/FormatValidator.java index 11851dbeb..667db95b6 100644 --- a/src/main/java/com/networknt/schema/FormatValidator.java +++ b/src/main/java/com/networknt/schema/FormatValidator.java @@ -40,26 +40,28 @@ public FormatValidator(String schemaPath, JsonNode schemaNode, JsonSchema parent public Set validate(ExecutionContext executionContext, JsonNode node, JsonNode rootNode, String at) { debug(logger, node, rootNode, at); - Set errors = new LinkedHashSet(); - JsonType nodeType = TypeFactory.getValueNodeType(node, this.validationContext.getConfig()); if (nodeType != JsonType.STRING) { - return errors; + return Collections.emptySet(); } + Set errors = new LinkedHashSet<>(); if (format != null) { if(format.getName().equals("ipv6")) { if(!node.textValue().trim().equals(node.textValue())) { // leading and trailing spaces - errors.add(buildValidationMessage(at, format.getName(), format.getErrorMessageDescription())); + errors.add(buildValidationMessage(at, executionContext.getExecutionConfig().getLocale(), + format.getName(), format.getErrorMessageDescription())); } else if(node.textValue().contains("%")) { // zone id is not part of the ipv6 - errors.add(buildValidationMessage(at, format.getName(), format.getErrorMessageDescription())); + errors.add(buildValidationMessage(at, executionContext.getExecutionConfig().getLocale(), + format.getName(), format.getErrorMessageDescription())); } } try { if (!format.matches(executionContext, node.textValue())) { - errors.add(buildValidationMessage(at, format.getName(), format.getErrorMessageDescription())); + errors.add(buildValidationMessage(at, executionContext.getExecutionConfig().getLocale(), + format.getName(), format.getErrorMessageDescription())); } } catch (PatternSyntaxException pse) { // String is considered valid if pattern is invalid diff --git a/src/main/java/com/networknt/schema/I18nSupport.java b/src/main/java/com/networknt/schema/I18nSupport.java index 1b05402c0..e77e07436 100644 --- a/src/main/java/com/networknt/schema/I18nSupport.java +++ b/src/main/java/com/networknt/schema/I18nSupport.java @@ -8,6 +8,7 @@ * * @author leaves chen leaves615@gmail.com */ +@Deprecated public class I18nSupport { public static final String DEFAULT_BUNDLE_BASE_NAME = "jsv-messages"; diff --git a/src/main/java/com/networknt/schema/ItemsValidator.java b/src/main/java/com/networknt/schema/ItemsValidator.java index 7ca1692cb..47c9fa338 100644 --- a/src/main/java/com/networknt/schema/ItemsValidator.java +++ b/src/main/java/com/networknt/schema/ItemsValidator.java @@ -127,7 +127,8 @@ private void doValidate(ExecutionContext executionContext, Set this.validationContext.getConfig().getMessageSource().getMessage( + ValidatorTypeCode.ID.getValue(), this.validationContext.getConfig().getLocale(), args)) + .build(); + throw new JsonSchemaException(validationMessage); } } } @@ -280,14 +280,15 @@ private Map read(JsonNode schemaNode) { if ("$recursiveAnchor".equals(pname)) { if (!nodeToUse.isBoolean()) { - throw new JsonSchemaException( - ValidationMessage.of( - "$recursiveAnchor", - CustomErrorMessageType.of("internal.invalidRecursiveAnchor"), - new MessageFormat("{0}: The value of a $recursiveAnchor must be a Boolean literal but is {1}"), - schemaPath, schemaPath, nodeToUse.getNodeType().toString() - ) - ); + ValidationMessage validationMessage = ValidationMessage.builder().type("$recursiveAnchor") + .code("internal.invalidRecursiveAnchor") + .message( + "{0}: The value of a $recursiveAnchor must be a Boolean literal but is {1}") + .path(schemaPath) + .schemaPath(schemaPath) + .arguments(nodeToUse.getNodeType().toString()) + .build(); + throw new JsonSchemaException(validationMessage); } this.dynamicAnchor = nodeToUse.booleanValue(); } @@ -647,6 +648,8 @@ public ExecutionContext createExecutionContext() { } CollectorContext collectorContext = new CollectorContext(config.isUnevaluatedItemsAnalysisDisabled(), config.isUnevaluatedPropertiesAnalysisDisabled()); - return new ExecutionContext(collectorContext); + ExecutionConfig executionConfig = new ExecutionConfig(); + executionConfig.setLocale(config.getLocale()); + return new ExecutionContext(executionConfig, collectorContext); } } diff --git a/src/main/java/com/networknt/schema/MaxItemsValidator.java b/src/main/java/com/networknt/schema/MaxItemsValidator.java index eac94cdcf..46ef19a47 100644 --- a/src/main/java/com/networknt/schema/MaxItemsValidator.java +++ b/src/main/java/com/networknt/schema/MaxItemsValidator.java @@ -44,11 +44,11 @@ public Set validate(ExecutionContext executionContext, JsonNo if (node.isArray()) { if (node.size() > max) { - return Collections.singleton(buildValidationMessage(at, "" + max)); + return Collections.singleton(buildValidationMessage(at, executionContext.getExecutionConfig().getLocale(), "" + max)); } } else if (this.validationContext.getConfig().isTypeLoose()) { if (1 > max) { - return Collections.singleton(buildValidationMessage(at, "" + max)); + return Collections.singleton(buildValidationMessage(at, executionContext.getExecutionConfig().getLocale(), "" + max)); } } diff --git a/src/main/java/com/networknt/schema/MaxLengthValidator.java b/src/main/java/com/networknt/schema/MaxLengthValidator.java index 3ae48cf29..3ed565e1b 100644 --- a/src/main/java/com/networknt/schema/MaxLengthValidator.java +++ b/src/main/java/com/networknt/schema/MaxLengthValidator.java @@ -47,7 +47,8 @@ public Set validate(ExecutionContext executionContext, JsonNo return Collections.emptySet(); } if (node.textValue().codePointCount(0, node.textValue().length()) > maxLength) { - return Collections.singleton(buildValidationMessage(at, "" + maxLength)); + return Collections.singleton( + buildValidationMessage(at, executionContext.getExecutionConfig().getLocale(), "" + maxLength)); } return Collections.emptySet(); } diff --git a/src/main/java/com/networknt/schema/MaxPropertiesValidator.java b/src/main/java/com/networknt/schema/MaxPropertiesValidator.java index cc39eb321..e41d7b0bb 100644 --- a/src/main/java/com/networknt/schema/MaxPropertiesValidator.java +++ b/src/main/java/com/networknt/schema/MaxPropertiesValidator.java @@ -43,7 +43,8 @@ public Set validate(ExecutionContext executionContext, JsonNo if (node.isObject()) { if (node.size() > max) { - return Collections.singleton(buildValidationMessage(at, "" + max)); + return Collections.singleton( + buildValidationMessage(at, executionContext.getExecutionConfig().getLocale(), "" + max)); } } diff --git a/src/main/java/com/networknt/schema/MaximumValidator.java b/src/main/java/com/networknt/schema/MaximumValidator.java index 1bb6b4819..a4d900bb0 100644 --- a/src/main/java/com/networknt/schema/MaximumValidator.java +++ b/src/main/java/com/networknt/schema/MaximumValidator.java @@ -116,7 +116,8 @@ public Set validate(ExecutionContext executionContext, JsonNo } if (typedMaximum.crossesThreshold(node)) { - return Collections.singleton(buildValidationMessage(at, typedMaximum.thresholdValue())); + return Collections.singleton(buildValidationMessage(at, executionContext.getExecutionConfig().getLocale(), + typedMaximum.thresholdValue())); } return Collections.emptySet(); } diff --git a/src/main/java/com/networknt/schema/MinItemsValidator.java b/src/main/java/com/networknt/schema/MinItemsValidator.java index 370348757..77263ae63 100644 --- a/src/main/java/com/networknt/schema/MinItemsValidator.java +++ b/src/main/java/com/networknt/schema/MinItemsValidator.java @@ -42,11 +42,13 @@ public Set validate(ExecutionContext executionContext, JsonNo if (node.isArray()) { if (node.size() < min) { - return Collections.singleton(buildValidationMessage(at, "" + min)); + return Collections.singleton( + buildValidationMessage(at, executionContext.getExecutionConfig().getLocale(), "" + min)); } } else if (this.validationContext.getConfig().isTypeLoose()) { if (1 < min) { - return Collections.singleton(buildValidationMessage(at, "" + min)); + return Collections.singleton( + buildValidationMessage(at, executionContext.getExecutionConfig().getLocale(), "" + min)); } } diff --git a/src/main/java/com/networknt/schema/MinLengthValidator.java b/src/main/java/com/networknt/schema/MinLengthValidator.java index 629a3db30..6ce311085 100644 --- a/src/main/java/com/networknt/schema/MinLengthValidator.java +++ b/src/main/java/com/networknt/schema/MinLengthValidator.java @@ -48,7 +48,8 @@ public Set validate(ExecutionContext executionContext, JsonNo } if (node.textValue().codePointCount(0, node.textValue().length()) < minLength) { - return Collections.singleton(buildValidationMessage(at, "" + minLength)); + return Collections.singleton( + buildValidationMessage(at, executionContext.getExecutionConfig().getLocale(), "" + minLength)); } return Collections.emptySet(); } diff --git a/src/main/java/com/networknt/schema/MinMaxContainsValidator.java b/src/main/java/com/networknt/schema/MinMaxContainsValidator.java index 5356e0ceb..aecc86d4e 100644 --- a/src/main/java/com/networknt/schema/MinMaxContainsValidator.java +++ b/src/main/java/com/networknt/schema/MinMaxContainsValidator.java @@ -2,8 +2,10 @@ import com.fasterxml.jackson.databind.JsonNode; +import java.util.Collections; import java.util.LinkedHashSet; import java.util.Set; +import java.util.stream.Collectors; /** * Tests the validity of {@literal maxContains} and {@literal minContains} in a schema. @@ -13,18 +15,23 @@ * and {@literal minContains} constraints exists within {@code ContainsValidator}. */ public class MinMaxContainsValidator extends BaseJsonValidator { - private final Set analysis = new LinkedHashSet<>(); + private final Set analysis; - public MinMaxContainsValidator(String schemaPath, JsonNode schemaNode, JsonSchema parentSchema, ValidationContext validationContext) { + public MinMaxContainsValidator(String schemaPath, JsonNode schemaNode, JsonSchema parentSchema, + ValidationContext validationContext) { super(schemaPath, schemaNode, parentSchema, ValidatorTypeCode.MAX_CONTAINS, validationContext); + Set analysis = null; int min = 1; int max = Integer.MAX_VALUE; JsonNode minNode = parentSchema.getSchemaNode().get("minContains"); if (null != minNode) { if (!minNode.isNumber() || !minNode.canConvertToExactIntegral() || minNode.intValue() < 0) { - report("minContains", schemaPath); + if (analysis == null) { + analysis = new LinkedHashSet<>(); + } + analysis.add(new Analysis("minContains", schemaPath)); } else { min = minNode.intValue(); } @@ -33,24 +40,49 @@ public MinMaxContainsValidator(String schemaPath, JsonNode schemaNode, JsonSchem JsonNode maxNode = parentSchema.getSchemaNode().get("maxContains"); if (null != maxNode) { if (!maxNode.isNumber() || !maxNode.canConvertToExactIntegral() || maxNode.intValue() < 0) { - report("maxContains", schemaPath); + if (analysis == null) { + analysis = new LinkedHashSet<>(); + } + analysis.add(new Analysis("maxContains", schemaPath)); } else { max = maxNode.intValue(); } } if (max < min) { - report("minContainsVsMaxContains", schemaPath); + if (analysis == null) { + analysis = new LinkedHashSet<>(); + } + analysis.add(new Analysis("minContainsVsMaxContains", schemaPath)); } - } - - private void report(String messageKey, String at) { - this.analysis.add(constructValidationMessage(messageKey, at, parentSchema.getSchemaNode().toString())); + this.analysis = analysis; } @Override - public Set validate(ExecutionContext executionContext, JsonNode node, JsonNode rootNode, String at) { - return this.analysis; + public Set validate(ExecutionContext executionContext, JsonNode node, JsonNode rootNode, + String at) { + return this.analysis != null ? this.analysis.stream() + .map(analysis -> buildValidationMessage(analysis.getAt(), analysis.getMessageKey(), + executionContext.getExecutionConfig().getLocale(), parentSchema.getSchemaNode().toString())) + .collect(Collectors.toCollection(LinkedHashSet::new)) : Collections.emptySet(); } + + public static class Analysis { + public String getMessageKey() { + return messageKey; + } + public String getAt() { + return at; + } + + private final String messageKey; + private final String at; + + public Analysis(String messageKey, String at) { + super(); + this.messageKey = messageKey; + this.at = at; + } + } } diff --git a/src/main/java/com/networknt/schema/MinPropertiesValidator.java b/src/main/java/com/networknt/schema/MinPropertiesValidator.java index ee963e4d2..61a6a491b 100644 --- a/src/main/java/com/networknt/schema/MinPropertiesValidator.java +++ b/src/main/java/com/networknt/schema/MinPropertiesValidator.java @@ -43,7 +43,8 @@ public Set validate(ExecutionContext executionContext, JsonNo if (node.isObject()) { if (node.size() < min) { - return Collections.singleton(buildValidationMessage(at, "" + min)); + return Collections.singleton( + buildValidationMessage(at, executionContext.getExecutionConfig().getLocale(), "" + min)); } } diff --git a/src/main/java/com/networknt/schema/MinimumValidator.java b/src/main/java/com/networknt/schema/MinimumValidator.java index b1e938b8d..33001c8e5 100644 --- a/src/main/java/com/networknt/schema/MinimumValidator.java +++ b/src/main/java/com/networknt/schema/MinimumValidator.java @@ -123,7 +123,8 @@ public Set validate(ExecutionContext executionContext, JsonNo } if (typedMinimum.crossesThreshold(node)) { - return Collections.singleton(buildValidationMessage(at, typedMinimum.thresholdValue())); + return Collections.singleton(buildValidationMessage(at, executionContext.getExecutionConfig().getLocale(), + typedMinimum.thresholdValue())); } return Collections.emptySet(); } diff --git a/src/main/java/com/networknt/schema/MultipleOfValidator.java b/src/main/java/com/networknt/schema/MultipleOfValidator.java index 15981b194..766a2c51a 100644 --- a/src/main/java/com/networknt/schema/MultipleOfValidator.java +++ b/src/main/java/com/networknt/schema/MultipleOfValidator.java @@ -49,7 +49,8 @@ public Set validate(ExecutionContext executionContext, JsonNo BigDecimal accurateDividend = node.isBigDecimal() ? node.decimalValue() : new BigDecimal(String.valueOf(nodeValue)); BigDecimal accurateDivisor = new BigDecimal(String.valueOf(divisor)); if (accurateDividend.divideAndRemainder(accurateDivisor)[1].abs().compareTo(BigDecimal.ZERO) > 0) { - return Collections.singleton(buildValidationMessage(at, "" + divisor)); + return Collections.singleton(buildValidationMessage(at, + executionContext.getExecutionConfig().getLocale(), "" + divisor)); } } } diff --git a/src/main/java/com/networknt/schema/NotAllowedValidator.java b/src/main/java/com/networknt/schema/NotAllowedValidator.java index 5b6d9250c..29e1e84b5 100644 --- a/src/main/java/com/networknt/schema/NotAllowedValidator.java +++ b/src/main/java/com/networknt/schema/NotAllowedValidator.java @@ -49,7 +49,7 @@ public Set validate(ExecutionContext executionContext, JsonNo JsonNode propertyNode = node.get(fieldName); if (propertyNode != null) { - errors.add(buildValidationMessage(at, fieldName)); + errors.add(buildValidationMessage(at, executionContext.getExecutionConfig().getLocale(), fieldName)); } } diff --git a/src/main/java/com/networknt/schema/NotValidator.java b/src/main/java/com/networknt/schema/NotValidator.java index 696cf49e8..7d38e9f1d 100644 --- a/src/main/java/com/networknt/schema/NotValidator.java +++ b/src/main/java/com/networknt/schema/NotValidator.java @@ -46,7 +46,8 @@ public Set validate(ExecutionContext executionContext, JsonNo debug(logger, node, rootNode, at); errors = this.schema.validate(executionContext, node, rootNode, at); if (errors.isEmpty()) { - return Collections.singleton(buildValidationMessage(at, this.schema.toString())); + return Collections.singleton(buildValidationMessage(at, + executionContext.getExecutionConfig().getLocale(), this.schema.toString())); } return Collections.emptySet(); } finally { @@ -65,7 +66,8 @@ public Set walk(ExecutionContext executionContext, JsonNode n Set errors = this.schema.walk(executionContext, node, rootNode, at, shouldValidateSchema); if (errors.isEmpty()) { - return Collections.singleton(buildValidationMessage(at, this.schema.toString())); + return Collections.singleton(buildValidationMessage(at, executionContext.getExecutionConfig().getLocale(), + this.schema.toString())); } return Collections.emptySet(); } diff --git a/src/main/java/com/networknt/schema/OneOfValidator.java b/src/main/java/com/networknt/schema/OneOfValidator.java index ccfe0a675..4aac96170 100644 --- a/src/main/java/com/networknt/schema/OneOfValidator.java +++ b/src/main/java/com/networknt/schema/OneOfValidator.java @@ -95,7 +95,8 @@ public Set validate(ExecutionContext executionContext, JsonNo // ensure there is always an "OneOf" error reported if number of valid schemas is not equal to 1. if (numberOfValidSchema != 1) { - ValidationMessage message = buildValidationMessage(at, Integer.toString(numberOfValidSchema)); + ValidationMessage message = buildValidationMessage(at, + executionContext.getExecutionConfig().getLocale(), Integer.toString(numberOfValidSchema)); if (this.failFast) { throw new JsonSchemaException(message); } diff --git a/src/main/java/com/networknt/schema/PatternValidator.java b/src/main/java/com/networknt/schema/PatternValidator.java index 628422808..f57a1aa5a 100644 --- a/src/main/java/com/networknt/schema/PatternValidator.java +++ b/src/main/java/com/networknt/schema/PatternValidator.java @@ -60,7 +60,8 @@ public Set validate(ExecutionContext executionContext, JsonNo try { if (!matches(node.asText())) { - return Collections.singleton(buildValidationMessage(at, this.pattern)); + return Collections.singleton( + buildValidationMessage(at, executionContext.getExecutionConfig().getLocale(), this.pattern)); } } catch (JsonSchemaException e) { throw e; diff --git a/src/main/java/com/networknt/schema/PropertyNamesValidator.java b/src/main/java/com/networknt/schema/PropertyNamesValidator.java index 40f750a4b..f1fb83f5d 100644 --- a/src/main/java/com/networknt/schema/PropertyNamesValidator.java +++ b/src/main/java/com/networknt/schema/PropertyNamesValidator.java @@ -48,7 +48,8 @@ public Set validate(ExecutionContext executionContext, JsonNo if (msg.startsWith(path)) msg = msg.substring(path.length()).replaceFirst("^:\\s*", ""); - errors.add(buildValidationMessage(schemaError.getPath(), msg)); + errors.add(buildValidationMessage(schemaError.getPath(), + executionContext.getExecutionConfig().getLocale(), msg)); } } return Collections.unmodifiableSet(errors); diff --git a/src/main/java/com/networknt/schema/ReadOnlyValidator.java b/src/main/java/com/networknt/schema/ReadOnlyValidator.java index 719c1632e..2b56e302e 100644 --- a/src/main/java/com/networknt/schema/ReadOnlyValidator.java +++ b/src/main/java/com/networknt/schema/ReadOnlyValidator.java @@ -16,7 +16,7 @@ package com.networknt.schema; -import java.util.HashSet; +import java.util.Collections; import java.util.Set; import org.slf4j.Logger; @@ -40,11 +40,10 @@ public ReadOnlyValidator(String schemaPath, JsonNode schemaNode, JsonSchema pare @Override public Set validate(ExecutionContext executionContext, JsonNode node, JsonNode rootNode, String at) { debug(logger, node, rootNode, at); - Set errors= new HashSet<>(); if (this.readOnly) { - errors.add(buildValidationMessage(at)); - } - return errors; + return Collections.singleton(buildValidationMessage(at, executionContext.getExecutionConfig().getLocale())); + } + return Collections.emptySet(); } } \ No newline at end of file diff --git a/src/main/java/com/networknt/schema/RecursiveRefValidator.java b/src/main/java/com/networknt/schema/RecursiveRefValidator.java index b43105d24..869daae1c 100644 --- a/src/main/java/com/networknt/schema/RecursiveRefValidator.java +++ b/src/main/java/com/networknt/schema/RecursiveRefValidator.java @@ -21,7 +21,6 @@ import org.slf4j.Logger; import org.slf4j.LoggerFactory; -import java.text.MessageFormat; import java.util.*; public class RecursiveRefValidator extends BaseJsonValidator { @@ -32,14 +31,11 @@ public RecursiveRefValidator(String schemaPath, JsonNode schemaNode, JsonSchema String refValue = schemaNode.asText(); if (!"#".equals(refValue)) { - throw new JsonSchemaException( - ValidationMessage.of( - ValidatorTypeCode.RECURSIVE_REF.getValue(), - CustomErrorMessageType.of("internal.invalidRecursiveRef"), - new MessageFormat("{0}: The value of a $recursiveRef must be '#' but is '{1}'"), - schemaPath, schemaPath, refValue - ) - ); + ValidationMessage validationMessage = ValidationMessage.builder() + .type(ValidatorTypeCode.RECURSIVE_REF.getValue()).code("internal.invalidRecursiveRef") + .message("{0}: The value of a $recursiveRef must be '#' but is '{1}'").path(schemaPath) + .schemaPath(schemaPath).arguments(refValue).build(); + throw new JsonSchemaException(validationMessage); } } diff --git a/src/main/java/com/networknt/schema/RefValidator.java b/src/main/java/com/networknt/schema/RefValidator.java index 83e8afe56..0c3175fd6 100644 --- a/src/main/java/com/networknt/schema/RefValidator.java +++ b/src/main/java/com/networknt/schema/RefValidator.java @@ -25,7 +25,6 @@ import org.slf4j.LoggerFactory; import java.net.URI; -import java.text.MessageFormat; import java.util.*; public class RefValidator extends BaseJsonValidator { @@ -44,12 +43,10 @@ public RefValidator(String schemaPath, JsonNode schemaNode, JsonSchema parentSch this.parentSchema = parentSchema; this.schema = getRefSchema(parentSchema, validationContext, refValue); if (this.schema == null) { - throw new JsonSchemaException( - ValidationMessage.of( - ValidatorTypeCode.REF.getValue(), - CustomErrorMessageType.of("internal.unresolvedRef"), - new MessageFormat("{0}: Reference {1} cannot be resolved"), - schemaPath, schemaPath, refValue)); + ValidationMessage validationMessage = ValidationMessage.builder().type(ValidatorTypeCode.REF.getValue()) + .code("internal.unresolvedRef").message("{0}: Reference {1} cannot be resolved") + .path(schemaPath).schemaPath(schemaPath).arguments(refValue).build(); + throw new JsonSchemaException(validationMessage); } } diff --git a/src/main/java/com/networknt/schema/RequiredValidator.java b/src/main/java/com/networknt/schema/RequiredValidator.java index 0a1cbebd7..3042acbe0 100644 --- a/src/main/java/com/networknt/schema/RequiredValidator.java +++ b/src/main/java/com/networknt/schema/RequiredValidator.java @@ -52,7 +52,7 @@ public Set validate(ExecutionContext executionContext, JsonNo JsonNode propertyNode = node.get(fieldName); if (propertyNode == null) { - errors.add(buildValidationMessage(at, fieldName)); + errors.add(buildValidationMessage(at, executionContext.getExecutionConfig().getLocale(), fieldName)); } } diff --git a/src/main/java/com/networknt/schema/SchemaValidatorsConfig.java b/src/main/java/com/networknt/schema/SchemaValidatorsConfig.java index 24e42b5c5..c6a167b47 100644 --- a/src/main/java/com/networknt/schema/SchemaValidatorsConfig.java +++ b/src/main/java/com/networknt/schema/SchemaValidatorsConfig.java @@ -17,11 +17,18 @@ package com.networknt.schema; import com.fasterxml.jackson.databind.JsonNode; +import com.networknt.schema.i18n.DefaultMessageSource; +import com.networknt.schema.i18n.MessageSource; import com.networknt.schema.uri.URITranslator; import com.networknt.schema.uri.URITranslator.CompositeURITranslator; import com.networknt.schema.walk.JsonSchemaWalkListener; -import java.util.*; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Locale; +import java.util.Map; +import java.util.Objects; import java.util.function.Supplier; public class SchemaValidatorsConfig { @@ -138,11 +145,9 @@ public class SchemaValidatorsConfig { private Locale locale; /** - * An alternative resource bundle to consider instead of the default one when producing validation messages. If this - * is provided the 'locale' is ignored given that the resource bundle is already loaded for a specific locale. + * The message source to use for generating localised messages. */ - private ResourceBundle resourceBundle; - private ResourceBundle resourceBundleToUse; + private MessageSource messageSource; /************************ START OF UNEVALUATED CHECKS **********************************/ @@ -215,7 +220,7 @@ public void setTypeLoose(boolean typeLoose) { /** * When enabled, {@link JsonValidator#validate(ExecutionContext, JsonNode, JsonNode, String)} or * {@link JsonValidator#validate(ExecutionContext, JsonNode)} doesn't return any - * {@link Set}<{@link ValidationMessage}>, instead a + * {@link java.util.Set}<{@link ValidationMessage}>, instead a * {@link JsonSchemaException} is thrown as soon as a validation errors is * discovered. * @@ -520,58 +525,53 @@ public void setStrict(String keyword, boolean strict) { } /** - * Get the locale to consider when generating localised messages (default is the JVM default). + * Get the locale to consider when generating localised messages (default is the + * JVM default). + *

+ * This locale is on a schema basis and will be used as the default locale for + * {@link com.networknt.schema.ExecutionConfig}. * * @return The locale. */ public Locale getLocale() { + if (this.locale == null) { + // This should not be cached as it can be changed using Locale#setDefault(Locale) + return Locale.getDefault(); + } return this.locale; } /** - * Get the locale to consider when generating localised messages. + * Set the locale to consider when generating localised messages. + *

+ * Note that this locale is set on a schema basis. To configure the schema on a + * per execution basis use + * {@link com.networknt.schema.ExecutionConfig#setLocale(Locale)}. * * @param locale The locale. */ public void setLocale(Locale locale) { this.locale = locale; - if (this.locale == null || (this.resourceBundleToUse != null && !this.locale.equals(this.resourceBundleToUse.getLocale()))) { - // If we have already loaded a resource bundle for a different locale set it to null so that it is reinitialised. - this.resourceBundleToUse = null; - } } /** - * Get the resource bundle to use when generating localised messages. - * - * @return The resource bundle. - */ - public ResourceBundle getResourceBundle() { - if (this.resourceBundleToUse == null) { - // Load and cache the resource bundle to use. - this.resourceBundleToUse = this.resourceBundle; - if (this.resourceBundleToUse == null) { - if (this.locale == null) { - this.resourceBundleToUse = I18nSupport.DEFAULT_RESOURCE_BUNDLE; - } else { - this.resourceBundleToUse = ResourceBundle.getBundle(I18nSupport.DEFAULT_BUNDLE_BASE_NAME, this.locale); - } - } + * Get the message source to use for generating localised messages. + * + * @return the message source + */ + public MessageSource getMessageSource() { + if (this.messageSource == null) { + return DefaultMessageSource.getInstance(); } - return this.resourceBundleToUse; + return this.messageSource; } /** - * Set the resource bundle to use when generating localised messages. - * - * @param resourceBundle The resource bundle. + * Set the message source to use for generating localised messages. + * + * @param messageSource the message source */ - public void setResourceBundle(ResourceBundle resourceBundle) { - this.resourceBundle = resourceBundle; - if (this.resourceBundle == null || !(this.resourceBundle.equals(this.resourceBundleToUse))) { - // If we have already loaded a different resource bundle set it to null so that it is reinitialised. - this.resourceBundleToUse = null; - } + public void setMessageSource(MessageSource messageSource) { + this.messageSource = messageSource; } - } diff --git a/src/main/java/com/networknt/schema/TypeValidator.java b/src/main/java/com/networknt/schema/TypeValidator.java index 1a2b9f8af..707be8421 100644 --- a/src/main/java/com/networknt/schema/TypeValidator.java +++ b/src/main/java/com/networknt/schema/TypeValidator.java @@ -60,7 +60,8 @@ public Set validate(ExecutionContext executionContext, JsonNo if (!equalsToSchemaType(node)) { JsonType nodeType = TypeFactory.getValueNodeType(node, this.validationContext.getConfig()); - return Collections.singleton(buildValidationMessage(at, nodeType.toString(), this.schemaType.toString())); + return Collections.singleton(buildValidationMessage(at, executionContext.getExecutionConfig().getLocale(), + nodeType.toString(), this.schemaType.toString())); } // TODO: Is this really necessary? diff --git a/src/main/java/com/networknt/schema/UnevaluatedItemsValidator.java b/src/main/java/com/networknt/schema/UnevaluatedItemsValidator.java index 3d55421c4..1b00d3fe5 100644 --- a/src/main/java/com/networknt/schema/UnevaluatedItemsValidator.java +++ b/src/main/java/com/networknt/schema/UnevaluatedItemsValidator.java @@ -60,7 +60,7 @@ public Set validate(ExecutionContext executionContext, JsonNo // Short-circuit since schema is 'false' if (super.schemaNode.isBoolean() && !super.schemaNode.asBoolean() && !unevaluatedPaths.isEmpty()) { - return reportUnevaluatedPaths(unevaluatedPaths); + return reportUnevaluatedPaths(unevaluatedPaths, executionContext); } Set failingPaths = new HashSet<>(); @@ -75,7 +75,7 @@ public Set validate(ExecutionContext executionContext, JsonNo if (failingPaths.isEmpty()) { collectorContext.getEvaluatedItems().addAll(allPaths); } else { - return reportUnevaluatedPaths(failingPaths); + return reportUnevaluatedPaths(failingPaths, executionContext); } return Collections.emptySet(); @@ -86,7 +86,7 @@ public Set validate(ExecutionContext executionContext, JsonNo private Set allPaths(JsonNode node, String at) { PathType pathType = getPathType(); - Set collector = new HashSet<>(); + Set collector = new LinkedHashSet<>(); int size = node.size(); for (int i = 0; i < size; ++i) { String path = pathType.append(at, i); @@ -95,10 +95,10 @@ private Set allPaths(JsonNode node, String at) { return collector; } - private Set reportUnevaluatedPaths(Set unevaluatedPaths) { + private Set reportUnevaluatedPaths(Set unevaluatedPaths, ExecutionContext executionContext) { List paths = new ArrayList<>(unevaluatedPaths); paths.sort(String.CASE_INSENSITIVE_ORDER); - return Collections.singleton(buildValidationMessage(String.join("\n ", paths))); + return Collections.singleton(buildValidationMessage(String.join("\n ", paths), executionContext.getExecutionConfig().getLocale())); } private static Set unevaluatedPaths(CollectorContext collectorContext, Set allPaths) { diff --git a/src/main/java/com/networknt/schema/UnevaluatedPropertiesValidator.java b/src/main/java/com/networknt/schema/UnevaluatedPropertiesValidator.java index 3b0d04487..ddcf0c1af 100644 --- a/src/main/java/com/networknt/schema/UnevaluatedPropertiesValidator.java +++ b/src/main/java/com/networknt/schema/UnevaluatedPropertiesValidator.java @@ -60,7 +60,7 @@ public Set validate(ExecutionContext executionContext, JsonNo // Short-circuit since schema is 'false' if (super.schemaNode.isBoolean() && !super.schemaNode.asBoolean() && !unevaluatedPaths.isEmpty()) { - return reportUnevaluatedPaths(unevaluatedPaths); + return reportUnevaluatedPaths(unevaluatedPaths, executionContext); } Set failingPaths = new HashSet<>(); @@ -75,7 +75,7 @@ public Set validate(ExecutionContext executionContext, JsonNo if (failingPaths.isEmpty()) { collectorContext.getEvaluatedProperties().addAll(allPaths); } else { - return reportUnevaluatedPaths(failingPaths); + return reportUnevaluatedPaths(failingPaths, executionContext); } return Collections.emptySet(); @@ -94,10 +94,10 @@ private Set allPaths(JsonNode node, String at) { return collector; } - private Set reportUnevaluatedPaths(Set unevaluatedPaths) { + private Set reportUnevaluatedPaths(Set unevaluatedPaths, ExecutionContext executionContext) { List paths = new ArrayList<>(unevaluatedPaths); paths.sort(String.CASE_INSENSITIVE_ORDER); - return Collections.singleton(buildValidationMessage(String.join("\n ", paths))); + return Collections.singleton(buildValidationMessage(String.join("\n ", paths), executionContext.getExecutionConfig().getLocale())); } private static Set unevaluatedPaths(CollectorContext collectorContext, Set allPaths) { diff --git a/src/main/java/com/networknt/schema/UnionTypeValidator.java b/src/main/java/com/networknt/schema/UnionTypeValidator.java index 07d1704a3..7ee38be0f 100644 --- a/src/main/java/com/networknt/schema/UnionTypeValidator.java +++ b/src/main/java/com/networknt/schema/UnionTypeValidator.java @@ -78,7 +78,8 @@ public Set validate(ExecutionContext executionContext, JsonNo } if (!valid) { - return Collections.singleton(buildValidationMessage(at, nodeType.toString(), error)); + return Collections.singleton(buildValidationMessage(at, executionContext.getExecutionConfig().getLocale(), + nodeType.toString(), error)); } return Collections.emptySet(); diff --git a/src/main/java/com/networknt/schema/UniqueItemsValidator.java b/src/main/java/com/networknt/schema/UniqueItemsValidator.java index e84567342..5ad3efec5 100644 --- a/src/main/java/com/networknt/schema/UniqueItemsValidator.java +++ b/src/main/java/com/networknt/schema/UniqueItemsValidator.java @@ -45,7 +45,7 @@ public Set validate(ExecutionContext executionContext, JsonNo Set set = new HashSet(); for (JsonNode n : node) { if (!set.add(n)) { - return Collections.singleton(buildValidationMessage(at)); + return Collections.singleton(buildValidationMessage(at, executionContext.getExecutionConfig().getLocale())); } } } diff --git a/src/main/java/com/networknt/schema/ValidationMessage.java b/src/main/java/com/networknt/schema/ValidationMessage.java index 7dbe712aa..004b165e9 100644 --- a/src/main/java/com/networknt/schema/ValidationMessage.java +++ b/src/main/java/com/networknt/schema/ValidationMessage.java @@ -16,32 +16,42 @@ package com.networknt.schema; +import com.networknt.schema.i18n.MessageFormatter; +import com.networknt.schema.utils.CachingSupplier; import com.networknt.schema.utils.StringUtils; import java.text.MessageFormat; import java.util.Arrays; import java.util.Map; +import java.util.function.Supplier; public class ValidationMessage { - private String type; - private String code; - private String path; - private String schemaPath; - private String[] arguments; - private Map details; - private String message; - - ValidationMessage() { + private final String type; + private final String code; + private final String path; + private final String schemaPath; + private final Object[] arguments; + private final Map details; + private final String messageKey; + private final Supplier messageSupplier; + + ValidationMessage(String type, String code, String path, String schemaPath, Object[] arguments, + Map details, String messageKey, Supplier messageSupplier) { + super(); + this.type = type; + this.code = code; + this.path = path; + this.schemaPath = schemaPath; + this.arguments = arguments; + this.details = details; + this.messageKey = messageKey; + this.messageSupplier = messageSupplier; } public String getCode() { return code; } - void setCode(String code) { - this.code = code; - } - /** * @return The path to the input json */ @@ -49,10 +59,6 @@ public String getPath() { return path; } - void setPath(String path) { - this.path = path; - } - /** * @return The path to the schema */ @@ -60,37 +66,25 @@ public String getSchemaPath() { return schemaPath; } - public void setSchemaPath(String schemaPath) { - this.schemaPath = schemaPath; - } - - public String[] getArguments() { + public Object[] getArguments() { return arguments; } - void setArguments(String[] arguments) { - this.arguments = arguments; - } - - void setDetails(Map details) { - this.details = details; - } - public Map getDetails() { return details; } public String getMessage() { - return message; + return messageSupplier.get(); } - void setMessage(String message) { - this.message = message; + public String getMessageKey() { + return messageKey; } @Override public String toString() { - return message; + return messageSupplier.get(); } @Override @@ -105,9 +99,9 @@ public boolean equals(Object o) { if (path != null ? !path.equals(that.path) : that.path != null) return false; if (schemaPath != null ? !schemaPath.equals(that.schemaPath) : that.schemaPath != null) return false; if (details != null ? !details.equals(that.details) : that.details != null) return false; + if (messageKey != null ? !messageKey.equals(that.messageKey) : that.messageKey != null) return false; if (!Arrays.equals(arguments, that.arguments)) return false; - return !(message != null ? !message.equals(that.message) : that.message != null); - + return true; } @Override @@ -118,7 +112,7 @@ public int hashCode() { result = 31 * result + (schemaPath != null ? schemaPath.hashCode() : 0); result = 31 * result + (details != null ? details.hashCode() : 0); result = 31 * result + (arguments != null ? Arrays.hashCode(arguments) : 0); - result = 31 * result + (message != null ? message.hashCode() : 0); + result = 31 * result + (messageKey != null ? messageKey.hashCode() : 0); return result; } @@ -126,38 +120,44 @@ public String getType() { return type; } - public void setType(String type) { - this.type = type; - } - - public static ValidationMessage ofWithCustom(String type, ErrorMessageType errorMessageType, MessageFormat messageFormat, String customMessage, String at, String schemaPath, String... arguments) { + @Deprecated // Use the builder + public static ValidationMessage ofWithCustom(String type, ErrorMessageType errorMessageType, MessageFormat messageFormat, String customMessage, String at, String schemaPath, Object... arguments) { ValidationMessage.Builder builder = new ValidationMessage.Builder(); builder.code(errorMessageType.getErrorCode()).path(at).schemaPath(schemaPath).arguments(arguments) .format(messageFormat).type(type) - .customMessage(customMessage); + .message(customMessage); return builder.build(); } - public static ValidationMessage of(String type, ErrorMessageType errorMessageType, MessageFormat messageFormat, String at, String schemaPath, String... arguments) { + @Deprecated // Use the builder + public static ValidationMessage of(String type, ErrorMessageType errorMessageType, MessageFormat messageFormat, String at, String schemaPath, Object... arguments) { return ofWithCustom(type, errorMessageType, messageFormat, errorMessageType.getCustomMessage(), at, schemaPath, arguments); } + @Deprecated // Use the builder public static ValidationMessage of(String type, ErrorMessageType errorMessageType, MessageFormat messageFormat, String at, String schemaPath, Map details) { ValidationMessage.Builder builder = new ValidationMessage.Builder(); builder.code(errorMessageType.getErrorCode()).path(at).schemaPath(schemaPath).details(details) .format(messageFormat).type(type); return builder.build(); } + + public static Builder builder() { + return new Builder(); + } public static class Builder { private String type; private String code; private String path; private String schemaPath; - private String[] arguments; + private Object[] arguments; private Map details; private MessageFormat format; - private String customMessage; + private String message; + private Supplier messageSupplier; + private MessageFormatter messageFormatter; + private String messageKey; public Builder type(String type) { this.type = type; @@ -179,7 +179,7 @@ public Builder schemaPath(String schemaPath) { return this; } - public Builder arguments(String... arguments) { + public Builder arguments(Object... arguments) { this.arguments = arguments; return this; } @@ -194,37 +194,69 @@ public Builder format(MessageFormat format) { return this; } - public Builder customMessage(String customMessage) { - this.customMessage = customMessage; + @Deprecated + public Builder customMessage(String message) { + return message(message); + } + + /** + * Explicitly sets the message pattern to be used. + *

+ * If set the message supplier and message formatter will be ignored. + * + * @param message the message pattern + * @return the builder + */ + public Builder message(String message) { + this.message = message; + return this; + } + + public Builder messageSupplier(Supplier messageSupplier) { + this.messageSupplier = messageSupplier; + return this; + } + + public Builder messageFormatter(MessageFormatter messageFormatter) { + this.messageFormatter = messageFormatter; + return this; + } + + public Builder messageKey(String messageKey) { + this.messageKey = messageKey; return this; } public ValidationMessage build() { - ValidationMessage msg = new ValidationMessage(); - msg.setType(type); - msg.setCode(code); - msg.setPath(path); - msg.setSchemaPath(schemaPath); - msg.setArguments(arguments); - msg.setDetails(details); - - if (format != null) { - String[] objs = new String[(arguments == null ? 0 : arguments.length) + 1]; - objs[0] = path; - if (arguments != null) { - for (int i = 1; i < objs.length; i++) { - objs[i] = arguments[i - 1]; - } - } - if(StringUtils.isNotBlank(customMessage)) { - msg.setMessage(customMessage); + Supplier messageSupplier = this.messageSupplier; + String messageKey = this.messageKey; + + if (StringUtils.isNotBlank(this.message)) { + messageKey = this.message; + if (this.message.contains("{")) { + Object[] objs = getArguments(); + MessageFormat format = new MessageFormat(this.message); + messageSupplier = new CachingSupplier<>(() -> format.format(objs)); } else { - msg.setMessage(format.format(objs)); + messageSupplier = message::toString; } - + } else if (messageSupplier == null) { + Object[] objs = getArguments(); + MessageFormatter formatter = this.messageFormatter != null ? this.messageFormatter : format::format; + messageSupplier = new CachingSupplier<>(() -> formatter.format(objs)); } - - return msg; + return new ValidationMessage(type, code, path, schemaPath, arguments, details, messageKey, messageSupplier); + } + + private Object[] getArguments() { + Object[] objs = new Object[(arguments == null ? 0 : arguments.length) + 1]; + objs[0] = path; + if (arguments != null) { + for (int i = 1; i < objs.length; i++) { + objs[i] = arguments[i - 1]; + } + } + return objs; } } } diff --git a/src/main/java/com/networknt/schema/ValidationMessageHandler.java b/src/main/java/com/networknt/schema/ValidationMessageHandler.java index 79f240889..f08ea25b8 100644 --- a/src/main/java/com/networknt/schema/ValidationMessageHandler.java +++ b/src/main/java/com/networknt/schema/ValidationMessageHandler.java @@ -1,15 +1,15 @@ package com.networknt.schema; import com.fasterxml.jackson.databind.JsonNode; +import com.networknt.schema.i18n.MessageSource; import com.networknt.schema.utils.StringUtils; -import java.text.MessageFormat; -import java.util.ResourceBundle; +import java.util.Locale; public abstract class ValidationMessageHandler { protected final boolean failFast; protected final String customMessage; - protected final ResourceBundle resourceBundle; + protected final MessageSource messageSource; protected ValidatorTypeCode validatorType; protected ErrorMessageType errorMessageType; @@ -17,36 +17,30 @@ public abstract class ValidationMessageHandler { protected JsonSchema parentSchema; - protected ValidationMessageHandler(boolean failFast, ErrorMessageType errorMessageType, String customMessage, ResourceBundle resourceBundle, ValidatorTypeCode validatorType, JsonSchema parentSchema, String schemaPath) { + protected ValidationMessageHandler(boolean failFast, ErrorMessageType errorMessageType, String customMessage, MessageSource messageSource, ValidatorTypeCode validatorType, JsonSchema parentSchema, String schemaPath) { this.failFast = failFast; this.errorMessageType = errorMessageType; this.customMessage = customMessage; - this.resourceBundle = resourceBundle; + this.messageSource = messageSource; this.validatorType = validatorType; this.schemaPath = schemaPath; this.parentSchema = parentSchema; } - - protected ValidationMessage buildValidationMessage(String at, String... arguments) { - MessageFormat messageFormat = new MessageFormat(this.resourceBundle.getString(getErrorMessageType().getErrorCodeValue())); - final ValidationMessage message = ValidationMessage.ofWithCustom(getValidatorType().getValue(), getErrorMessageType(), messageFormat, this.customMessage, at, this.schemaPath, arguments); - if (this.failFast && isApplicator()) { - throw new JsonSchemaException(message); - } - return message; + protected ValidationMessage buildValidationMessage(String at, Locale locale, Object... arguments) { + return buildValidationMessage(at, getErrorMessageType().getErrorCodeValue(), locale, arguments); } - protected ValidationMessage constructValidationMessage(String messageKey, String at, String... arguments) { - MessageFormat messageFormat = new MessageFormat(this.resourceBundle.getString(messageKey)); - final ValidationMessage message = new ValidationMessage.Builder() + protected ValidationMessage buildValidationMessage(String at, String messageKey, Locale locale, Object... arguments) { + final ValidationMessage message = ValidationMessage.builder() .code(getErrorMessageType().getErrorCode()) .path(at) .schemaPath(this.schemaPath) .arguments(arguments) - .format(messageFormat) + .messageKey(messageKey) + .messageFormatter(args -> this.messageSource.getMessage(messageKey, locale, args)) .type(getValidatorType().getValue()) - .customMessage(this.customMessage) + .message(this.customMessage) .build(); if (this.failFast && isApplicator()) { throw new JsonSchemaException(message); diff --git a/src/main/java/com/networknt/schema/WriteOnlyValidator.java b/src/main/java/com/networknt/schema/WriteOnlyValidator.java index 4dcd63872..aced43aba 100644 --- a/src/main/java/com/networknt/schema/WriteOnlyValidator.java +++ b/src/main/java/com/networknt/schema/WriteOnlyValidator.java @@ -1,6 +1,6 @@ package com.networknt.schema; -import java.util.HashSet; +import java.util.Collections; import java.util.Set; import org.slf4j.Logger; @@ -24,11 +24,10 @@ public WriteOnlyValidator(String schemaPath, JsonNode schemaNode, JsonSchema par @Override public Set validate(ExecutionContext executionContext, JsonNode node, JsonNode rootNode, String at) { debug(logger, node, rootNode, at); - Set errors= new HashSet<>(); if (this.writeOnly) { - errors.add(buildValidationMessage(at)); + return Collections.singleton(buildValidationMessage(at, executionContext.getExecutionConfig().getLocale())); } - return errors; + return Collections.emptySet(); } } diff --git a/src/main/java/com/networknt/schema/format/DateTimeValidator.java b/src/main/java/com/networknt/schema/format/DateTimeValidator.java index cc4fd7d0f..670cf3e6e 100644 --- a/src/main/java/com/networknt/schema/format/DateTimeValidator.java +++ b/src/main/java/com/networknt/schema/format/DateTimeValidator.java @@ -32,7 +32,6 @@ import org.slf4j.LoggerFactory; import java.util.Collections; -import java.util.LinkedHashSet; import java.util.Set; public class DateTimeValidator extends BaseJsonValidator { @@ -50,16 +49,15 @@ public DateTimeValidator(String schemaPath, JsonNode schemaNode, JsonSchema pare public Set validate(ExecutionContext executionContext, JsonNode node, JsonNode rootNode, String at) { debug(logger, node, rootNode, at); - Set errors = new LinkedHashSet<>(); - JsonType nodeType = TypeFactory.getValueNodeType(node, this.validationContext.getConfig()); if (nodeType != JsonType.STRING) { - return errors; + return Collections.emptySet(); } if (!isLegalDateTime(node.textValue())) { - errors.add(buildValidationMessage(at, node.textValue(), DATETIME)); + return Collections.singleton(buildValidationMessage(at, executionContext.getExecutionConfig().getLocale(), node.textValue(), + DATETIME)); } - return Collections.unmodifiableSet(errors); + return Collections.emptySet(); } private static boolean isLegalDateTime(String string) { diff --git a/src/main/java/com/networknt/schema/i18n/DefaultMessageSource.java b/src/main/java/com/networknt/schema/i18n/DefaultMessageSource.java new file mode 100644 index 000000000..79acb5b53 --- /dev/null +++ b/src/main/java/com/networknt/schema/i18n/DefaultMessageSource.java @@ -0,0 +1,36 @@ +/* + * Copyright (c) 2023 the original author or authors. + * + * Licensed 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 com.networknt.schema.i18n; + +/** + * The default {@link MessageSource} singleton. + */ +public class DefaultMessageSource { + public static final String BUNDLE_BASE_NAME = "jsv-messages"; + + public static class Holder { + private static final MessageSource INSTANCE = new ResourceBundleMessageSource(BUNDLE_BASE_NAME); + } + + /** + * Gets the default {@link MessageSource} using the jsv-messages bundle. + * + * @return the message source of the resource bundle + */ + public static MessageSource getInstance() { + return Holder.INSTANCE; + } +} diff --git a/src/main/java/com/networknt/schema/i18n/MessageFormatter.java b/src/main/java/com/networknt/schema/i18n/MessageFormatter.java new file mode 100644 index 000000000..03e4a7905 --- /dev/null +++ b/src/main/java/com/networknt/schema/i18n/MessageFormatter.java @@ -0,0 +1,30 @@ +/* + * Copyright (c) 2023 the original author or authors. + * + * Licensed 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 com.networknt.schema.i18n; + +/** + * Formats messages with arguments. + */ +@FunctionalInterface +public interface MessageFormatter { + /** + * Formats a message with arguments. + * + * @param args the arguments + * @return the message + */ + String format(Object... args); +} diff --git a/src/main/java/com/networknt/schema/utils/CachingSupplier.java b/src/main/java/com/networknt/schema/utils/CachingSupplier.java new file mode 100644 index 000000000..0f43f9478 --- /dev/null +++ b/src/main/java/com/networknt/schema/utils/CachingSupplier.java @@ -0,0 +1,43 @@ +/* + * Copyright (c) 2023 the original author or authors. + * + * Licensed 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 com.networknt.schema.utils; + +import java.util.function.Supplier; + +/** + * {@link Supplier} that caches the value. + *

+ * This is not threadsafe. + * + * @param the type of results supplied by this supplier + */ +public class CachingSupplier implements Supplier { + private final Supplier delegate; + private T cached = null; + + public CachingSupplier(Supplier supplier) { + this.delegate = supplier; + } + + @Override + public T get() { + if (this.cached == null) { + this.cached = this.delegate.get(); + } + return this.cached; + } +} diff --git a/src/test/java/com/networknt/schema/CustomMetaSchemaTest.java b/src/test/java/com/networknt/schema/CustomMetaSchemaTest.java index e900fe3c1..e8411daa1 100644 --- a/src/test/java/com/networknt/schema/CustomMetaSchemaTest.java +++ b/src/test/java/com/networknt/schema/CustomMetaSchemaTest.java @@ -68,7 +68,10 @@ public Set validate(ExecutionContext executionContext, JsonNo } String valueName = enumNames.get(idx); Set messages = new HashSet<>(); - messages.add(ValidationMessage.of(keyword, CustomErrorMessageType.of("tests.example.enumNames"), new MessageFormat("{0}: enumName is {1}"), at, null, valueName)); + ValidationMessage validationMessage = ValidationMessage.builder().type(keyword) + .code("tests.example.enumNames").message("{0}: enumName is {1}").path(at) + .arguments(valueName).build(); + messages.add(validationMessage); return messages; } } diff --git a/src/test/java/com/networknt/schema/Issue686Test.java b/src/test/java/com/networknt/schema/Issue686Test.java index dcdef7c65..0576a7b76 100644 --- a/src/test/java/com/networknt/schema/Issue686Test.java +++ b/src/test/java/com/networknt/schema/Issue686Test.java @@ -2,6 +2,9 @@ import com.fasterxml.jackson.core.JsonProcessingException; import com.fasterxml.jackson.databind.ObjectMapper; +import com.networknt.schema.i18n.DefaultMessageSource; +import com.networknt.schema.i18n.ResourceBundleMessageSource; + import org.junit.jupiter.api.Test; import java.text.MessageFormat; @@ -16,37 +19,14 @@ public class Issue686Test { @Test void testDefaults() { SchemaValidatorsConfig config = new SchemaValidatorsConfig(); - assertEquals(I18nSupport.DEFAULT_RESOURCE_BUNDLE, config.getResourceBundle()); - } - - @Test - void testCustomLocale() { - SchemaValidatorsConfig config = new SchemaValidatorsConfig(); - config.setLocale(Locale.FRENCH); - assertEquals(Locale.FRENCH.getLanguage(), config.getResourceBundle().getLocale().getLanguage()); - } - - @Test - void testLocaleDoesNotOverrideCustomResourceBundle() { - SchemaValidatorsConfig config = new SchemaValidatorsConfig(); - config.setLocale(Locale.FRENCH); - ResourceBundle bundle = ResourceBundle.getBundle("issue686/translations", Locale.GERMAN); - assertEquals(Locale.GERMAN.getLanguage(), bundle.getLocale().getLanguage()); - } - - @Test - void testBundleResetAfterChangingLocale() { - SchemaValidatorsConfig config = new SchemaValidatorsConfig(); - config.setLocale(Locale.FRENCH); - assertEquals(Locale.FRENCH.getLanguage(), config.getResourceBundle().getLocale().getLanguage()); - config.setLocale(Locale.GERMAN); - assertEquals(Locale.GERMAN.getLanguage(), config.getResourceBundle().getLocale().getLanguage()); + assertEquals(DefaultMessageSource.getInstance(), config.getMessageSource()); } @Test void testValidationWithDefaultBundleAndLocale() throws JsonProcessingException { SchemaValidatorsConfig config = new SchemaValidatorsConfig(); - String expectedMessage = new MessageFormat(I18nSupport.DEFAULT_RESOURCE_BUNDLE.getString("type")).format(new String[] {"$.foo", "integer", "string"}); + ResourceBundle resourceBundle = ResourceBundle.getBundle(DefaultMessageSource.BUNDLE_BASE_NAME, Locale.getDefault()); + String expectedMessage = new MessageFormat(resourceBundle.getString("type")).format(new String[] {"$.foo", "integer", "string"}); verify(config, expectedMessage); } @@ -60,7 +40,8 @@ void testValidationWithDefaultBundleAndCustomLocale() throws JsonProcessingExcep @Test void testValidationWithCustomBundle() throws JsonProcessingException { SchemaValidatorsConfig config = new SchemaValidatorsConfig(); - config.setResourceBundle(ResourceBundle.getBundle("issue686/translations", Locale.FRENCH)); + config.setMessageSource(new ResourceBundleMessageSource("issue686/translations")); + config.setLocale(Locale.FRENCH); verify(config, "$.foo: integer found, string expected (TEST) (FR)"); } diff --git a/src/test/java/com/networknt/schema/Issue898Test.java b/src/test/java/com/networknt/schema/Issue898Test.java index 25be111ee..6255fc24a 100644 --- a/src/test/java/com/networknt/schema/Issue898Test.java +++ b/src/test/java/com/networknt/schema/Issue898Test.java @@ -17,9 +17,8 @@ class Issue898Test extends BaseJsonSchemaValidatorTest { @Test void testMessagesWithSingleQuotes() throws Exception { - ResourceBundle bundle = ResourceBundle.getBundle(I18nSupport.DEFAULT_BUNDLE_BASE_NAME, Locale.FRENCH); SchemaValidatorsConfig config = new SchemaValidatorsConfig(); - config.setResourceBundle(bundle); + config.setLocale(Locale.FRENCH); JsonSchema schema = getJsonSchemaFromClasspath("schema/issue898.json", SpecVersion.VersionFlag.V202012, config); JsonNode node = getJsonNodeFromClasspath("data/issue898.json"); diff --git a/src/test/java/com/networknt/schema/LocaleTest.java b/src/test/java/com/networknt/schema/LocaleTest.java new file mode 100644 index 000000000..1267c8c5c --- /dev/null +++ b/src/test/java/com/networknt/schema/LocaleTest.java @@ -0,0 +1,68 @@ +/* + * Copyright (c) 2023 the original author or authors. + * + * Licensed 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 com.networknt.schema; + +import static org.junit.jupiter.api.Assertions.assertEquals; + +import java.util.Locale; +import java.util.Set; + +import org.junit.jupiter.api.Test; + +import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.databind.JsonMappingException; +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.networknt.schema.i18n.Locales; + +public class LocaleTest { + private JsonSchema getSchema(SchemaValidatorsConfig config) { + JsonSchemaFactory factory = JsonSchemaFactory.getInstance(SpecVersion.VersionFlag.V201909); + return factory.getSchema( + "{ \"$schema\": \"https://json-schema.org/draft/2019-09/schema\", \"$id\": \"https://json-schema.org/draft/2019-09/schema\", \"type\": \"object\", \"properties\": { \"foo\": { \"type\": \"string\" } } } }", + config); + } + + /** + * Tests that the validation messages are generated based on the execution + * context locale. + * + * @throws JsonMappingException the error + * @throws JsonProcessingException the error + */ + @Test + void executionContextLocale() throws JsonMappingException, JsonProcessingException { + JsonNode rootNode = new ObjectMapper().readTree(" { \"foo\": 123 } "); + SchemaValidatorsConfig config = new SchemaValidatorsConfig(); + JsonSchema jsonSchema = getSchema(config); + + Locale locale = Locales.findSupported("it;q=0.9,fr;q=1.0"); // fr + ExecutionContext executionContext = jsonSchema.createExecutionContext(); + assertEquals(config.getLocale(), executionContext.getExecutionConfig().getLocale()); + executionContext.getExecutionConfig().setLocale(locale); + Set messages = jsonSchema.validate(executionContext, rootNode); + assertEquals(1, messages.size()); + assertEquals("$.foo: integer a été trouvé, mais string est attendu", messages.iterator().next().getMessage()); + + locale = Locales.findSupported("it;q=1.0,fr;q=0.9"); // it + executionContext = jsonSchema.createExecutionContext(); + assertEquals(config.getLocale(), executionContext.getExecutionConfig().getLocale()); + executionContext.getExecutionConfig().setLocale(locale); + messages = jsonSchema.validate(executionContext, rootNode); + assertEquals(1, messages.size()); + assertEquals("$.foo: integer trovato, string atteso", messages.iterator().next().getMessage()); + } +} diff --git a/src/test/java/com/networknt/schema/OverwritingCustomMessageBugTest.java b/src/test/java/com/networknt/schema/OverwritingCustomMessageBugTest.java index da8b5466f..f8e208163 100644 --- a/src/test/java/com/networknt/schema/OverwritingCustomMessageBugTest.java +++ b/src/test/java/com/networknt/schema/OverwritingCustomMessageBugTest.java @@ -27,8 +27,8 @@ public void customMessageIsNotOverwritten() throws Exception { Map errorMsgMap = transferErrorMsg(errors); Assertions.assertTrue(errorMsgMap.containsKey("$.toplevel[1].foos"), "error message must contains key: $.foos"); Assertions.assertTrue(errorMsgMap.containsKey("$.toplevel[1].bars"), "error message must contains key: $.bars"); - Assertions.assertEquals("{0}: Must be a string with the a shape foofoofoofoo... with at least one foo", errorMsgMap.get("$.toplevel[1].foos")); - Assertions.assertEquals("{0}: Must be a string with the a shape barbarbar... with at least one bar", errorMsgMap.get("$.toplevel[1].bars")); + Assertions.assertEquals("$.toplevel[1].foos: Must be a string with the a shape foofoofoofoo... with at least one foo", errorMsgMap.get("$.toplevel[1].foos")); + Assertions.assertEquals("$.toplevel[1].bars: Must be a string with the a shape barbarbar... with at least one bar", errorMsgMap.get("$.toplevel[1].bars")); } From c4840b31426aa72bfd89942c768abf73f721e598 Mon Sep 17 00:00:00 2001 From: Justin Tay <49700559+justin-tay@users.noreply.github.com> Date: Wed, 13 Dec 2023 20:15:03 +0800 Subject: [PATCH 4/4] Support custom message per property --- doc/cust-msg.md | 60 ++++++++++-- .../schema/AdditionalPropertiesValidator.java | 2 +- .../com/networknt/schema/AnyOfValidator.java | 12 +-- .../com/networknt/schema/ConstValidator.java | 6 +- .../networknt/schema/ContainsValidator.java | 2 +- .../schema/DependenciesValidator.java | 4 +- .../networknt/schema/DependentRequired.java | 4 +- .../com/networknt/schema/EnumValidator.java | 2 +- .../networknt/schema/ErrorMessageType.java | 4 +- .../schema/ExclusiveMaximumValidator.java | 4 +- .../schema/ExclusiveMinimumValidator.java | 4 +- .../com/networknt/schema/FalseValidator.java | 2 +- .../com/networknt/schema/FormatKeyword.java | 2 +- .../com/networknt/schema/FormatValidator.java | 12 +-- .../com/networknt/schema/ItemsValidator.java | 2 +- .../com/networknt/schema/JsonMetaSchema.java | 2 +- .../java/com/networknt/schema/JsonSchema.java | 28 ++++-- .../java/com/networknt/schema/Keyword.java | 4 +- .../networknt/schema/MaxItemsValidator.java | 4 +- .../networknt/schema/MaxLengthValidator.java | 2 +- .../schema/MaxPropertiesValidator.java | 2 +- .../networknt/schema/MaximumValidator.java | 4 +- .../networknt/schema/MinItemsValidator.java | 4 +- .../networknt/schema/MinLengthValidator.java | 2 +- .../schema/MinMaxContainsValidator.java | 4 +- .../schema/MinPropertiesValidator.java | 2 +- .../networknt/schema/MinimumValidator.java | 4 +- .../networknt/schema/MultipleOfValidator.java | 4 +- .../networknt/schema/NotAllowedValidator.java | 2 +- .../com/networknt/schema/NotValidator.java | 8 +- .../com/networknt/schema/OneOfValidator.java | 4 +- .../networknt/schema/PatternValidator.java | 2 +- .../schema/PropertyNamesValidator.java | 4 +- .../networknt/schema/ReadOnlyValidator.java | 2 +- .../networknt/schema/RequiredValidator.java | 2 +- .../com/networknt/schema/TypeValidator.java | 4 +- .../schema/UnevaluatedItemsValidator.java | 2 +- .../UnevaluatedPropertiesValidator.java | 2 +- .../networknt/schema/UnionTypeValidator.java | 4 +- .../schema/UniqueItemsValidator.java | 2 +- .../networknt/schema/ValidationContext.java | 2 +- .../networknt/schema/ValidationMessage.java | 2 +- .../schema/ValidationMessageHandler.java | 23 +++-- .../networknt/schema/ValidatorTypeCode.java | 6 +- .../networknt/schema/WriteOnlyValidator.java | 2 +- .../schema/format/DateTimeValidator.java | 4 +- .../custom-message-tests.json | 93 +++++++++++++++++++ 47 files changed, 264 insertions(+), 94 deletions(-) diff --git a/doc/cust-msg.md b/doc/cust-msg.md index d5863f938..c6c96137b 100644 --- a/doc/cust-msg.md +++ b/doc/cust-msg.md @@ -6,7 +6,7 @@ The json schema itself has a place for the customised message. ## Examples ### Example 1 : The custom message can be provided outside properties for each type, as shown in the schema below. -````json +```json { "type": "object", "properties": { @@ -24,10 +24,10 @@ The custom message can be provided outside properties for each type, as shown in "type" : "Invalid type" } } -```` +``` ### Example 2 : To keep custom messages distinct for each type, one can even give them in each property. -````json +```json { "type": "object", "properties": { @@ -47,14 +47,62 @@ To keep custom messages distinct for each type, one can even give them in each p } } } -```` +``` +### Example 3 : +For the keywords `required` and `dependencies`, different messages can be specified for different properties. + +```json +{ + "type": "object", + "properties": { + "foo": { + "type": "number" + }, + "bar": { + "type": "string" + } + }, + "required": ["foo", "bar"], + "message": { + "type" : "should be an object", + "required": { + "foo" : "'foo' is required", + "bar" : "'bar' is required" + } + } +} +``` +### Example 4 : +The message can use arguments but note that single quotes need to be escaped as `java.text.MessageFormat` will be used to format the message. + +```json +{ + "type": "object", + "properties": { + "foo": { + "type": "number" + }, + "bar": { + "type": "string" + } + }, + "required": ["foo", "bar"], + "message": { + "type" : "should be an object", + "required": { + "foo" : "{0}: ''foo'' is required", + "bar" : "{0}: ''bar'' is required" + } + } +} +``` ## Format -````json +```json "message": { [validationType] : [customMessage] } -```` +``` Users can express custom message in the **'message'** field. The **'validation type'** should be the key and the **'custom message'** should be the value. diff --git a/src/main/java/com/networknt/schema/AdditionalPropertiesValidator.java b/src/main/java/com/networknt/schema/AdditionalPropertiesValidator.java index 69660b659..42b489095 100644 --- a/src/main/java/com/networknt/schema/AdditionalPropertiesValidator.java +++ b/src/main/java/com/networknt/schema/AdditionalPropertiesValidator.java @@ -97,7 +97,7 @@ public Set validate(ExecutionContext executionContext, JsonNo if (!allowedProperties.contains(pname) && !handledByPatternProperties) { if (!allowAdditionalProperties) { - errors.add(buildValidationMessage(at, executionContext.getExecutionConfig().getLocale(), pname)); + errors.add(buildValidationMessage(null, at, executionContext.getExecutionConfig().getLocale(), pname)); } else { if (additionalPropertiesSchema != null) { ValidatorState state = (ValidatorState) collectorContext.get(ValidatorState.VALIDATOR_STATE_KEY); diff --git a/src/main/java/com/networknt/schema/AnyOfValidator.java b/src/main/java/com/networknt/schema/AnyOfValidator.java index ca63ebfcc..bffac5e13 100644 --- a/src/main/java/com/networknt/schema/AnyOfValidator.java +++ b/src/main/java/com/networknt/schema/AnyOfValidator.java @@ -77,8 +77,8 @@ public Set validate(ExecutionContext executionContext, JsonNo //If schema has type validator and node type doesn't match with schemaType then ignore it //For union type, it is a must to call TypeValidator if (typeValidator.getSchemaType() != JsonType.UNION && !typeValidator.equalsToSchemaType(node)) { - allErrors.add(buildValidationMessage(at, executionContext.getExecutionConfig().getLocale(), - typeValidator.getSchemaType().toString())); + allErrors.add(buildValidationMessage(null, at, + executionContext.getExecutionConfig().getLocale(), typeValidator.getSchemaType().toString())); continue; } } @@ -107,8 +107,8 @@ public Set validate(ExecutionContext executionContext, JsonNo if (this.discriminatorContext.isDiscriminatorMatchFound()) { if (!errors.isEmpty()) { allErrors.addAll(errors); - allErrors.add(buildValidationMessage(at, - executionContext.getExecutionConfig().getLocale(), DISCRIMINATOR_REMARK)); + allErrors.add(buildValidationMessage(null, + at, executionContext.getExecutionConfig().getLocale(), DISCRIMINATOR_REMARK)); } else { // Clear all errors. allErrors.clear(); @@ -135,8 +135,8 @@ public Set validate(ExecutionContext executionContext, JsonNo if (this.validationContext.getConfig().isOpenAPI3StyleDiscriminators() && this.discriminatorContext.isActive()) { final Set errors = new HashSet<>(); - errors.add(buildValidationMessage(at, executionContext.getExecutionConfig().getLocale(), - "based on the provided discriminator. No alternative could be chosen based on the discriminator property")); + errors.add(buildValidationMessage(null, at, + executionContext.getExecutionConfig().getLocale(), "based on the provided discriminator. No alternative could be chosen based on the discriminator property")); return Collections.unmodifiableSet(errors); } } finally { diff --git a/src/main/java/com/networknt/schema/ConstValidator.java b/src/main/java/com/networknt/schema/ConstValidator.java index 4c3aea4cf..df47af289 100644 --- a/src/main/java/com/networknt/schema/ConstValidator.java +++ b/src/main/java/com/networknt/schema/ConstValidator.java @@ -36,12 +36,12 @@ public Set validate(ExecutionContext executionContext, JsonNo if (schemaNode.isNumber() && node.isNumber()) { if (schemaNode.decimalValue().compareTo(node.decimalValue()) != 0) { - return Collections.singleton(buildValidationMessage(at, - executionContext.getExecutionConfig().getLocale(), schemaNode.asText())); + return Collections.singleton(buildValidationMessage(null, + at, executionContext.getExecutionConfig().getLocale(), schemaNode.asText())); } } else if (!schemaNode.equals(node)) { return Collections.singleton( - buildValidationMessage(at, executionContext.getExecutionConfig().getLocale(), schemaNode.asText())); + buildValidationMessage(null, at, executionContext.getExecutionConfig().getLocale(), schemaNode.asText())); } return Collections.emptySet(); } diff --git a/src/main/java/com/networknt/schema/ContainsValidator.java b/src/main/java/com/networknt/schema/ContainsValidator.java index abbae9818..5a147c914 100644 --- a/src/main/java/com/networknt/schema/ContainsValidator.java +++ b/src/main/java/com/networknt/schema/ContainsValidator.java @@ -112,6 +112,6 @@ public void preloadJsonSchema() { } private Set boundsViolated(String messageKey, Locale locale, String at, int bounds) { - return Collections.singleton(buildValidationMessage(at, messageKey, locale, String.valueOf(bounds), this.schema.getSchemaNode().toString())); + return Collections.singleton(buildValidationMessage(null, at, messageKey, locale, String.valueOf(bounds), this.schema.getSchemaNode().toString())); } } diff --git a/src/main/java/com/networknt/schema/DependenciesValidator.java b/src/main/java/com/networknt/schema/DependenciesValidator.java index 379e1fc10..93b1b007f 100644 --- a/src/main/java/com/networknt/schema/DependenciesValidator.java +++ b/src/main/java/com/networknt/schema/DependenciesValidator.java @@ -62,8 +62,8 @@ public Set validate(ExecutionContext executionContext, JsonNo if (deps != null && !deps.isEmpty()) { for (String field : deps) { if (node.get(field) == null) { - errors.add(buildValidationMessage(at, executionContext.getExecutionConfig().getLocale(), - propertyDeps.toString())); + errors.add(buildValidationMessage(pname, at, + executionContext.getExecutionConfig().getLocale(), propertyDeps.toString())); } } } diff --git a/src/main/java/com/networknt/schema/DependentRequired.java b/src/main/java/com/networknt/schema/DependentRequired.java index 9aa6beb9f..bf3f50042 100644 --- a/src/main/java/com/networknt/schema/DependentRequired.java +++ b/src/main/java/com/networknt/schema/DependentRequired.java @@ -56,8 +56,8 @@ public Set validate(ExecutionContext executionContext, JsonNo if (dependencies != null && !dependencies.isEmpty()) { for (String field : dependencies) { if (node.get(field) == null) { - errors.add(buildValidationMessage(at, executionContext.getExecutionConfig().getLocale(), field, - pname)); + errors.add(buildValidationMessage(pname, at, executionContext.getExecutionConfig().getLocale(), + field, pname)); } } } diff --git a/src/main/java/com/networknt/schema/EnumValidator.java b/src/main/java/com/networknt/schema/EnumValidator.java index a8b03d832..1f10ef451 100644 --- a/src/main/java/com/networknt/schema/EnumValidator.java +++ b/src/main/java/com/networknt/schema/EnumValidator.java @@ -82,7 +82,7 @@ public Set validate(ExecutionContext executionContext, JsonNo if (node.isNumber()) node = DecimalNode.valueOf(node.decimalValue()); if (!nodes.contains(node) && !( this.validationContext.getConfig().isTypeLoose() && isTypeLooseContainsInEnum(node))) { - return Collections.singleton(buildValidationMessage(at, executionContext.getExecutionConfig().getLocale(), error)); + return Collections.singleton(buildValidationMessage(null, at, executionContext.getExecutionConfig().getLocale(), error)); } return Collections.emptySet(); diff --git a/src/main/java/com/networknt/schema/ErrorMessageType.java b/src/main/java/com/networknt/schema/ErrorMessageType.java index 6be1e14f0..b7751cfd4 100644 --- a/src/main/java/com/networknt/schema/ErrorMessageType.java +++ b/src/main/java/com/networknt/schema/ErrorMessageType.java @@ -16,6 +16,8 @@ package com.networknt.schema; +import java.util.Map; + public interface ErrorMessageType { /** * Your error code. Please ensure global uniqueness. Builtin error codes are sequential numbers. @@ -26,7 +28,7 @@ public interface ErrorMessageType { */ String getErrorCode(); - default String getCustomMessage() { + default Map getCustomMessage() { return null; } diff --git a/src/main/java/com/networknt/schema/ExclusiveMaximumValidator.java b/src/main/java/com/networknt/schema/ExclusiveMaximumValidator.java index a667d6a22..73c285f66 100644 --- a/src/main/java/com/networknt/schema/ExclusiveMaximumValidator.java +++ b/src/main/java/com/networknt/schema/ExclusiveMaximumValidator.java @@ -107,8 +107,8 @@ public Set validate(ExecutionContext executionContext, JsonNo } if (typedMaximum.crossesThreshold(node)) { - return Collections.singleton(buildValidationMessage(at, executionContext.getExecutionConfig().getLocale(), - typedMaximum.thresholdValue())); + return Collections.singleton(buildValidationMessage(null, at, + executionContext.getExecutionConfig().getLocale(), typedMaximum.thresholdValue())); } return Collections.emptySet(); } diff --git a/src/main/java/com/networknt/schema/ExclusiveMinimumValidator.java b/src/main/java/com/networknt/schema/ExclusiveMinimumValidator.java index c1c2fda58..62a20af4a 100644 --- a/src/main/java/com/networknt/schema/ExclusiveMinimumValidator.java +++ b/src/main/java/com/networknt/schema/ExclusiveMinimumValidator.java @@ -114,8 +114,8 @@ public Set validate(ExecutionContext executionContext, JsonNo } if (typedMinimum.crossesThreshold(node)) { - return Collections.singleton(buildValidationMessage(at, executionContext.getExecutionConfig().getLocale(), - typedMinimum.thresholdValue())); + return Collections.singleton(buildValidationMessage(null, at, + executionContext.getExecutionConfig().getLocale(), typedMinimum.thresholdValue())); } return Collections.emptySet(); } diff --git a/src/main/java/com/networknt/schema/FalseValidator.java b/src/main/java/com/networknt/schema/FalseValidator.java index 3eafa938f..10c4a9133 100644 --- a/src/main/java/com/networknt/schema/FalseValidator.java +++ b/src/main/java/com/networknt/schema/FalseValidator.java @@ -32,6 +32,6 @@ public FalseValidator(String schemaPath, final JsonNode schemaNode, JsonSchema p public Set validate(ExecutionContext executionContext, JsonNode node, JsonNode rootNode, String at) { debug(logger, node, rootNode, at); // For the false validator, it is always not valid - return Collections.singleton(buildValidationMessage(at, executionContext.getExecutionConfig().getLocale())); + return Collections.singleton(buildValidationMessage(null, at, executionContext.getExecutionConfig().getLocale())); } } diff --git a/src/main/java/com/networknt/schema/FormatKeyword.java b/src/main/java/com/networknt/schema/FormatKeyword.java index d38e6ff10..5074e7d37 100644 --- a/src/main/java/com/networknt/schema/FormatKeyword.java +++ b/src/main/java/com/networknt/schema/FormatKeyword.java @@ -73,7 +73,7 @@ public String getValue() { } @Override - public void setCustomMessage(String message) { + public void setCustomMessage(Map message) { this.type.setCustomMessage(message); } } diff --git a/src/main/java/com/networknt/schema/FormatValidator.java b/src/main/java/com/networknt/schema/FormatValidator.java index 667db95b6..49a739156 100644 --- a/src/main/java/com/networknt/schema/FormatValidator.java +++ b/src/main/java/com/networknt/schema/FormatValidator.java @@ -50,18 +50,18 @@ public Set validate(ExecutionContext executionContext, JsonNo if(format.getName().equals("ipv6")) { if(!node.textValue().trim().equals(node.textValue())) { // leading and trailing spaces - errors.add(buildValidationMessage(at, executionContext.getExecutionConfig().getLocale(), - format.getName(), format.getErrorMessageDescription())); + errors.add(buildValidationMessage(null, at, + executionContext.getExecutionConfig().getLocale(), format.getName(), format.getErrorMessageDescription())); } else if(node.textValue().contains("%")) { // zone id is not part of the ipv6 - errors.add(buildValidationMessage(at, executionContext.getExecutionConfig().getLocale(), - format.getName(), format.getErrorMessageDescription())); + errors.add(buildValidationMessage(null, at, + executionContext.getExecutionConfig().getLocale(), format.getName(), format.getErrorMessageDescription())); } } try { if (!format.matches(executionContext, node.textValue())) { - errors.add(buildValidationMessage(at, executionContext.getExecutionConfig().getLocale(), - format.getName(), format.getErrorMessageDescription())); + errors.add(buildValidationMessage(null, at, + executionContext.getExecutionConfig().getLocale(), format.getName(), format.getErrorMessageDescription())); } } catch (PatternSyntaxException pse) { // String is considered valid if pattern is invalid diff --git a/src/main/java/com/networknt/schema/ItemsValidator.java b/src/main/java/com/networknt/schema/ItemsValidator.java index 47c9fa338..9e151b4c3 100644 --- a/src/main/java/com/networknt/schema/ItemsValidator.java +++ b/src/main/java/com/networknt/schema/ItemsValidator.java @@ -128,7 +128,7 @@ private void doValidate(ExecutionContext executionContext, Set getKeywords() { } public JsonValidator newValidator(ValidationContext validationContext, String schemaPath, String keyword /* keyword */, JsonNode schemaNode, - JsonSchema parentSchema, String customMessage) { + JsonSchema parentSchema, Map customMessage) { try { Keyword kw = this.keywords.get(keyword); diff --git a/src/main/java/com/networknt/schema/JsonSchema.java b/src/main/java/com/networknt/schema/JsonSchema.java index c4a2e43fb..7d9cd8ecd 100644 --- a/src/main/java/com/networknt/schema/JsonSchema.java +++ b/src/main/java/com/networknt/schema/JsonSchema.java @@ -18,6 +18,7 @@ import com.fasterxml.jackson.databind.JsonNode; import com.fasterxml.jackson.databind.node.ObjectNode; +import com.fasterxml.jackson.databind.node.TextNode; import com.networknt.schema.CollectorContext.Scope; import com.networknt.schema.SpecVersion.VersionFlag; import com.networknt.schema.ValidationContext.DiscriminatorContext; @@ -258,11 +259,11 @@ private Map read(JsonNode schemaNode) { Map validators = new TreeMap<>(VALIDATOR_SORT); if (schemaNode.isBoolean()) { if (schemaNode.booleanValue()) { - final String customMessage = getCustomMessage(schemaNode, "true"); + final Map customMessage = getCustomMessage(schemaNode, "true"); JsonValidator validator = this.validationContext.newValidator(getSchemaPath(), "true", schemaNode, this, customMessage); validators.put(getSchemaPath() + "/true", validator); } else { - final String customMessage = getCustomMessage(schemaNode, "false"); + final Map customMessage = getCustomMessage(schemaNode, "false"); JsonValidator validator = this.validationContext.newValidator(getSchemaPath(), "false", schemaNode, this, customMessage); validators.put(getSchemaPath() + "/false", validator); } @@ -276,7 +277,7 @@ private Map read(JsonNode schemaNode) { while (pnames.hasNext()) { String pname = pnames.next(); JsonNode nodeToUse = pname.equals("if") ? schemaNode : schemaNode.get(pname); - String customMessage = getCustomMessage(schemaNode, pname); + Map customMessage = getCustomMessage(schemaNode, pname); if ("$recursiveAnchor".equals(pname)) { if (!nodeToUse.isBoolean()) { @@ -347,16 +348,29 @@ private long activeDialect() { return lhs.compareTo(rhs); // TODO: This smells. We are performing a lexicographical ordering of paths of unknown depth. }; - private String getCustomMessage(JsonNode schemaNode, String pname) { + private Map getCustomMessage(JsonNode schemaNode, String pname) { if (!this.validationContext.getConfig().isCustomMessageSupported()) { return null; } final JsonSchema parentSchema = getParentSchema(); final JsonNode message = getMessageNode(schemaNode, parentSchema, pname); - if (message != null && message.get(pname) != null) { - return message.get(pname).asText(); + if (message != null) { + JsonNode messageNode = message.get(pname); + if (messageNode != null) { + if (messageNode.isTextual()) { + return Collections.singletonMap("", messageNode.asText()); + } else if (messageNode.isObject()) { + Map result = new LinkedHashMap<>(); + messageNode.fields().forEachRemaining(entry -> { + result.put(entry.getKey(), entry.getValue().textValue()); + }); + if (!result.isEmpty()) { + return result; + } + } + } } - return null; + return Collections.emptyMap(); } private JsonNode getMessageNode(JsonNode schemaNode, JsonSchema parentSchema, String pname) { diff --git a/src/main/java/com/networknt/schema/Keyword.java b/src/main/java/com/networknt/schema/Keyword.java index 0f9283b19..749d01387 100644 --- a/src/main/java/com/networknt/schema/Keyword.java +++ b/src/main/java/com/networknt/schema/Keyword.java @@ -17,12 +17,14 @@ package com.networknt.schema; +import java.util.Map; + import com.fasterxml.jackson.databind.JsonNode; public interface Keyword { String getValue(); - default void setCustomMessage(String message) { + default void setCustomMessage(Map message) { //setCustom message } diff --git a/src/main/java/com/networknt/schema/MaxItemsValidator.java b/src/main/java/com/networknt/schema/MaxItemsValidator.java index 46ef19a47..9990771b3 100644 --- a/src/main/java/com/networknt/schema/MaxItemsValidator.java +++ b/src/main/java/com/networknt/schema/MaxItemsValidator.java @@ -44,11 +44,11 @@ public Set validate(ExecutionContext executionContext, JsonNo if (node.isArray()) { if (node.size() > max) { - return Collections.singleton(buildValidationMessage(at, executionContext.getExecutionConfig().getLocale(), "" + max)); + return Collections.singleton(buildValidationMessage(null, at, executionContext.getExecutionConfig().getLocale(), "" + max)); } } else if (this.validationContext.getConfig().isTypeLoose()) { if (1 > max) { - return Collections.singleton(buildValidationMessage(at, executionContext.getExecutionConfig().getLocale(), "" + max)); + return Collections.singleton(buildValidationMessage(null, at, executionContext.getExecutionConfig().getLocale(), "" + max)); } } diff --git a/src/main/java/com/networknt/schema/MaxLengthValidator.java b/src/main/java/com/networknt/schema/MaxLengthValidator.java index 3ed565e1b..5a9330178 100644 --- a/src/main/java/com/networknt/schema/MaxLengthValidator.java +++ b/src/main/java/com/networknt/schema/MaxLengthValidator.java @@ -48,7 +48,7 @@ public Set validate(ExecutionContext executionContext, JsonNo } if (node.textValue().codePointCount(0, node.textValue().length()) > maxLength) { return Collections.singleton( - buildValidationMessage(at, executionContext.getExecutionConfig().getLocale(), "" + maxLength)); + buildValidationMessage(null, at, executionContext.getExecutionConfig().getLocale(), "" + maxLength)); } return Collections.emptySet(); } diff --git a/src/main/java/com/networknt/schema/MaxPropertiesValidator.java b/src/main/java/com/networknt/schema/MaxPropertiesValidator.java index e41d7b0bb..ce6b6acc6 100644 --- a/src/main/java/com/networknt/schema/MaxPropertiesValidator.java +++ b/src/main/java/com/networknt/schema/MaxPropertiesValidator.java @@ -44,7 +44,7 @@ public Set validate(ExecutionContext executionContext, JsonNo if (node.isObject()) { if (node.size() > max) { return Collections.singleton( - buildValidationMessage(at, executionContext.getExecutionConfig().getLocale(), "" + max)); + buildValidationMessage(null, at, executionContext.getExecutionConfig().getLocale(), "" + max)); } } diff --git a/src/main/java/com/networknt/schema/MaximumValidator.java b/src/main/java/com/networknt/schema/MaximumValidator.java index a4d900bb0..b3328c21e 100644 --- a/src/main/java/com/networknt/schema/MaximumValidator.java +++ b/src/main/java/com/networknt/schema/MaximumValidator.java @@ -116,8 +116,8 @@ public Set validate(ExecutionContext executionContext, JsonNo } if (typedMaximum.crossesThreshold(node)) { - return Collections.singleton(buildValidationMessage(at, executionContext.getExecutionConfig().getLocale(), - typedMaximum.thresholdValue())); + return Collections.singleton(buildValidationMessage(null, at, + executionContext.getExecutionConfig().getLocale(), typedMaximum.thresholdValue())); } return Collections.emptySet(); } diff --git a/src/main/java/com/networknt/schema/MinItemsValidator.java b/src/main/java/com/networknt/schema/MinItemsValidator.java index 77263ae63..d18c0a655 100644 --- a/src/main/java/com/networknt/schema/MinItemsValidator.java +++ b/src/main/java/com/networknt/schema/MinItemsValidator.java @@ -43,12 +43,12 @@ public Set validate(ExecutionContext executionContext, JsonNo if (node.isArray()) { if (node.size() < min) { return Collections.singleton( - buildValidationMessage(at, executionContext.getExecutionConfig().getLocale(), "" + min)); + buildValidationMessage(null, at, executionContext.getExecutionConfig().getLocale(), "" + min)); } } else if (this.validationContext.getConfig().isTypeLoose()) { if (1 < min) { return Collections.singleton( - buildValidationMessage(at, executionContext.getExecutionConfig().getLocale(), "" + min)); + buildValidationMessage(null, at, executionContext.getExecutionConfig().getLocale(), "" + min)); } } diff --git a/src/main/java/com/networknt/schema/MinLengthValidator.java b/src/main/java/com/networknt/schema/MinLengthValidator.java index 6ce311085..5965c89e9 100644 --- a/src/main/java/com/networknt/schema/MinLengthValidator.java +++ b/src/main/java/com/networknt/schema/MinLengthValidator.java @@ -49,7 +49,7 @@ public Set validate(ExecutionContext executionContext, JsonNo if (node.textValue().codePointCount(0, node.textValue().length()) < minLength) { return Collections.singleton( - buildValidationMessage(at, executionContext.getExecutionConfig().getLocale(), "" + minLength)); + buildValidationMessage(null, at, executionContext.getExecutionConfig().getLocale(), "" + minLength)); } return Collections.emptySet(); } diff --git a/src/main/java/com/networknt/schema/MinMaxContainsValidator.java b/src/main/java/com/networknt/schema/MinMaxContainsValidator.java index aecc86d4e..791b2f9a4 100644 --- a/src/main/java/com/networknt/schema/MinMaxContainsValidator.java +++ b/src/main/java/com/networknt/schema/MinMaxContainsValidator.java @@ -62,8 +62,8 @@ public MinMaxContainsValidator(String schemaPath, JsonNode schemaNode, JsonSchem public Set validate(ExecutionContext executionContext, JsonNode node, JsonNode rootNode, String at) { return this.analysis != null ? this.analysis.stream() - .map(analysis -> buildValidationMessage(analysis.getAt(), analysis.getMessageKey(), - executionContext.getExecutionConfig().getLocale(), parentSchema.getSchemaNode().toString())) + .map(analysis -> buildValidationMessage(null, analysis.getAt(), + analysis.getMessageKey(), executionContext.getExecutionConfig().getLocale(), parentSchema.getSchemaNode().toString())) .collect(Collectors.toCollection(LinkedHashSet::new)) : Collections.emptySet(); } diff --git a/src/main/java/com/networknt/schema/MinPropertiesValidator.java b/src/main/java/com/networknt/schema/MinPropertiesValidator.java index 61a6a491b..d23b573e6 100644 --- a/src/main/java/com/networknt/schema/MinPropertiesValidator.java +++ b/src/main/java/com/networknt/schema/MinPropertiesValidator.java @@ -44,7 +44,7 @@ public Set validate(ExecutionContext executionContext, JsonNo if (node.isObject()) { if (node.size() < min) { return Collections.singleton( - buildValidationMessage(at, executionContext.getExecutionConfig().getLocale(), "" + min)); + buildValidationMessage(null, at, executionContext.getExecutionConfig().getLocale(), "" + min)); } } diff --git a/src/main/java/com/networknt/schema/MinimumValidator.java b/src/main/java/com/networknt/schema/MinimumValidator.java index 33001c8e5..2e0016d0a 100644 --- a/src/main/java/com/networknt/schema/MinimumValidator.java +++ b/src/main/java/com/networknt/schema/MinimumValidator.java @@ -123,8 +123,8 @@ public Set validate(ExecutionContext executionContext, JsonNo } if (typedMinimum.crossesThreshold(node)) { - return Collections.singleton(buildValidationMessage(at, executionContext.getExecutionConfig().getLocale(), - typedMinimum.thresholdValue())); + return Collections.singleton(buildValidationMessage(null, at, + executionContext.getExecutionConfig().getLocale(), typedMinimum.thresholdValue())); } return Collections.emptySet(); } diff --git a/src/main/java/com/networknt/schema/MultipleOfValidator.java b/src/main/java/com/networknt/schema/MultipleOfValidator.java index 766a2c51a..383edd823 100644 --- a/src/main/java/com/networknt/schema/MultipleOfValidator.java +++ b/src/main/java/com/networknt/schema/MultipleOfValidator.java @@ -49,8 +49,8 @@ public Set validate(ExecutionContext executionContext, JsonNo BigDecimal accurateDividend = node.isBigDecimal() ? node.decimalValue() : new BigDecimal(String.valueOf(nodeValue)); BigDecimal accurateDivisor = new BigDecimal(String.valueOf(divisor)); if (accurateDividend.divideAndRemainder(accurateDivisor)[1].abs().compareTo(BigDecimal.ZERO) > 0) { - return Collections.singleton(buildValidationMessage(at, - executionContext.getExecutionConfig().getLocale(), "" + divisor)); + return Collections.singleton(buildValidationMessage(null, + at, executionContext.getExecutionConfig().getLocale(), "" + divisor)); } } } diff --git a/src/main/java/com/networknt/schema/NotAllowedValidator.java b/src/main/java/com/networknt/schema/NotAllowedValidator.java index 29e1e84b5..a53c4db94 100644 --- a/src/main/java/com/networknt/schema/NotAllowedValidator.java +++ b/src/main/java/com/networknt/schema/NotAllowedValidator.java @@ -49,7 +49,7 @@ public Set validate(ExecutionContext executionContext, JsonNo JsonNode propertyNode = node.get(fieldName); if (propertyNode != null) { - errors.add(buildValidationMessage(at, executionContext.getExecutionConfig().getLocale(), fieldName)); + errors.add(buildValidationMessage(fieldName, at, executionContext.getExecutionConfig().getLocale(), fieldName)); } } diff --git a/src/main/java/com/networknt/schema/NotValidator.java b/src/main/java/com/networknt/schema/NotValidator.java index 7d38e9f1d..89f18990e 100644 --- a/src/main/java/com/networknt/schema/NotValidator.java +++ b/src/main/java/com/networknt/schema/NotValidator.java @@ -46,8 +46,8 @@ public Set validate(ExecutionContext executionContext, JsonNo debug(logger, node, rootNode, at); errors = this.schema.validate(executionContext, node, rootNode, at); if (errors.isEmpty()) { - return Collections.singleton(buildValidationMessage(at, - executionContext.getExecutionConfig().getLocale(), this.schema.toString())); + return Collections.singleton(buildValidationMessage(null, + at, executionContext.getExecutionConfig().getLocale(), this.schema.toString())); } return Collections.emptySet(); } finally { @@ -66,8 +66,8 @@ public Set walk(ExecutionContext executionContext, JsonNode n Set errors = this.schema.walk(executionContext, node, rootNode, at, shouldValidateSchema); if (errors.isEmpty()) { - return Collections.singleton(buildValidationMessage(at, executionContext.getExecutionConfig().getLocale(), - this.schema.toString())); + return Collections.singleton(buildValidationMessage(null, at, + executionContext.getExecutionConfig().getLocale(), this.schema.toString())); } return Collections.emptySet(); } diff --git a/src/main/java/com/networknt/schema/OneOfValidator.java b/src/main/java/com/networknt/schema/OneOfValidator.java index 4aac96170..068a2e51c 100644 --- a/src/main/java/com/networknt/schema/OneOfValidator.java +++ b/src/main/java/com/networknt/schema/OneOfValidator.java @@ -95,8 +95,8 @@ public Set validate(ExecutionContext executionContext, JsonNo // ensure there is always an "OneOf" error reported if number of valid schemas is not equal to 1. if (numberOfValidSchema != 1) { - ValidationMessage message = buildValidationMessage(at, - executionContext.getExecutionConfig().getLocale(), Integer.toString(numberOfValidSchema)); + ValidationMessage message = buildValidationMessage(null, + at, executionContext.getExecutionConfig().getLocale(), Integer.toString(numberOfValidSchema)); if (this.failFast) { throw new JsonSchemaException(message); } diff --git a/src/main/java/com/networknt/schema/PatternValidator.java b/src/main/java/com/networknt/schema/PatternValidator.java index f57a1aa5a..362070391 100644 --- a/src/main/java/com/networknt/schema/PatternValidator.java +++ b/src/main/java/com/networknt/schema/PatternValidator.java @@ -61,7 +61,7 @@ public Set validate(ExecutionContext executionContext, JsonNo try { if (!matches(node.asText())) { return Collections.singleton( - buildValidationMessage(at, executionContext.getExecutionConfig().getLocale(), this.pattern)); + buildValidationMessage(null, at, executionContext.getExecutionConfig().getLocale(), this.pattern)); } } catch (JsonSchemaException e) { throw e; diff --git a/src/main/java/com/networknt/schema/PropertyNamesValidator.java b/src/main/java/com/networknt/schema/PropertyNamesValidator.java index f1fb83f5d..765cb17eb 100644 --- a/src/main/java/com/networknt/schema/PropertyNamesValidator.java +++ b/src/main/java/com/networknt/schema/PropertyNamesValidator.java @@ -48,8 +48,8 @@ public Set validate(ExecutionContext executionContext, JsonNo if (msg.startsWith(path)) msg = msg.substring(path.length()).replaceFirst("^:\\s*", ""); - errors.add(buildValidationMessage(schemaError.getPath(), - executionContext.getExecutionConfig().getLocale(), msg)); + errors.add(buildValidationMessage(pname, + schemaError.getPath(), executionContext.getExecutionConfig().getLocale(), msg)); } } return Collections.unmodifiableSet(errors); diff --git a/src/main/java/com/networknt/schema/ReadOnlyValidator.java b/src/main/java/com/networknt/schema/ReadOnlyValidator.java index 2b56e302e..31518fc59 100644 --- a/src/main/java/com/networknt/schema/ReadOnlyValidator.java +++ b/src/main/java/com/networknt/schema/ReadOnlyValidator.java @@ -41,7 +41,7 @@ public ReadOnlyValidator(String schemaPath, JsonNode schemaNode, JsonSchema pare public Set validate(ExecutionContext executionContext, JsonNode node, JsonNode rootNode, String at) { debug(logger, node, rootNode, at); if (this.readOnly) { - return Collections.singleton(buildValidationMessage(at, executionContext.getExecutionConfig().getLocale())); + return Collections.singleton(buildValidationMessage(null, at, executionContext.getExecutionConfig().getLocale())); } return Collections.emptySet(); } diff --git a/src/main/java/com/networknt/schema/RequiredValidator.java b/src/main/java/com/networknt/schema/RequiredValidator.java index 3042acbe0..8a340363d 100644 --- a/src/main/java/com/networknt/schema/RequiredValidator.java +++ b/src/main/java/com/networknt/schema/RequiredValidator.java @@ -52,7 +52,7 @@ public Set validate(ExecutionContext executionContext, JsonNo JsonNode propertyNode = node.get(fieldName); if (propertyNode == null) { - errors.add(buildValidationMessage(at, executionContext.getExecutionConfig().getLocale(), fieldName)); + errors.add(buildValidationMessage(fieldName, at, executionContext.getExecutionConfig().getLocale(), fieldName)); } } diff --git a/src/main/java/com/networknt/schema/TypeValidator.java b/src/main/java/com/networknt/schema/TypeValidator.java index 707be8421..97bcf5d77 100644 --- a/src/main/java/com/networknt/schema/TypeValidator.java +++ b/src/main/java/com/networknt/schema/TypeValidator.java @@ -60,8 +60,8 @@ public Set validate(ExecutionContext executionContext, JsonNo if (!equalsToSchemaType(node)) { JsonType nodeType = TypeFactory.getValueNodeType(node, this.validationContext.getConfig()); - return Collections.singleton(buildValidationMessage(at, executionContext.getExecutionConfig().getLocale(), - nodeType.toString(), this.schemaType.toString())); + return Collections.singleton(buildValidationMessage(null, at, + executionContext.getExecutionConfig().getLocale(), nodeType.toString(), this.schemaType.toString())); } // TODO: Is this really necessary? diff --git a/src/main/java/com/networknt/schema/UnevaluatedItemsValidator.java b/src/main/java/com/networknt/schema/UnevaluatedItemsValidator.java index 1b00d3fe5..78783e318 100644 --- a/src/main/java/com/networknt/schema/UnevaluatedItemsValidator.java +++ b/src/main/java/com/networknt/schema/UnevaluatedItemsValidator.java @@ -98,7 +98,7 @@ private Set allPaths(JsonNode node, String at) { private Set reportUnevaluatedPaths(Set unevaluatedPaths, ExecutionContext executionContext) { List paths = new ArrayList<>(unevaluatedPaths); paths.sort(String.CASE_INSENSITIVE_ORDER); - return Collections.singleton(buildValidationMessage(String.join("\n ", paths), executionContext.getExecutionConfig().getLocale())); + return Collections.singleton(buildValidationMessage(null, String.join("\n ", paths), executionContext.getExecutionConfig().getLocale())); } private static Set unevaluatedPaths(CollectorContext collectorContext, Set allPaths) { diff --git a/src/main/java/com/networknt/schema/UnevaluatedPropertiesValidator.java b/src/main/java/com/networknt/schema/UnevaluatedPropertiesValidator.java index ddcf0c1af..79e7517b1 100644 --- a/src/main/java/com/networknt/schema/UnevaluatedPropertiesValidator.java +++ b/src/main/java/com/networknt/schema/UnevaluatedPropertiesValidator.java @@ -97,7 +97,7 @@ private Set allPaths(JsonNode node, String at) { private Set reportUnevaluatedPaths(Set unevaluatedPaths, ExecutionContext executionContext) { List paths = new ArrayList<>(unevaluatedPaths); paths.sort(String.CASE_INSENSITIVE_ORDER); - return Collections.singleton(buildValidationMessage(String.join("\n ", paths), executionContext.getExecutionConfig().getLocale())); + return Collections.singleton(buildValidationMessage(null, String.join("\n ", paths), executionContext.getExecutionConfig().getLocale())); } private static Set unevaluatedPaths(CollectorContext collectorContext, Set allPaths) { diff --git a/src/main/java/com/networknt/schema/UnionTypeValidator.java b/src/main/java/com/networknt/schema/UnionTypeValidator.java index 7ee38be0f..48997c751 100644 --- a/src/main/java/com/networknt/schema/UnionTypeValidator.java +++ b/src/main/java/com/networknt/schema/UnionTypeValidator.java @@ -78,8 +78,8 @@ public Set validate(ExecutionContext executionContext, JsonNo } if (!valid) { - return Collections.singleton(buildValidationMessage(at, executionContext.getExecutionConfig().getLocale(), - nodeType.toString(), error)); + return Collections.singleton(buildValidationMessage(null, at, + executionContext.getExecutionConfig().getLocale(), nodeType.toString(), error)); } return Collections.emptySet(); diff --git a/src/main/java/com/networknt/schema/UniqueItemsValidator.java b/src/main/java/com/networknt/schema/UniqueItemsValidator.java index 5ad3efec5..5d5d3323a 100644 --- a/src/main/java/com/networknt/schema/UniqueItemsValidator.java +++ b/src/main/java/com/networknt/schema/UniqueItemsValidator.java @@ -45,7 +45,7 @@ public Set validate(ExecutionContext executionContext, JsonNo Set set = new HashSet(); for (JsonNode n : node) { if (!set.add(n)) { - return Collections.singleton(buildValidationMessage(at, executionContext.getExecutionConfig().getLocale())); + return Collections.singleton(buildValidationMessage(null, at, executionContext.getExecutionConfig().getLocale())); } } } diff --git a/src/main/java/com/networknt/schema/ValidationContext.java b/src/main/java/com/networknt/schema/ValidationContext.java index 671a03d97..83c3cc9ce 100644 --- a/src/main/java/com/networknt/schema/ValidationContext.java +++ b/src/main/java/com/networknt/schema/ValidationContext.java @@ -59,7 +59,7 @@ public JsonSchema newSchema(String schemaPath, JsonNode schemaNode, JsonSchema p } public JsonValidator newValidator(String schemaPath, String keyword /* keyword */, JsonNode schemaNode, - JsonSchema parentSchema, String customMessage) { + JsonSchema parentSchema, Map customMessage) { return this.metaSchema.newValidator(this, schemaPath, keyword, schemaNode, parentSchema, customMessage); } diff --git a/src/main/java/com/networknt/schema/ValidationMessage.java b/src/main/java/com/networknt/schema/ValidationMessage.java index 004b165e9..bc0a6a520 100644 --- a/src/main/java/com/networknt/schema/ValidationMessage.java +++ b/src/main/java/com/networknt/schema/ValidationMessage.java @@ -131,7 +131,7 @@ public static ValidationMessage ofWithCustom(String type, ErrorMessageType error @Deprecated // Use the builder public static ValidationMessage of(String type, ErrorMessageType errorMessageType, MessageFormat messageFormat, String at, String schemaPath, Object... arguments) { - return ofWithCustom(type, errorMessageType, messageFormat, errorMessageType.getCustomMessage(), at, schemaPath, arguments); + return ofWithCustom(type, errorMessageType, messageFormat, errorMessageType.getCustomMessage().get(""), at, schemaPath, arguments); } @Deprecated // Use the builder diff --git a/src/main/java/com/networknt/schema/ValidationMessageHandler.java b/src/main/java/com/networknt/schema/ValidationMessageHandler.java index f08ea25b8..4341979ed 100644 --- a/src/main/java/com/networknt/schema/ValidationMessageHandler.java +++ b/src/main/java/com/networknt/schema/ValidationMessageHandler.java @@ -5,10 +5,11 @@ import com.networknt.schema.utils.StringUtils; import java.util.Locale; +import java.util.Map; public abstract class ValidationMessageHandler { protected final boolean failFast; - protected final String customMessage; + protected final Map customMessage; protected final MessageSource messageSource; protected ValidatorTypeCode validatorType; protected ErrorMessageType errorMessageType; @@ -17,7 +18,7 @@ public abstract class ValidationMessageHandler { protected JsonSchema parentSchema; - protected ValidationMessageHandler(boolean failFast, ErrorMessageType errorMessageType, String customMessage, MessageSource messageSource, ValidatorTypeCode validatorType, JsonSchema parentSchema, String schemaPath) { + protected ValidationMessageHandler(boolean failFast, ErrorMessageType errorMessageType, Map customMessage, MessageSource messageSource, ValidatorTypeCode validatorType, JsonSchema parentSchema, String schemaPath) { this.failFast = failFast; this.errorMessageType = errorMessageType; this.customMessage = customMessage; @@ -27,11 +28,21 @@ protected ValidationMessageHandler(boolean failFast, ErrorMessageType errorMessa this.parentSchema = parentSchema; } - protected ValidationMessage buildValidationMessage(String at, Locale locale, Object... arguments) { - return buildValidationMessage(at, getErrorMessageType().getErrorCodeValue(), locale, arguments); + protected ValidationMessage buildValidationMessage(String propertyName, String at, Locale locale, Object... arguments) { + return buildValidationMessage(propertyName, at, getErrorMessageType().getErrorCodeValue(), locale, arguments); } - protected ValidationMessage buildValidationMessage(String at, String messageKey, Locale locale, Object... arguments) { + protected ValidationMessage buildValidationMessage(String propertyName, String at, String messageKey, Locale locale, Object... arguments) { + String messagePattern = null; + if (this.customMessage != null) { + messagePattern = this.customMessage.get(""); + if (propertyName != null) { + String specificMessagePattern = this.customMessage.get(propertyName); + if (specificMessagePattern != null) { + messagePattern = specificMessagePattern; + } + } + } final ValidationMessage message = ValidationMessage.builder() .code(getErrorMessageType().getErrorCode()) .path(at) @@ -40,7 +51,7 @@ protected ValidationMessage buildValidationMessage(String at, String messageKey, .messageKey(messageKey) .messageFormatter(args -> this.messageSource.getMessage(messageKey, locale, args)) .type(getValidatorType().getValue()) - .message(this.customMessage) + .message(messagePattern) .build(); if (this.failFast && isApplicator()) { throw new JsonSchemaException(message); diff --git a/src/main/java/com/networknt/schema/ValidatorTypeCode.java b/src/main/java/com/networknt/schema/ValidatorTypeCode.java index f3a961617..f87006f67 100644 --- a/src/main/java/com/networknt/schema/ValidatorTypeCode.java +++ b/src/main/java/com/networknt/schema/ValidatorTypeCode.java @@ -117,7 +117,7 @@ public enum ValidatorTypeCode implements Keyword, ErrorMessageType { private final String value; private final String errorCode; - private String customMessage; + private Map customMessage; private final String errorCodeKey; private final Class validator; private final VersionCode versionCode; @@ -178,12 +178,12 @@ public String getErrorCode() { } @Override - public void setCustomMessage(String message) { + public void setCustomMessage(Map message) { this.customMessage = message; } @Override - public String getCustomMessage() { + public Map getCustomMessage() { return this.customMessage; } diff --git a/src/main/java/com/networknt/schema/WriteOnlyValidator.java b/src/main/java/com/networknt/schema/WriteOnlyValidator.java index aced43aba..3047a6ea3 100644 --- a/src/main/java/com/networknt/schema/WriteOnlyValidator.java +++ b/src/main/java/com/networknt/schema/WriteOnlyValidator.java @@ -25,7 +25,7 @@ public WriteOnlyValidator(String schemaPath, JsonNode schemaNode, JsonSchema par public Set validate(ExecutionContext executionContext, JsonNode node, JsonNode rootNode, String at) { debug(logger, node, rootNode, at); if (this.writeOnly) { - return Collections.singleton(buildValidationMessage(at, executionContext.getExecutionConfig().getLocale())); + return Collections.singleton(buildValidationMessage(null, at, executionContext.getExecutionConfig().getLocale())); } return Collections.emptySet(); } diff --git a/src/main/java/com/networknt/schema/format/DateTimeValidator.java b/src/main/java/com/networknt/schema/format/DateTimeValidator.java index 670cf3e6e..0117d58e1 100644 --- a/src/main/java/com/networknt/schema/format/DateTimeValidator.java +++ b/src/main/java/com/networknt/schema/format/DateTimeValidator.java @@ -54,8 +54,8 @@ public Set validate(ExecutionContext executionContext, JsonNo return Collections.emptySet(); } if (!isLegalDateTime(node.textValue())) { - return Collections.singleton(buildValidationMessage(at, executionContext.getExecutionConfig().getLocale(), node.textValue(), - DATETIME)); + return Collections.singleton(buildValidationMessage(null, at, executionContext.getExecutionConfig().getLocale(), + node.textValue(), DATETIME)); } return Collections.emptySet(); } diff --git a/src/test/resources/schema/customMessageTests/custom-message-tests.json b/src/test/resources/schema/customMessageTests/custom-message-tests.json index ffe99402f..1c3e90629 100644 --- a/src/test/resources/schema/customMessageTests/custom-message-tests.json +++ b/src/test/resources/schema/customMessageTests/custom-message-tests.json @@ -99,5 +99,98 @@ ] } ] + }, + { + "description": "messages for keywords for different properties", + "schema": { + "type": "object", + "properties": { + "foo": { + "type": "number" + }, + "bar": { + "type": "string" + } + }, + "required": ["foo", "bar"], + "message": { + "type" : "should be an object", + "required": { + "foo" : "{0}: ''foo'' is required", + "bar" : "{0}: ''bar'' is required" + } + } + }, + "tests": [ + { + "description": "bar is required", + "data": { + "foo": 1 + }, + "valid": false, + "validationMessages": [ + "$: 'bar' is required" + ] + }, + { + "description": "foo is required", + "data": { + "bar": "bar" + }, + "valid": false, + "validationMessages": [ + "$: 'foo' is required" + ] + }, + { + "description": "both foo and bar are required", + "data": { + }, + "valid": false, + "validationMessages": [ + "$: 'foo' is required", + "$: 'bar' is required" + ] + } + ] + }, + { + "description": "messages for keywords with arguments", + "schema": { + "type": "object", + "properties": { + "requestedItems": { + "type": "array", + "minItems": 1, + "items": { + "properties": { + "item": { + "type": "string", + "minLength": 1, + "message": { + "minLength": "{0}: Item should not be empty" + } + } + } + } + } + } + }, + "tests": [ + { + "description": "should not be empty", + "data": { + "requestedItems": [ + { + "item": "" + } + ] + }, + "valid": false, + "validationMessages": [ + "$.requestedItems[0].item: Item should not be empty" + ] + } + ] } ] \ No newline at end of file