/*
*
* * Copyright (c) 2016. David Sowerby
* *
* * Licensed 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 uk.q3c.krail.core.validation;
import com.google.inject.Inject;
import org.apache.bval.constraints.Email;
import org.apache.bval.constraints.NotEmpty;
import org.apache.bval.jsr303.ConstraintAnnotationAttributes;
import org.apache.commons.lang3.ClassUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import uk.q3c.krail.core.config.ConfigurationException;
import uk.q3c.krail.core.i18n.CurrentLocale;
import uk.q3c.krail.core.i18n.I18NKey;
import uk.q3c.krail.core.i18n.Translate;
import uk.q3c.util.MessageFormat;
import javax.validation.MessageInterpolator;
import javax.validation.constraints.Min;
import java.lang.annotation.Annotation;
import java.util.Locale;
import java.util.Map;
import java.util.Optional;
/**
* Krail specific implementation of {@link MessageInterpolator}. This implementation supports the following uses of the JSR303 annotation message
* parameter provided by Apache BVal. (This includes the two additional, BVal specific annotations of @{@link Email} and @{@link NotEmpty}):<ol>
* <li>no value (that is, uses the annotation default) - looks up the associated {@link ValidationKey}, and uses the Krail I18N translation method</li>
* <li>a custom key, used for a single instance of an annotation, as a String representing the full qualified name of an I18NKey constant, enclosed in curly
* brackets, for example '{com.example.i18n.LabelKey.Misty}' - this will find the appropriate key (assuming it exists of course) and use that with the Krail
* I18N translation process</li>
* <li>A custom pattern, a String without curly brackets, which is used as it is - arguments can be placed within it using the format defined by {@link
* MessageFormat}, but no translation takes place</li>
* <li>A custom annotation, which should use an I18NKey</li>
* <p>
* </ol>
* see also: http://krail.readthedocs.org/en/latest/tutorial14/ and <br>
* http://krail.readthedocs.org/en/latest/devguide14/
* <p>
* <p>
* <p>
* Created by David Sowerby on 04/02/15.
*/
public class KrailInterpolator implements MessageInterpolator {
private static Logger log = LoggerFactory.getLogger(KrailInterpolator.class);
private final CurrentLocale currentLocale;
private final Translate translate;
private Map<Class<? extends Annotation>, I18NKey> javaxValidationSubstitutes;
@Inject
protected KrailInterpolator(CurrentLocale currentLocale, Translate translate, @JavaxValidationSubstitutes Map<Class<? extends Annotation>, I18NKey>
javaxValidationSubstitutes) {
this.currentLocale = currentLocale;
this.translate = translate;
this.javaxValidationSubstitutes = javaxValidationSubstitutes;
}
/**
* Calls {@link #interpolate(String, Context, Locale)} with {@link CurrentLocale#getLocale()}
*
* @param pattern
* The pattern to interpolate.
* @param context
* contextual information related to the interpolation
*
* @return Interpolated error message.
*/
@Override
public String interpolate(String pattern, Context context) {
return interpolate(pattern, context, currentLocale.getLocale());
}
/**
* Interpolate the message pattern based on the constraint validation context. Javax constraint annotations can be
* used without changes, but standard javax messages can be replaced if required, and will be translated using
* Krail
* It is assumed that any custom validation constraints use this method:
* see: https://sites.google.com/site/q3cjava/validation#TOC-Create-a-Custom-Validation
* <p><p>
*
* @param patternOrKey
* The message pattern, or if it enclosed in "{}", the key to a message pattern
* @param context
* contextual information related to the interpolation
* @param locale
* the locale targeted for the message
*
* @return Interpolated error message - a message pattern, translated where possible, with parameters filled in
*/
@Override
public String interpolate(String patternOrKey, Context context, Locale locale) {
//standard annotation with substituted key unless it has a custom message
if (isJavaxAnnotation(context) || isBValAnnotation(context)) {
if (isCustomMessage(patternOrKey, context)) {
return processStandardAnnotationWithCustomMessage(patternOrKey, context, locale);
} else {
I18NKey i18NKey = krailSubstitute(patternOrKey, context).get();
return translateKey(i18NKey, context, locale);
}
}
return processCustomAnnotation(patternOrKey, context, locale);
}
protected String processCustomAnnotation(String patternOrKey, Context context, Locale locale) {
Map<String, Object> attributes = context.getConstraintDescriptor()
.getAttributes();
//if it has a valid messageKey() process it
if (hasKrailMessageKeyAttribute(context)) {
I18NKey i18NKey = (I18NKey) attributes.get("messageKey");
if (i18NKey == null) {
throw new ConfigurationException("A custom validation annotation must have a messageKey() method and return value of type I18NKey");
}
return translateKey(i18NKey, context, locale);
} else {
throw new ConfigurationException("A custom validation annotation must have a messageKey() method and return value of type I18NKey");
}
}
protected boolean hasKrailMessageKeyAttribute(Context context) {
return annotationHasAttribute("messageKey", context);
}
protected boolean annotationHasAttribute(String attributeName, Context context) {
return context.getConstraintDescriptor()
.getAttributes()
.containsKey(attributeName);
}
/**
* Processes a standard javax or BVal annotation with a custom (non-default) message. This could be a
*
* @param patternOrKey
* @param context
*
* @param locale
* @return
*/
protected String processStandardAnnotationWithCustomMessage(String patternOrKey, Context context, Locale locale) {
if (isPattern(patternOrKey)) {
return MessageFormat.format(patternOrKey, context.getConstraintDescriptor()
.getAttributes()
.get("value"));
}
I18NKey i18NKey = findI18NKey(patternOrKey);
return translateKey(i18NKey, context, locale);
}
/**
* Returns true if {@code patternOrKey} is a pattern, false if it is a message key (determined by a key being
* surrounded with curly braces
*
* @param patternOrKey
* the pattern or key to assess
*
* @return returns true if {@code patternOrKey} is a pattern, false if it is a message key
*/
protected boolean isPattern(String patternOrKey) {
String s = patternOrKey.trim();
if (!s.startsWith("{")) {
return true;
}
return !s.endsWith("}");
}
/**
* Translates the {@code i18NKey} for the given {@code locale}.
*
* @param context
* @param locale
*
* @return
*/
protected String translateKey(I18NKey i18NKey, Context context, Locale locale) {
Map<String, Object> attributes = context.getConstraintDescriptor()
.getAttributes();
return translate.from(i18NKey, locale, attributes.get("value"));
}
/**
* Find a an I18NKey from its full string representation (for example uk.q3c.krail.core.i18n.LabelKey.Yes). The full
* string representation can be obtained using {@link I18NKey#fullName(I18NKey)}
*
* @param keyName
*
* @return the I18NKey for the supplied name, or null if not found for any reason
*/
protected I18NKey findI18NKey(String keyName) {
String k = keyName.replace("{", "")
.replace("}", "")
.trim();
//This is cheating, using ClassUtils to split by '.', these are not package and class names
String enumClassName = ClassUtils.getPackageCanonicalName(k);
String constantName = ClassUtils.getShortClassName(k);
Enum<?> enumConstant;
try {
Class<Enum> enumClass = (Class<Enum>) Class.forName(enumClassName);
enumConstant = Enum.valueOf(enumClass, constantName);
} catch (Exception e) {
log.warn("Could not find an I18NKey for {}", k);
enumConstant = null;
}
return (I18NKey) enumConstant;
}
/**
* Returns true if the annotation in the {@code context} is in the javax.validation.constraints package
*
* @param context
*
* @return
*/
protected boolean isJavaxAnnotation(Context context) {
String annotationClassName = annotationClass(context).getName();
String javaxPackageName = ClassUtils.getPackageCanonicalName(Min.class);
return annotationClassName.startsWith(javaxPackageName);
}
/**
* The annotation held by the descriptor can be a proxy (don't know whether it always is or sometimes), but
* annotationType seems to work where getClass() does not
*
* @param context
*
* @return
*/
protected Class<? extends Annotation> annotationClass(Context context) {
Annotation annotation = context.getConstraintDescriptor()
.getAnnotation();
return annotation.annotationType();
}
/**
* Returns true if the annotation in the {@code context} is in the org.apache.bval.constraints package
*
* @param context
*
* @return
*/
protected boolean isBValAnnotation(Context context) {
String annotationClassName = annotationClass(context).getName();
String bvalPackageName = ClassUtils.getPackageCanonicalName(Email.class);
return annotationClassName.startsWith(bvalPackageName);
}
protected Optional<I18NKey> krailSubstitute(String patternOrKey, Context context) {
I18NKey i18NKey = javaxValidationSubstitutes.get(annotationClass(context));
if (i18NKey == null) {
return Optional.empty();
} else {
return Optional.of(i18NKey);
}
}
/**
* Returns true if the message for the annotation is a custom message. False indicates that the default message for the annotation is being used.. Only
* valid for use with the message attribute (javax or Bval) not the messageKey from a custom annotation
*
* @param patternOrKey
* @param context
*
* @return true if the message is the default for the annotation. False indicates that an explicit message has been
* set for this annotation instance. Only valid for use with the message attribute not the messageKey
*/
protected boolean isCustomMessage(String patternOrKey, Context context) {
Object defaultValue = ConstraintAnnotationAttributes.MESSAGE.getDefaultValue(annotationClass(context));
return !(patternOrKey.equals(defaultValue));
}
protected boolean hasKrailSubstitute(String patternOrKey, Context context) {
return krailSubstitute(patternOrKey, context).isPresent();
}
/**
* If all we have is a pattern, the best we can do is try and fill in the parameters, but we can't translate it
*
* @param patternOrKey
* the I18N pattern, or if in curly braces, the I18NKey which will provide the pattern
*
* @return
*/
private String formatPattern(String patternOrKey) {
return MessageFormat.format(patternOrKey);
}
/**
* Returns true if {@code patternOrKey} is a pattern, and is from a standard javax.validation constraint annotation
* (therefore not a custom constraint)
*
* @param patternOrKey
* the I18N pattern, or a String representation of the I18NKey which will provide the pattern
* @param context
*
* @return true if {@code patternOrKey} is a pattern, and is from a standard javax.validation constraint annotation
* (therefore not a custom constraint)
*/
protected boolean isJavaxPattern(String patternOrKey, Context context) {
if (isPattern(patternOrKey)) {
return isJavaxAnnotation(context);
}
return false;
}
/**
* Identifies an unsubstituted javax message key
*
* @param patternOrKey
*
* @return
*/
protected boolean isJavaxMessageKey(String patternOrKey, Context context) {
if (!isPattern(patternOrKey)) {
if ((patternOrKey.contains("javax.validation.constraints")) || (patternOrKey.contains("org.apache.bval"))) {
return true;
}
}
return false;
}
}