| /* |
| * Licensed to the Apache Software Foundation (ASF) under one or more |
| * contributor license agreements. See the NOTICE file distributed with this |
| * work for additional information regarding copyright ownership. The ASF |
| * licenses this file to you under the Apache License, Version 2.0 (the |
| * "License"); you may not use this file except in compliance with the License. |
| * You may obtain a copy of the License at |
| * |
| * http://www.apache.org/licenses/LICENSE-2.0 |
| * |
| * Unless required by applicable law or agreed to in writing, software |
| * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT |
| * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the |
| * License for the specific language governing permissions and limitations under |
| * the License. |
| */ |
| package org.apache.bval.jsr; |
| |
| import static org.hamcrest.CoreMatchers.anyOf; |
| import static org.hamcrest.CoreMatchers.equalTo; |
| import static org.junit.Assert.assertEquals; |
| import static org.junit.Assert.assertFalse; |
| import static org.junit.Assert.assertTrue; |
| import static org.junit.Assume.assumeThat; |
| import static org.junit.Assume.assumeTrue; |
| import static org.mockito.Mockito.any; |
| import static org.mockito.Mockito.when; |
| |
| import java.lang.annotation.Annotation; |
| import java.net.URL; |
| import java.net.URLClassLoader; |
| import java.util.Arrays; |
| import java.util.List; |
| import java.util.Locale; |
| import java.util.Objects; |
| import java.util.function.Predicate; |
| import java.util.function.Supplier; |
| |
| import javax.el.ExpressionFactory; |
| import javax.validation.MessageInterpolator; |
| import javax.validation.Validator; |
| import javax.validation.constraints.Digits; |
| import javax.validation.constraints.Pattern; |
| import javax.validation.metadata.ConstraintDescriptor; |
| |
| import org.apache.bval.constraints.NotEmpty; |
| import org.apache.bval.jsr.ApacheValidatorConfiguration; |
| import org.apache.bval.jsr.example.Author; |
| import org.apache.bval.jsr.example.PreferredGuest; |
| import org.junit.After; |
| import org.junit.AfterClass; |
| import org.junit.Before; |
| import org.junit.Test; |
| import org.junit.runner.RunWith; |
| import org.junit.runners.Parameterized; |
| import org.junit.runners.Parameterized.Parameters; |
| import org.mockito.Mockito; |
| |
| /** |
| * MessageResolverImpl Tester. |
| */ |
| @RunWith(Parameterized.class) |
| public class DefaultMessageInterpolatorTest { |
| @Parameters(name="{0}") |
| public static List<Object[]> generateParameters(){ |
| return Arrays.asList(new Object[] { "default", null }, |
| new Object[] { "ri", "com.sun.el.ExpressionFactoryImpl" }, |
| new Object[] { "tomcat", "org.apache.el.ExpressionFactoryImpl" }, |
| new Object[] { "juel", "de.odysseus.el.ExpressionFactoryImpl" }, |
| new Object[] { "invalid", "java.lang.Object" }); |
| } |
| |
| @AfterClass |
| public static void cleanup() { |
| System.clearProperty(ExpressionFactory.class.getName()); |
| } |
| |
| private static Predicate<ConstraintDescriptor<?>> forConstraintType(Class<? extends Annotation> type) { |
| return d -> Objects.equals(type, d.getAnnotation().annotationType()); |
| } |
| |
| private String elImpl; |
| private String elFactory; |
| private DefaultMessageInterpolator interpolator; |
| private Validator validator; |
| private boolean elAvailable; |
| private ClassLoader originalClassLoader; |
| |
| public DefaultMessageInterpolatorTest(String elImpl, String elFactory) { |
| this.elImpl = elImpl; |
| this.elFactory = elFactory; |
| } |
| |
| @Before |
| public void setUp() throws Exception { |
| // store and replace CCL to sidestep EL factory caching |
| originalClassLoader = Thread.currentThread().getContextClassLoader(); |
| Thread.currentThread().setContextClassLoader(new URLClassLoader(new URL[] {}, originalClassLoader)); |
| |
| try { |
| Class<?> elFactoryClass; |
| if (elFactory == null) { |
| elFactoryClass = ExpressionFactory.class; |
| System.clearProperty(ExpressionFactory.class.getName()); |
| } else { |
| elFactoryClass = Class.forName(elFactory); |
| System.setProperty(ExpressionFactory.class.getName(), elFactory); |
| } |
| assertTrue(elFactoryClass.isInstance(ExpressionFactory.newInstance())); |
| elAvailable = true; |
| } catch (Exception e) { |
| elAvailable = false; |
| } |
| interpolator = new DefaultMessageInterpolator(); |
| interpolator.setLocale(Locale.ENGLISH); |
| validator = ApacheValidatorFactory.getDefault().getValidator(); |
| } |
| |
| @After |
| public void tearDownEL() { |
| assumeTrue(originalClassLoader != null); |
| Thread.currentThread().setContextClassLoader(originalClassLoader); |
| } |
| |
| @Test |
| public void testInterpolateFromValidationResources() { |
| String msg = interpolator.interpolate("{validator.creditcard}", |
| context("12345678", |
| () -> validator.getConstraintsForClass(PreferredGuest.class) |
| .getConstraintsForProperty("guestCreditCardNumber").getConstraintDescriptors().stream() |
| .filter(forConstraintType(Digits.class)).findFirst() |
| .orElseThrow(() -> new AssertionError("expected constraint missing")))); |
| |
| assertEquals("credit card is not valid", msg); |
| } |
| |
| @Test |
| public void testInterpolateFromDefaultResources() { |
| String msg = interpolator.interpolate("{org.apache.bval.constraints.NotEmpty.message}", |
| context("", |
| () -> validator.getConstraintsForClass(Author.class).getConstraintsForProperty("lastName") |
| .getConstraintDescriptors().stream().filter(forConstraintType(NotEmpty.class)).findFirst() |
| .orElseThrow(() -> new AssertionError("expected constraint missing")))); |
| |
| assertEquals("may not be empty", msg); |
| } |
| |
| /** |
| * Checks that strings containing special characters are correctly |
| * substituted when interpolating. |
| */ |
| @Test |
| public void testReplacementWithSpecialChars() { |
| // Try to interpolate an annotation attribute containing $ |
| String idNumberResult = this.interpolator.interpolate("Id number should match {regexp}", |
| context("12345678", |
| () -> validator.getConstraintsForClass(Person.class).getConstraintsForProperty("idNumber") |
| .getConstraintDescriptors().stream().filter(forConstraintType(Pattern.class)).findFirst() |
| .orElseThrow(() -> new AssertionError("expected constraint missing")))); |
| |
| assertEquals("Incorrect message interpolation when $ is in an attribute", "Id number should match ....$", |
| idNumberResult); |
| |
| // Try to interpolate an annotation attribute containing \ |
| String otherIdResult = this.interpolator.interpolate("Other id should match {regexp}", |
| context("12345678", |
| () -> validator.getConstraintsForClass(Person.class).getConstraintsForProperty("otherId") |
| .getConstraintDescriptors().stream().filter(forConstraintType(Pattern.class)).findFirst() |
| .orElseThrow(() -> new AssertionError("expected constraint missing")))); |
| |
| assertEquals("Incorrect message interpolation when \\ is in an attribute value", "Other id should match .\\n", |
| otherIdResult); |
| } |
| |
| @Test |
| public void testRecursiveInterpolation() { |
| String msg = this.interpolator.interpolate("{recursive.interpolation.1}", |
| context("12345678", |
| () -> validator.getConstraintsForClass(Person.class).getConstraintsForProperty("idNumber") |
| .getConstraintDescriptors().stream().filter(forConstraintType(Pattern.class)).findFirst() |
| .orElseThrow(() -> new AssertionError("expected constraint missing")))); |
| |
| assertEquals("must match \"....$\"", msg); |
| } |
| |
| @Test |
| public void testNoELAvailable() { |
| assumeThat(elImpl, equalTo("invalid")); |
| assertFalse(elAvailable); |
| |
| ApacheMessageContext context = context("12345678", |
| () -> validator.getConstraintsForClass(Person.class).getConstraintsForProperty("idNumber") |
| .getConstraintDescriptors().stream().filter(forConstraintType(Pattern.class)).findFirst() |
| .orElseThrow(() -> new AssertionError("expected constraint missing"))); |
| |
| when(context |
| .getConfigurationProperty(ApacheValidatorConfiguration.Properties.CUSTOM_TEMPLATE_EXPRESSION_EVALUATION)) |
| .thenAnswer(invocation -> Boolean.toString(true)); |
| |
| assertEquals("${regexp.charAt(4)}", interpolator.interpolate("${regexp.charAt(4)}", |
| context)); |
| } |
| |
| @Test |
| public void testDisallowCustomTemplateExpressionEvaluationByDefault() { |
| assumeTrue(elAvailable); |
| |
| assertEquals("${regexp.charAt(4)}", interpolator.interpolate("${regexp.charAt(4)}", |
| context("12345678", |
| () -> validator.getConstraintsForClass(Person.class).getConstraintsForProperty("idNumber") |
| .getConstraintDescriptors().stream().filter(forConstraintType(Pattern.class)).findFirst() |
| .orElseThrow(() -> new AssertionError("expected constraint missing"))))); |
| } |
| |
| @Test |
| public void testExpressionLanguageEvaluation() { |
| assumeTrue(elAvailable); |
| |
| final MessageInterpolator.Context context = context("12345678", |
| () -> validator.getConstraintsForClass(Person.class).getConstraintsForProperty("anotherValue") |
| .getConstraintDescriptors().stream().filter(forConstraintType(Pattern.class)).findFirst() |
| .orElseThrow(() -> new AssertionError("expected constraint missing"))); |
| |
| assertEquals("Another value should match ....$", |
| interpolator.interpolate(context.getConstraintDescriptor().getMessageTemplate(), context)); |
| } |
| |
| @Test |
| public void testMixedEvaluation() { |
| assumeTrue(elAvailable); |
| |
| final MessageInterpolator.Context context = context("12345678", |
| () -> validator.getConstraintsForClass(Person.class).getConstraintsForProperty("mixedMessageValue") |
| .getConstraintDescriptors().stream().filter(forConstraintType(Pattern.class)).findFirst() |
| .orElseThrow(() -> new AssertionError("expected constraint missing"))); |
| |
| assertEquals("Mixed message value of length 8 should match ....$", |
| interpolator.interpolate(context.getConstraintDescriptor().getMessageTemplate(), context)); |
| } |
| |
| @Test |
| public void testELEscapingTomcatJuel() { |
| assumeTrue(elAvailable); |
| assumeThat(elImpl, anyOf(equalTo("tomcat"), equalTo("juel"))); |
| |
| // not so much a test as an illustration that the specified EL implementations are seemingly confused by leading |
| // backslashes and treats the whole expression as literal. We could skip any literal text before the first |
| // non-escaped $, but that would only expose us to inconsistency for composite expressions containing more |
| // than one component EL expression |
| |
| ApacheMessageContext context = context("12345678", |
| () -> validator.getConstraintsForClass(Person.class).getConstraintsForProperty("idNumber") |
| .getConstraintDescriptors().stream().filter(forConstraintType(Pattern.class)).findFirst() |
| .orElseThrow(() -> new AssertionError("expected constraint missing"))); |
| |
| when(context |
| .getConfigurationProperty(ApacheValidatorConfiguration.Properties.CUSTOM_TEMPLATE_EXPRESSION_EVALUATION)) |
| .thenAnswer(invocation -> Boolean.toString(true)); |
| |
| assertEquals("${regexp.charAt(4)}", interpolator.interpolate("\\${regexp.charAt(4)}", |
| context)); |
| |
| assertEquals("${regexp.charAt(4)}", interpolator.interpolate("\\\\${regexp.charAt(4)}", |
| context)); |
| } |
| |
| @Test |
| public void testELEscapingRI() { |
| assumeTrue(elAvailable); |
| assumeThat(elImpl, equalTo("ri")); |
| |
| ApacheMessageContext context = context("12345678", |
| () -> validator.getConstraintsForClass(Person.class).getConstraintsForProperty("idNumber") |
| .getConstraintDescriptors().stream().filter(forConstraintType(Pattern.class)).findFirst() |
| .orElseThrow(() -> new AssertionError("expected constraint missing"))); |
| |
| when(context |
| .getConfigurationProperty(ApacheValidatorConfiguration.Properties.CUSTOM_TEMPLATE_EXPRESSION_EVALUATION)) |
| .thenAnswer(invocation -> Boolean.toString(true)); |
| |
| assertEquals("returns literal", "${regexp.charAt(4)}", |
| interpolator.interpolate("\\${regexp.charAt(4)}", |
| context)); |
| |
| assertEquals("returns literal \\ followed by $, later interpreted as an escape sequence", "$", |
| interpolator.interpolate("\\\\${regexp.charAt(4)}", |
| context)); |
| |
| assertEquals("returns literal \\ followed by .", "\\.", |
| interpolator.interpolate("\\\\${regexp.charAt(3)}", |
| context)); |
| } |
| |
| @Test |
| public void testEscapedELPattern() { |
| assertEquals("$must match \"....$\"", |
| interpolator.interpolate("\\${javax.validation.constraints.Pattern.message}", |
| context("12345678", |
| () -> validator.getConstraintsForClass(Person.class).getConstraintsForProperty("idNumber") |
| .getConstraintDescriptors().stream().filter(forConstraintType(Pattern.class)).findFirst() |
| .orElseThrow(() -> new AssertionError("expected constraint missing"))))); |
| |
| assertEquals("$must match \"....$\"", |
| interpolator.interpolate("\\${javax.validation.constraints.Pattern.message}", |
| context("12345678", |
| () -> validator.getConstraintsForClass(Person.class).getConstraintsForProperty("idNumber") |
| .getConstraintDescriptors().stream().filter(forConstraintType(Pattern.class)).findFirst() |
| .orElseThrow(() -> new AssertionError("expected constraint missing"))))); |
| |
| assertEquals("\\$must match \"....$\"", |
| interpolator.interpolate("\\\\\\${javax.validation.constraints.Pattern.message}", |
| context("12345678", |
| () -> validator.getConstraintsForClass(Person.class).getConstraintsForProperty("idNumber") |
| .getConstraintDescriptors().stream().filter(forConstraintType(Pattern.class)).findFirst() |
| .orElseThrow(() -> new AssertionError("expected constraint missing"))))); |
| } |
| |
| @SuppressWarnings("unchecked") |
| private ApacheMessageContext context(Object validatedValue, Supplier<ConstraintDescriptor<?>> descriptor) { |
| final ApacheMessageContext result = Mockito.mock(ApacheMessageContext.class); |
| when(result.unwrap(any(Class.class))) |
| .thenAnswer(invocation -> invocation.getArgumentAt(0, Class.class).cast(result)); |
| when(result.getValidatedValue()).thenReturn(validatedValue); |
| when(result.getConstraintDescriptor()).thenAnswer(invocation -> descriptor.get()); |
| return result; |
| } |
| |
| public static class Person { |
| |
| @Pattern(message = "Id number should match {regexp}", regexp = "....$") |
| public String idNumber; |
| |
| @Pattern(message = "Other id should match {regexp}", regexp = ".\\n") |
| public String otherId; |
| |
| @Pattern(message = "Another value should match ${regexp.intern()}", regexp = "....$") |
| public String anotherValue; |
| |
| @Pattern(message = "Mixed message value of length ${validatedValue.length()} should match {regexp}", regexp = "....$") |
| public String mixedMessageValue; |
| } |
| } |