Skip to content
Open
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.
*/
private 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 @@ -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

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What about #{ here?

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
Original file line number Diff line number Diff line change
Expand Up @@ -80,6 +80,8 @@ private static <T> Set<T> set(Supplier<T[]> array) {

private final Set<ConstraintDescriptor<?>> composingConstraints;
private final List<Class<? extends ConstraintValidator<A, ?>>> constraintValidatorClasses;
// racy single-check memoization: the annotation is immutable, so concurrent initializations agree
private String messageTemplate;
private final Lazy<String> toString =
new Lazy<>(() -> String.format("%s: %s", ConstraintD.class.getSimpleName(), getAnnotation()));

Expand Down Expand Up @@ -142,7 +144,12 @@ public boolean isReportAsSingleViolation() {

@Override
public String getMessageTemplate() {
return read(ConstraintAnnotationAttributes.MESSAGE, Optionality.REQUIRED);
String result = messageTemplate;
if (result == null) {
result = read(ConstraintAnnotationAttributes.MESSAGE, Optionality.REQUIRED);
messageTemplate = result;
}
return result;
}

@Override
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,105 @@ public class EMailValidationUtils {
* @return <code>true</code> if the validation passes
*/
public static boolean isValid(Object value) {
return isValid(value, DEFAULT_EMAIL_PATTERN);
if (value == null) {
return true;
}
if (!(value instanceof CharSequence)) {
return false;
}
final CharSequence seq = (CharSequence) value;
return seq.length() == 0 || matchesDefaultPattern(seq);
}

/**
* Equivalent to {@code DEFAULT_EMAIL_PATTERN.matcher(seq).matches()} without the regular expression engine: one
* or more dot-separated runs of atom characters, {@code @}, then either such runs or a bracketed dotted quad.
*/
private static boolean matchesDefaultPattern(CharSequence seq) {
final int length = seq.length();
final int at = matchAtomRuns(seq, 0, length);
if (at < 0 || at >= length || seq.charAt(at) != '@') {
return false;
}
final int domain = at + 1;
return matchAtomRuns(seq, domain, length) == length || matchIpDomain(seq, domain, length);
}

/**
* Match {@code ATOM+(\.ATOM+)*} starting at {@code start} as far as possible.
*
* @return the index after the match, or {@code -1} if there is none
*/
private static int matchAtomRuns(CharSequence seq, int start, int length) {
int i = start;
while (true) {
final int runStart = i;
while (i < length && isAtom(seq.charAt(i))) {
i++;
}
if (i == runStart) {
// an empty run: fail at the start, otherwise back off the dot that introduced it
return runStart == start ? -1 : runStart - 1;
}
if (i < length && seq.charAt(i) == '.') {
i++;
} else {
return i;
}
}
}

private static boolean isAtom(char c) {
if (c <= 0x20) {
// control characters, and the space of \s; the other \s characters are control characters
return false;
}
switch (c) {
case '(':
case ')':
case '<':
case '>':
case '@':
case ',':
case ';':
case ':':
case '\\':
case '"':
case '.':
case '[':
case ']':
return false;
default:
return true;
}
}

/** Match {@code \[d{1,3}\.d{1,3}\.d{1,3}\.d{1,3}\]} spanning exactly {@code [start, length)}. */
private static boolean matchIpDomain(CharSequence seq, int start, int length) {
if (length - start < 9 || seq.charAt(start) != '[' || seq.charAt(length - 1) != ']') {
return false;
}
int i = start + 1;
for (int part = 0; part < 4; part++) {
if (part > 0) {
if (seq.charAt(i) != '.') {
return false;
}
i++;
}
final int digitsStart = i;
while (i < length - 1 && i - digitsStart < 3 && isDigit(seq.charAt(i))) {
i++;
}
if (i == digitsStart) {
return false;
}
}
return i == length - 1;
}

private static boolean isDigit(char c) {
return c >= '0' && c <= '9';
}

/**
Expand Down
Loading
Loading