Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,12 @@ public interface PatternDescriptor {

protected Pattern pattern;

/**
* Whether {@link #pattern} is {@code .*} without flags, the default of {@code @Email}, which matches exactly the
* values without a line terminator.
*/
private boolean matchesAnyLine;

protected AbstractPatternValidator(Function<A, PatternDescriptor> toDescriptor) {
super();
this.toDescriptor = Validate.notNull(toDescriptor);
Expand All @@ -60,13 +66,33 @@ public void initialize(A constraintAnnotation) {
}
try {
pattern = Pattern.compile(pd.regexp(), intFlag);
matchesAnyLine = intFlag == 0 && ".*".equals(pd.regexp());
} catch (PatternSyntaxException e) {
throw new IllegalArgumentException("Invalid regular expression.", e);
}
}

@Override
public boolean isValid(T value, ConstraintValidatorContext context) {
return value == null || pattern.matcher(value).matches();
if (value == null) {
return true;
}
return matchesAnyLine ? !containsLineTerminator(value) : pattern.matcher(value).matches();
}

private static boolean containsLineTerminator(CharSequence value) {
for (int i = 0, n = value.length(); i < n; i++) {
switch (value.charAt(i)) {
case '\n':
case '\r':
case '\u0085':
case '\u2028':
case '\u2029':
return true;
default:
break;
}
}
return false;
}
}
47 changes: 27 additions & 20 deletions bval-jsr/src/main/java/org/apache/bval/el/ELFacade.java
Original file line number Diff line number Diff line change
Expand Up @@ -34,27 +34,10 @@
import jakarta.el.ValueExpression;
import jakarta.el.VariableMapper;

import org.apache.bval.jsr.util.LookBehindRegexHolder;
import org.apache.bval.util.Lazy;

// ELProcessor or JavaEE 7 would be perfect too but this impl can be used in javaee 6
public final class ELFacade implements MessageEvaluator {
private enum EvaluationType {
IMMEDIATE("\\$"), DEFERRED("#");

/**
* {@link LookBehindRegexHolder} to recognize a non-escaped EL
* expression of this evaluation type, hallmarked by a trigger
* character.
*/
private final LookBehindRegexHolder regex;

private EvaluationType(String trigger) {
this.regex = new LookBehindRegexHolder(
String.format("(?<!(?:^|[^\\\\])(?:\\\\\\\\){0,%%d}\\\\)%s\\{", trigger), n -> (n - 3) / 2);
}
}

private static final ELResolver RESOLVER = initResolver();

// Lazily initialized: ExpressionFactory.newInstance() performs a relatively expensive
Expand All @@ -78,16 +61,40 @@ public String interpolate(final String message, final Map<String, Object> annota
expressionFactory.createValueExpression(validatedValue, Object.class));

// Java Bean Validation does not support EL expressions that look like JSP "deferred" expressions
return expressionFactory.createValueExpression(context,
EvaluationType.DEFERRED.regex.matcher(message).replaceAll("\\$0"), String.class).getValue(context)
.toString();
return expressionFactory.createValueExpression(context, disarmDeferredExpressions(message), String.class)
.getValue(context).toString();
} catch (final Exception e) {
// no-op
}
}
return message;
}

/**
* Replace each {@code #{} that is not escaped by an odd number of preceding backslashes with {@code $0}, so that
* it is not evaluated as a deferred expression.
*/
static String disarmDeferredExpressions(String message) {
int trigger = message.indexOf("#{");
if (trigger < 0) {
return message;
}
final StringBuilder result = new StringBuilder(message.length());
int copied = 0;
do {
int backslashes = 0;
while (trigger - backslashes > 0 && message.charAt(trigger - backslashes - 1) == '\\') {
backslashes++;
}
if (backslashes % 2 == 0) {
result.append(message, copied, trigger).append("$0");
copied = trigger + 2;
}
trigger = message.indexOf("#{", trigger + 2);
} while (trigger >= 0);
return result.append(message, copied, message.length()).toString();
}

