BVAL-170 more aggressive caching of interpolated messages
diff --git a/bval-jsr/src/main/java/org/apache/bval/jsr/DefaultMessageInterpolator.java b/bval-jsr/src/main/java/org/apache/bval/jsr/DefaultMessageInterpolator.java
index a3eb78a..ca3e8ab 100644
--- a/bval-jsr/src/main/java/org/apache/bval/jsr/DefaultMessageInterpolator.java
+++ b/bval-jsr/src/main/java/org/apache/bval/jsr/DefaultMessageInterpolator.java
@@ -21,6 +21,7 @@
 import static java.util.Optional.of;
 
 import java.lang.reflect.InvocationTargetException;
+import java.lang.reflect.Method;
 import java.util.Arrays;
 import java.util.Enumeration;
 import java.util.Locale;
@@ -84,6 +85,8 @@
     private final Map<Locale, ResourceBundle> defaultBundlesMap = new ConcurrentHashMap<>();
 
     private final ConcurrentMap<MessageKey, String> cachedMessages = new ConcurrentHashMap<>();
+    private final ConcurrentMap<Class<?>, Method> toStringMethods = new ConcurrentHashMap<>();
+    private final ConcurrentMap<MessageWithParamsKey, String> interpolations = new ConcurrentHashMap<>();
 
     private final MessageEvaluator evaluator;
 
@@ -270,38 +273,62 @@
     }
 
     private String replaceAnnotationAttributes(final String message, final Map<String, Object> annotationParameters) {
-        final Matcher matcher = MESSAGE_PARAMETER.matcher(message);
-        final StringBuilder sb = new StringBuilder(64);
-        int prev = 0;
-        while (matcher.find()) {
-            int start = matcher.start();
-            String resolvedParameterValue;
-            String parameter = matcher.group(1);
-            Object variable = annotationParameters.get(parameter);
-            if (variable == null) {
-                resolvedParameterValue = matcher.group();
-            } else if (Object[].class.isInstance(variable)) {
-                resolvedParameterValue = Arrays.toString((Object[]) variable);
-            } else if (variable.getClass().isArray()) {
-                try {
-                    resolvedParameterValue = (String) Reflection
-                        .getDeclaredMethod(Arrays.class, "toString", variable.getClass()).invoke(null, variable);
-                } catch (IllegalAccessException | IllegalArgumentException | InvocationTargetException e) {
-                    throw new IllegalStateException("Could not expand array " + variable);
-                }
+        final MessageWithParamsKey key = new MessageWithParamsKey(message, annotationParameters);
+        String result = interpolations.get(key);
+        if (result == null) {
+            final Matcher matcher = MESSAGE_PARAMETER.matcher(message);
+            if (!matcher.find()) {
+                result = message;
             } else {
-                resolvedParameterValue = variable.toString();
+                final StringBuilder sb = new StringBuilder(64);
+                int prev = 0;
+                do {
+                    int start = matcher.start();
+                    String resolvedParameterValue;
+                    String parameter = matcher.group(1);
+                    Object variable = annotationParameters.get(parameter);
+                    if (variable == null) {
+                        resolvedParameterValue = matcher.group();
+                    } else if (Object[].class.isInstance(variable)) {
+                        resolvedParameterValue = Arrays.toString((Object[]) variable);
+                    } else if (variable.getClass().isArray()) {
+                        try {
+                            resolvedParameterValue = (String) getToStringMethod(variable).invoke(null, variable);
+                        } catch (IllegalAccessException | IllegalArgumentException | InvocationTargetException e) {
+                            throw new IllegalStateException("Could not expand array " + variable);
+                        }
+                    } else {
+                        resolvedParameterValue = variable.toString();
+                    }
+                    if (start > prev) {
+                        sb.append(message, prev, start);
+                    }
+                    sb.append(resolvedParameterValue);
+                    prev = matcher.end();
+                } while (matcher.find());
+                if (prev < message.length()) {
+                    sb.append(message, prev, message.length());
+                }
+                result = sb.toString();
             }
-            if (start > prev) {
-                sb.append(message, prev, start);
-            }
-            sb.append(resolvedParameterValue);
-            prev = matcher.end();
+            interpolations.putIfAbsent(key, result);
         }
-        if (prev < message.length()) {
-            sb.append(message, prev, message.length());
+        return result;
+    }
+
+    public void clearCache() {
+        cachedMessages.clear();
+        interpolations.clear();
+    }
+
+    private Method getToStringMethod(final Object variable) {
+        final Class<?> variableClass = variable.getClass();
+        Method method = toStringMethods.get(variableClass);
+        if (method == null) {
+            method = Reflection.getDeclaredMethod(Arrays.class, "toString", variableClass);;
+            toStringMethods.putIfAbsent(variableClass, method);
         }
-        return sb.toString();
+        return method;
     }
 
     private Optional<String> resolveParameter(String parameterName, ResourceBundle bundle, Locale locale,
@@ -329,6 +356,7 @@
      */
     public void setLocale(Locale locale) {
         defaultLocale = locale;
+        clearCache(); // cause there is a probability of 50% the old cache will be replaced by the new locale
     }
 
     private static class MessageKey {
@@ -361,4 +389,33 @@
             return hash;
         }
     }
+
+    private static class MessageWithParamsKey {
+        private final String message;
+        private final Map<String, Object> annotationParameters;
+        private final int hash;
+
+        private MessageWithParamsKey(final String message, final Map<String, Object> annotationParameters) {
+            this.message = message;
+            this.annotationParameters = annotationParameters;
+            this.hash = Objects.hash(message, annotationParameters);
+        }
+
+        @Override
+        public boolean equals(final Object o) {
+            if (this == o) {
+                return true;
+            }
+            if (o == null || getClass() != o.getClass()) {
+                return false;
+            }
+            final MessageWithParamsKey that = MessageWithParamsKey.class.cast(o);
+            return message.equals(that.message) && annotationParameters.equals(that.annotationParameters);
+        }
+
+        @Override
+        public int hashCode() {
+            return hash;
+        }
+    }
 }
diff --git a/bval-jsr/src/main/java/org/apache/bval/jsr/util/AnnotationProxy.java b/bval-jsr/src/main/java/org/apache/bval/jsr/util/AnnotationProxy.java
index 1d21189..2443f42 100644
--- a/bval-jsr/src/main/java/org/apache/bval/jsr/util/AnnotationProxy.java
+++ b/bval-jsr/src/main/java/org/apache/bval/jsr/util/AnnotationProxy.java
@@ -43,8 +43,6 @@
 
     /** Serialization version */
     private static final long serialVersionUID = 1L;
-    
-    private Signature EQUALS = new Signature("equals", Object.class);
 
     private final Class<? extends Annotation> annotationType;
     private final SortedMap<String, Object> values;
@@ -80,10 +78,11 @@
      */
     @Override
     public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
-        if (values.containsKey(method.getName())) {
-            return values.get(method.getName());
+        final Object value = values.get(method.getName());
+        if (value != null) {
+            return value;
         }
-        if (EQUALS.equals(Signature.of(method))) {
+        if (method.getName().equals("equals")) { // this is an annotation, no need to be more fancy (and slower)
             return equalTo(args[0]);
         }
         return method.invoke(this, args);