Skip to content

Commit

Permalink
Add 'when' conditional expressions; update tests
Browse files Browse the repository at this point in the history
  • Loading branch information
MikeEdgar committed Jun 25, 2018
1 parent f9a0faa commit 933ca73
Show file tree
Hide file tree
Showing 7 changed files with 165 additions and 23 deletions.
20 changes: 20 additions & 0 deletions src/main/java/io/xlate/validation/constraints/Expression.java
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@
import java.lang.annotation.Target;

import javax.validation.Constraint;
import javax.validation.ConstraintViolation;
import javax.validation.Payload;

import io.xlate.validation.internal.constraintvalidators.ExpressionValidator;
Expand Down Expand Up @@ -60,6 +61,25 @@
*/
String value();

/**
* An EL expression used to determine if the expression given by
* {@link #value()} should be checked. This expression is available to
* short-circuit the constraint validation of this {@link Expression} in
* scenarios when it should not apply, e.g. a value is null and the
* constraint only applies to non-null values.
*
* @return the expression to evaluate to determine whether the constraint
* should be checked
*/
String when() default "";

/**
* Name of the node to be identified in a {@link ConstraintViolation} should
* validation fail.
*
* @return the name of the node to be identified in a
* {@link ConstraintViolation} should validation fail.
*/
String node() default "";

/**
Expand Down
12 changes: 12 additions & 0 deletions src/main/java/io/xlate/validation/constraints/JdbcStatement.java
Original file line number Diff line number Diff line change
Expand Up @@ -85,6 +85,18 @@
*/
String[] parameters() default {};

/**
* An EL expression used to determine if the expression given by
* {@link #value()} should be checked. This expression is available to
* short-circuit the constraint validation of this {@link Expression} in
* scenarios when it should not apply, e.g. a value is null and the
* constraint only applies to non-null values.
*
* @return the expression to evaluate to determine whether the constraint
* should be checked
*/
String when() default "";