private static ELResolver initResolver() {
final CompositeELResolver resolver = new CompositeELResolver();
resolver.add(new MapELResolver());
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -83,8 +83,17 @@ public ConstraintViolationImpl(String messageTemplate, String message, T rootBea
this.elementType = elementType;
this.returnValue = returnValue;
this.parameters = parameters;
this.hashCode = Arrays.deepHashCode(new Object[] { messageTemplate, message, rootBean, rootBeanClass, leafBean,
value, propertyPath, elementType, constraintDescriptor, returnValue, parameters });
int h = Objects.hashCode(messageTemplate);
h = 31 * h + Objects.hashCode(message);
h = 31 * h + Objects.hashCode(rootBean);
h = 31 * h + Objects.hashCode(rootBeanClass);
h = 31 * h + Objects.hashCode(leafBean);
h = 31 * h + Objects.hashCode(value);
h = 31 * h + Objects.hashCode(propertyPath);
h = 31 * h + Objects.hashCode(elementType);
h = 31 * h + Objects.hashCode(constraintDescriptor);
h = 31 * h + Objects.hashCode(returnValue);
this.hashCode = 31 * h + Arrays.deepHashCode(parameters);
}

/**
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -87,6 +87,8 @@ public Enumeration<String> getKeys() {
private final ConcurrentMap<ComputedMessageKey, String> interpolatedMessages = new ConcurrentHashMap<>();
private final ConcurrentMap<Class<?>, Method> toStringMethods = new ConcurrentHashMap<>();
private final ConcurrentMap<MessageWithParamsKey, String> interpolations = new ConcurrentHashMap<>();
/** Fully interpolated messages that involve no EL evaluation and so do not depend on the validated value. */
private final ConcurrentMap<FinalMessageKey, String> finalMessages = new ConcurrentHashMap<>();

private final MessageEvaluator evaluator;

Expand Down Expand Up @@ -134,17 +136,27 @@ public String interpolate(final String message, final Context context) {
/** {@inheritDoc} */
@Override
public String interpolate(final String message, final Context context, final Locale locale) {
final Map<String, Object> annotationParameters = context.getConstraintDescriptor().getAttributes();
final FinalMessageKey finalKey = new FinalMessageKey(locale, message, annotationParameters);
final String cached = finalMessages.get(finalKey);
if (cached != null) {
return cached;
}
if (!message.contains("{")) {
return resolveEscapeSequences(message);
}

String resolvedMessage = getSharedInterpolatedMessage(message, locale);

// resolve annotation attributes (step 4)
final Map<String, Object> annotationParameters = context.getConstraintDescriptor().getAttributes();
resolvedMessage = replaceAnnotationAttributes(resolvedMessage, annotationParameters);

// EL handling
// EL handling; the evaluator leaves messages without "${" untouched
if (!resolvedMessage.contains("${")) {
final String result = resolveEscapeSequences(resolvedMessage);
finalMessages.putIfAbsent(finalKey, result);
return result;
}
if (evaluateExpressionLanguage(message, context)) {
resolvedMessage = evaluator.interpolate(resolvedMessage, annotationParameters, context.getValidatedValue());
}
Expand Down Expand Up @@ -323,6 +335,7 @@ private String replaceAnnotationAttributes(final String message, final Map<Strin
public void clearCache() {
interpolatedMessages.clear();
interpolations.clear();
finalMessages.clear();
}

private Method getToStringMethod(final Object variable) {
Expand Down Expand Up @@ -392,6 +405,46 @@ public int hashCode() {
}
}

/**
* Keys the parameter map by identity, like {@link MessageWithParamsKey}.
*/
private static class FinalMessageKey {
private final Locale locale;
private final String message;
private final Map<String, Object> annotationParameters;
private final int hash;

private FinalMessageKey(final Locale locale, final String message,
final Map<String, Object> annotationParameters) {
this.locale = locale;
this.message = message;
this.annotationParameters = annotationParameters;
this.hash = 31 * (31 * locale.hashCode() + message.hashCode()) + System.identityHashCode(annotationParameters);
}

@Override
public boolean equals(final Object o) {
if (this == o) {
return true;
}
if (o == null || getClass() != o.getClass()) {
return false;
}
final FinalMessageKey that = (FinalMessageKey) o;
return annotationParameters == that.annotationParameters && message.equals(that.message)
&& locale.equals(that.locale);
}

@Override
public int hashCode() {
return hash;
}
}

/**
* Keys the parameter map by identity: a constraint descriptor hands out the same attribute map instance on every
* call, so this avoids hashing and comparing the whole map for each interpolation.
*/
private static class MessageWithParamsKey {
private final String message;
private final Map<String, Object> annotationParameters;
Expand All @@ -400,7 +453,7 @@ private static class MessageWithParamsKey {
private MessageWithParamsKey(final String message, final Map<String, Object> annotationParameters) {
this.message = message;
this.annotationParameters = annotationParameters;
this.hash = Objects.hash(message, annotationParameters);
this.hash = 31 * message.hashCode() + System.identityHashCode(annotationParameters);
}

@Override
Expand All @@ -412,7 +465,7 @@ public boolean equals(final Object o) {
return false;
}
final MessageWithParamsKey that = MessageWithParamsKey.class.cast(o);
return message.equals(that.message) && annotationParameters.equals(that.annotationParameters);
return annotationParameters == that.annotationParameters && message.equals(that.message);
}

@Override
Expand Down
47 changes: 41 additions & 6 deletions bval-jsr/src/main/java/org/apache/bval/jsr/GraphContext.java
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,12 @@ public ApacheFactoryContext getValidatorContext() {
}

public PathImpl getPath() {
final PathImpl p = path;
if (p == null && pendingNode == null) {
// derive a private copy straight from the parent rather than materializing this context's path only to
// copy it again; the mutation appends fresh nodes, so nothing is shared with other copies
return derivePath();
}
return PathImpl.copy(pathReference());
}

Expand All @@ -77,17 +83,46 @@ public PathImpl getPath() {
public PathImpl pathReference() {
PathImpl p = path;
if (p == null) {
p = PathImpl.copy(parent.pathReference());
if (pendingNode != null) {
p.addNode(pendingNode);
} else {
pendingMutation.accept(p);
}
p = derivePath();
path = p;
}
return p;
}

private PathImpl derivePath() {
final PathImpl p = PathImpl.copy(parent.pathReference());
if (pendingNode != null) {
p.addNode(pendingNode);
} else {
pendingMutation.accept(p);
}
return p;
}

/**
* Get the leaf node of this context's path without materializing the path when it is only pending.
*
* @return {@link NodeImpl}
*/
public NodeImpl getLeafNode() {
return path == null && pendingNode != null ? pendingNode : pathReference().getLeafNode();
}

/**
* Learn whether {@code other}'s path equals this context's path with its leaf node removed, as by
* {@link PathImpl#removeLeafNode()}.
*
* @param other
* @return {@code boolean}
*/
public boolean hasParentPath(GraphContext other) {
if (path == null && pendingNode != null && !parent.pathReference().isRootPath()) {
// our path is the parent's with pendingNode appended, so removing the leaf yields the parent's path
return other == parent || other.pathReference().equals(parent.pathReference());
}
return pathReference().isParentPath(other.pathReference());
}

public Object getValue() {
return value;
}
Expand Down
Loading
Loading