uk.q3c.krail.core.validation.DefaultKrailInterpolator.java Source code

Java tutorial

Introduction

Here is the source code for uk.q3c.krail.core.validation.DefaultKrailInterpolator.java

Source

/*
 * Copyright (c) 2015. 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.jsr303.ConstraintAnnotationAttributes;
import org.apache.commons.lang3.ClassUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import uk.q3c.krail.i18n.CurrentLocale;
import uk.q3c.krail.i18n.I18NKey;
import uk.q3c.krail.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;
import java.util.Set;

/**
 * Created by David Sowerby on 04/02/15.
 */
public class DefaultKrailInterpolator implements KrailInterpolator {
    private static Logger log = LoggerFactory.getLogger(DefaultKrailInterpolator.class);
    private final CurrentLocale currentLocale;
    private final Translate translate;
    private MessageInterpolator defaultMessageInterpolator;
    private Set<Class<? extends I18NKey>> fieldNameBundles;
    private Map<Class<? extends Annotation>, I18NKey> javaxValidationSubstitutes;

    @Inject
    protected DefaultKrailInterpolator(CurrentLocale currentLocale, Translate translate,
            MessageInterpolator defaultMessageInterpolator,
            @JavaxValidationSubstitutes Map<Class<? extends Annotation>, I18NKey> javaxValidationSubstitutes,
            @FieldNameBundles Set<Class<? extends I18NKey>> fieldNameBundles) {
        this.currentLocale = currentLocale;
        this.translate = translate;
        this.defaultMessageInterpolator = defaultMessageInterpolator;
        this.javaxValidationSubstitutes = javaxValidationSubstitutes;
        this.fieldNameBundles = fieldNameBundles;
    }

    /**
     * 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());
    }

    /**
     * see: https://sites.google.com/site/q3cjava/validation for more information about how to set up validation
     * <p>
     * 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) {

        Map<String, Object> attributes = context.getConstraintDescriptor().getAttributes();

        //if it has a Krail message key, just process it
        if (hasKrailMessageKey(context)) {
            I18NKey i18NKey = (I18NKey) attributes.get("messageKey");
            return translateKey(i18NKey, context, locale);
        }

        // it is not a Krail key by default, but there could be a substitution
        // but substitutions are overruled by explicit messages, so we'll do those first

        if (isDefaultMessage(patternOrKey, context)) {

            if (hasKrailSubstitute(patternOrKey, context)) {
                //get the substitution and process it
                I18NKey i18NKey = krailSubstitute(patternOrKey, context).get();
                return translateKey(i18NKey, context, locale);
            } else {
                //it is not a krail key or substitute, it must be using javax standard
                return defaultMessageInterpolator.interpolate(patternOrKey, context, locale);
            }

        } else {//explicit message
            if (isPattern(patternOrKey)) {
                //pattern is just processed as it is
                return formatPattern(patternOrKey);
            } else {
                //it's a key, but is it Krail or Javax?
                if (isJavaxMessageKey(patternOrKey, context)) {
                    //javax is delegated to default interpolator
                    return defaultMessageInterpolator.interpolate(patternOrKey, context, locale);
                } else {
                    // find the Krail key from the test
                    I18NKey key = findI18NKey(patternOrKey);

                    //if we can't find it, return it as it is
                    //otherwise translate and return it
                    if (key == null) {
                        return patternOrKey;
                    } else {
                        return translateKey(key, context, locale);
                    }
                }
            }
        }

    }

    protected boolean hasKrailSubstitute(String patternOrKey, Context context) {
        return krailSubstitute(patternOrKey, context).isPresent();
    }

    protected boolean hasKrailMessageKey(Context context) {
        return annotationHasAttribute("messageKey", context);
    }

    protected boolean annotationHasAttribute(String attributeName, Context context) {
        return context.getConstraintDescriptor().getAttributes().containsKey(attributeName);
    }

    /**
     * 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
     *
     * @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
     * @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;
    }

    /**
     * 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 {@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;
        }
        if (!s.endsWith("}")) {
            return true;
        }
        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;
    }

    /**
     * Translates the {@code i18NKey} for the given {@code locale}.  Optionally, the field name can be included in
     * the message, as determined by {@link SimpleContext#isUseFieldNamesInMessages()} but must always be at {0} in the
     * pattern. The field value is applied at {1} in the pattern.<p>
     * Field names can also translated, but that requires one or more field name bundles to be defined by
     * {@link KrailValidationModule#addFieldNameBundle (Class)},  If no field name bundles are provided, or a
     * translation cannot be made for any reason, then the field name itself is used.<p>
     *
     * @param context
     * @param locale
     *
     * @return
     */
    protected <E extends Enum<E> & I18NKey> String translateKey(I18NKey i18NKey, Context context, Locale locale) {

        //has to be an Object because it could be a key or a string
        // default to an empty string, equivalent to not using field name in message

        Object fieldNameArg = "";

        //if we want to use the field name in the message we also need to translate it
        if (((SimpleContext) context).isUseFieldNamesInMessages()) {
            E key = (E) i18NKey;
            SimpleContext simpleContext = (SimpleContext) context;
            String fieldName = simpleContext.getPropertyName();
            Enum nameEnum = null;
            for (Class<? extends I18NKey> fieldNameBundle : fieldNameBundles) {
                nameEnum = Enum.valueOf(I18NKey.enumClass(key), fieldName);
                if (nameEnum != null) {
                    break;
                }
            }
            if (nameEnum != null) {
                fieldNameArg = nameEnum;
            } else {
                fieldNameArg = fieldName;
            }

        }

        Map<String, Object> attributes = context.getConstraintDescriptor().getAttributes();

        return translate.from(i18NKey, locale, fieldNameArg, attributes.get("value")).trim();

    }

    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 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
     *
     * @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 isDefaultMessage(String patternOrKey, Context context) {

        Object defaultValue = ConstraintAnnotationAttributes.MESSAGE.getDefaultValue(annotationClass(context));
        return patternOrKey.equals(defaultValue);
    }

    /**
     * Find a an I18NKey from its full string representation (for example uk.q3c.krail.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;
        }
        I18NKey key = (I18NKey) enumConstant;
        return key;
    }

}