/**
* The JNDI lookup name of the resource to be used to obtain a
* {@link Connection}. It can link to any compatible {@link DataSource}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
package io.xlate.validation.internal.constraintvalidators;

import javax.el.ELProcessor;
import javax.validation.ConstraintDeclarationException;

public interface BooleanExpression {

default boolean evaluate(ELProcessor processor, String expression) {
if (!expression.isEmpty()) {
Object result = processor.eval(expression);

if (result instanceof Boolean) {
return (Boolean) result;
} else {
throw new ConstraintDeclarationException("Expression `" + expression + "` does not evaluate to Boolean");
}
}

return true;
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -17,13 +17,12 @@
package io.xlate.validation.internal.constraintvalidators;

import javax.el.ELProcessor;
import javax.validation.ConstraintDeclarationException;
import javax.validation.ConstraintValidator;
import javax.validation.ConstraintValidatorContext;

import io.xlate.validation.constraints.Expression;

public class ExpressionValidator implements ConstraintValidator<Expression, Object> {
public class ExpressionValidator implements BooleanExpression, ConstraintValidator<Expression, Object> {

private Expression annotation;

Expand All @@ -34,13 +33,14 @@ public void initialize(Expression constraintAnnotation) {

@Override
public boolean isValid(Object target, ConstraintValidatorContext context) {
String expression = annotation.value();
ELProcessor processor = new ELProcessor();
processor.defineBean("self", target);

Object result = processor.eval(expression);
if (!evaluate(processor, annotation.when())) {
return true;
}

if (result instanceof Boolean) {
if (!evaluate(processor, annotation.value())) {
String nodeName = annotation.node();

if (!nodeName.isEmpty()) {
Expand All @@ -50,9 +50,9 @@ public boolean isValid(Object target, ConstraintValidatorContext context) {
.addConstraintViolation();
}

return (Boolean) result;
return false;
}

throw new ConstraintDeclarationException("Expression `" + expression + "` does not evaluate to Boolean");
return true;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,7 @@

import io.xlate.validation.constraints.JdbcStatement;

public class JdbcStatementValidator implements ConstraintValidator<JdbcStatement, Object> {
public class JdbcStatementValidator implements BooleanExpression, ConstraintValidator<JdbcStatement, Object> {

JdbcStatement annotation;
DataSource dataSource;
Expand All @@ -47,7 +47,22 @@ public void initialize(JdbcStatement constraintAnnotation) {
public boolean isValid(Object target, ConstraintValidatorContext context) {
final String sql = annotation.value();
final String[] parameters = annotation.parameters();
final boolean valid = executeQuery(target, sql, parameters);
final String when = annotation.when();
final ELProcessor processor;

if (!when.isEmpty() || parameters.length > 0) {
processor = new ELProcessor();
processor.defineBean("self", target);

if (!evaluate(processor, when)) {
// Validation does not apply based on 'when' condition
return true;
}
} else {
processor = null;
}

final boolean valid = executeQuery(processor, sql, parameters);

if (!valid) {
updateValidationContext(context, annotation.node(), annotation.message());
Expand All @@ -72,12 +87,12 @@ DataSource getDataSource(String dataSourceLookup) {
return dataSource;
}

boolean executeQuery(Object target, String sql, String[] parameters) {
boolean executeQuery(ELProcessor processor, String sql, String[] parameters) {
final boolean valid;

try (Connection connection = dataSource.getConnection()) {
try (PreparedStatement statement = connection.prepareStatement(sql)) {
setParameters(target, parameters, statement);
setParameters(processor, parameters, statement);

try (ResultSet results = statement.executeQuery()) {
valid = results.next();
Expand All @@ -90,13 +105,11 @@ boolean executeQuery(Object target, String sql, String[] parameters) {
return valid;
}

void setParameters(Object target, String[] parameters, PreparedStatement statement) {
void setParameters(ELProcessor processor, String[] parameters, PreparedStatement statement) {
if (parameters.length == 0) {
return;
}

ELProcessor processor = new ELProcessor();
processor.defineBean("self", target);
int p = 0;

for (String parameterExpression : parameters) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,7 @@ class ExpressionValidatorTest {
@BeforeEach
void setUp() {
target = new ExpressionValidator();
Mockito.when(annotation.when()).thenReturn("");
Mockito.when(annotation.node()).thenReturn("");
}

Expand Down Expand Up @@ -126,4 +127,40 @@ void testPrimitiveIntArray() {
target.initialize(annotation);
Assertions.assertTrue(target.isValid(data, context));
}

@Test
void testDateComparisonWhenConditionTrue() {
Mockito.when(annotation.value()).thenReturn("self.earlier lt self.later");
Mockito.when(annotation.when()).thenReturn("self.earlier.time eq 5");
Map<String, Date> data = new HashMap<>();
data.put("earlier", new Date(5));
data.put("later", new Date());
target.initialize(annotation);
Assertions.assertTrue(target.isValid(data, context));
}

@Test
void testDateComparisonWhenConditionFalse() {
Mockito.when(annotation.value()).thenReturn("self.earlier lt self.later");
Mockito.when(annotation.when()).thenReturn("self.earlier.time ne 1");
Map<String, Date> data = new HashMap<>();
data.put("earlier", new Date(1));
data.put("later", new Date());
target.initialize(annotation);
Assertions.assertTrue(target.isValid(data, context));
}

@Test
void testNonBooleanInWhenCondition() {
Mockito.when(annotation.value()).thenReturn("self.earlier lt self.later");
Mockito.when(annotation.when()).thenReturn("'0'");
Map<String, Date> data = new HashMap<>();
data.put("earlier", new Date(1));
data.put("later", new Date());
target.initialize(annotation);
ConstraintDeclarationException ex = assertThrows(ConstraintDeclarationException.class, () -> {
target.isValid(data, context);
});
Assertions.assertTrue(ex.getMessage().contains("`'0'` does not evaluate to Boolean"));
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.concurrent.atomic.AtomicInteger;

import javax.el.ELProcessor;
import javax.naming.Context;
import javax.naming.InitialContext;
import javax.naming.NamingException;
Expand All @@ -23,28 +24,60 @@
import javax.validation.ConstraintValidatorContext.ConstraintViolationBuilder.NodeBuilderCustomizableContext;
import javax.validation.ValidationException;

import org.junit.jupiter.api.Assertions;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.junit.platform.runner.JUnitPlatform;
import org.junit.runner.RunWith;
import org.mockito.Mock;
import org.mockito.Mockito;
import org.mockito.junit.jupiter.MockitoExtension;
import org.mockito.junit.jupiter.MockitoSettings;
import org.mockito.quality.Strictness;
import org.mockito.stubbing.Answer;

import io.xlate.validation.constraints.JdbcStatement;

@ExtendWith(MockitoExtension.class)
@RunWith(JUnitPlatform.class)
class JdbcStatementValidatorTest {

JdbcStatementValidator target;

@Mock
ConstraintValidatorContext constraintContext;

@BeforeEach
void setUp() {
target = new JdbcStatementValidator();
}

@Test
void testWhenExpessionFalse() throws NamingException {
JdbcStatement annotation = Mockito.mock(JdbcStatement.class);
Mockito.when(annotation.value()).thenReturn("SELECT 1");
Mockito.when(annotation.when()).thenReturn("0 == 1");
Mockito.when(annotation.dataSourceLookup()).thenReturn("");

DataSource dataSource = Mockito.mock(DataSource.class);
System.setProperty("java.naming.factory.initial", "org.osjava.sj.SimpleContextFactory");
System.setProperty("org.osjava.sj.jndi.shared", "true");
System.setProperty("org.osjava.sj.delimiter", "/");

Context context = new InitialContext();
context.createSubcontext("java:");
context.createSubcontext("java:comp");
context.bind("java:comp/DefaultDataSource", dataSource);

try {
target.initialize(annotation);
Assertions.assertTrue(target.isValid(new Object(), constraintContext));
} finally {
context.close();
}
}

@Test
void testGetDataSourceReturnsDefault() throws NamingException {
DataSource dataSource = Mockito.mock(DataSource.class);
Expand Down Expand Up @@ -110,7 +143,6 @@ void testGetDataSourceThrowsValidationExceptionCausedByNamingException() throws

@Test
void testExecuteQuerySucceeds() throws SQLException {
Object self = new Object();
String sql = "SELECT 1";
String[] parameters = { };

Expand All @@ -126,19 +158,18 @@ void testExecuteQuerySucceeds() throws SQLException {
Mockito.when(results.next()).thenReturn(true);

target.dataSource = dataSource;
assertTrue(target.executeQuery(self, sql, parameters));
assertTrue(target.executeQuery(null, sql, parameters));
}

@Test
void testExecuteQueryThrowsValidationException() throws SQLException {
Object self = new Object();
DataSource dataSource = Mockito.mock(DataSource.class);
String sql = "SELECT 1";
String[] parameters = { };
Mockito.when(dataSource.getConnection()).thenThrow(SQLException.class);
target.dataSource = dataSource;
ValidationException ex = assertThrows(ValidationException.class, () -> {
target.executeQuery(self, sql, parameters);
target.executeQuery(null, sql, parameters);
});

Throwable cause = ex.getCause();
Expand All @@ -149,7 +180,6 @@ void testExecuteQueryThrowsValidationException() throws SQLException {
@Test
@MockitoSettings(strictness = Strictness.LENIENT)
void testSetParametersNoParameters() throws SQLException {
Object self = new Object();
String[] parameters = { };
PreparedStatement statement = Mockito.mock(PreparedStatement.class);
final AtomicInteger callCount = new AtomicInteger(0);
Expand All @@ -159,7 +189,7 @@ void testSetParametersNoParameters() throws SQLException {
return null;
}).when(statement).setObject(1, Object.class);

target.setParameters(self, parameters, statement);
target.setParameters(null, parameters, statement);
assertEquals(0, callCount.get());
}

Expand Down Expand Up @@ -190,7 +220,10 @@ void testSetParametersSucceeds() throws SQLException {
return null;
}).when(statement).setObject(2, self.getValue2());

target.setParameters(self, parameters, statement);
ELProcessor processor = new ELProcessor();
processor.defineBean("self", self);

target.setParameters(processor, parameters, statement);
assertEquals(2, callCount.get());
}

Expand All @@ -211,8 +244,11 @@ void testSetParametersPropertyNotFound() {
String[] parameters = { "self.value1", "self.value2" };
PreparedStatement statement = Mockito.mock(PreparedStatement.class);

ELProcessor processor = new ELProcessor();
processor.defineBean("self", self);

ConstraintDeclarationException ex = assertThrows(ConstraintDeclarationException.class, () -> {
target.setParameters(self, parameters, statement);
target.setParameters(processor, parameters, statement);
});

Throwable cause = ex.getCause();
Expand All @@ -229,10 +265,12 @@ void testSetParametersSQLException() {
TestSetParametersSQLException self = new TestSetParametersSQLException();
String[] parameters = { "self" };
PreparedStatement statement = Mockito.mock(PreparedStatement.class);
ELProcessor processor = new ELProcessor();
processor.defineBean("self", self);

ConstraintDeclarationException ex = assertThrows(ConstraintDeclarationException.class, () -> {
Mockito.doThrow(java.sql.SQLException.class).when(statement).setObject(1, self);
target.setParameters(self, parameters, statement);
target.setParameters(processor, parameters, statement);
});

Throwable cause = ex.getCause();
Expand Down

0 comments on commit 933ca73

Please sign in to comment.