package cn.com.ty.lift.common.verify; import lombok.extern.slf4j.Slf4j; import javax.validation.constraints.*; import java.io.ObjectStreamException; import java.lang.annotation.Annotation; import java.lang.invoke.MethodHandle; import java.lang.invoke.MethodHandles; import java.lang.invoke.MethodType; import java.lang.reflect.Array; import java.lang.reflect.Field; import java.lang.reflect.InvocationTargetException; import java.lang.reflect.Method; import java.math.BigDecimal; import java.math.BigInteger; import java.net.IDN; import java.time.*; import java.time.chrono.HijrahDate; import java.time.chrono.JapaneseDate; import java.time.chrono.MinguoDate; import java.time.chrono.ThaiBuddhistDate; import java.time.temporal.TemporalAccessor; import java.util.*; import java.util.concurrent.ConcurrentHashMap; import java.util.regex.Matcher; import java.util.regex.PatternSyntaxException; import java.util.stream.Collectors; import static java.util.regex.Pattern.CASE_INSENSITIVE; /** * the validation for parameter implements {@linkplain javax.validation.constraints}, * reform from hibernate validator (v6.0.16.Final) * * @author wcz * @since 2020/2/15 */ @Slf4j public class VerifyProcessor { /** * the name of the verify method. */ private final String verifyMethod = "verify"; /** * the list of validation annotation & method can be work in {@code javax.validation.constraints.*} */ private final Map, Method> verifyMethodMap = new ConcurrentHashMap<>(); /** * whether write log to file. it's {@code true} by default. */ private boolean logWrite = true; /** * the message to complete verify. */ private final ThreadLocal message = new ThreadLocal<>(); /** * the type of message ,1: verify ,2: illegal argument */ private final ThreadLocal code = new ThreadLocal<>(); /** * the max length for a valid email address local part. */ private final int MAX_LOCAL_PART_LENGTH = 64; /** * the regular expression for local part of a valid email address. */ private final String LOCAL_PART_ATOM = "[a-z0-9!#$%&'*+/=?^_`{|}~\u0080-\uFFFF-]"; /** * the regular expression for the local part of an email address. */ private final String LOCAL_PART_INSIDE_QUOTES_ATOM = "([a-z0-9!#$%&'*.(),<>\\[\\]:; @+/=?^_`{|}~\u0080-\uFFFF-]|\\\\\\\\|\\\\\\\")"; /** * Regular expression for the local part of an email address (everything before '@') */ private final java.util.regex.Pattern LOCAL_PART_PATTERN = java.util.regex.Pattern.compile( "(" + LOCAL_PART_ATOM + "+|\"" + LOCAL_PART_INSIDE_QUOTES_ATOM + "+\")" + "(\\." + "(" + LOCAL_PART_ATOM + "+|\"" + LOCAL_PART_INSIDE_QUOTES_ATOM + "+\")" + ")*", CASE_INSENSITIVE ); /** * This is the maximum length of a domain name. But be aware that each label (parts separated by a dot) of the * domain name must be at most 63 characters long. This is verified by {@link IDN#toASCII(String)}. */ private final int MAX_DOMAIN_PART_LENGTH = 255; private final String DOMAIN_CHARS_WITHOUT_DASH = "[a-z\u0080-\uFFFF0-9!#$%&'*+/=?^_`{|}~]"; private final String DOMAIN_LABEL = "(" + DOMAIN_CHARS_WITHOUT_DASH + "-*)*" + DOMAIN_CHARS_WITHOUT_DASH + "+"; /** * the regex for check domain */ private final String DOMAIN = DOMAIN_LABEL + "+(\\." + DOMAIN_LABEL + "+)*"; /** * regex for check IP v4 */ private final String IP_DOMAIN = "[0-9]{1,3}\\.[0-9]{1,3}\\.[0-9]{1,3}\\.[0-9]{1,3}"; /** * IP v6 regex taken from http://stackoverflow.com/questions/53497/regular-expression-that-matches-valid-ipv6-addresses */ private final String IP_V6_DOMAIN = "(([0-9a-fA-F]{1,4}:){7,7}[0-9a-fA-F]{1,4}|([0-9a-fA-F]{1,4}:){1,7}:|([0-9a-fA-F]{1,4}:){1,6}:[0-9a-fA-F]{1,4}|([0-9a-fA-F]{1,4}:){1,5}(:[0-9a-fA-F]{1,4}){1,2}|([0-9a-fA-F]{1,4}:){1,4}(:[0-9a-fA-F]{1,4}){1,3}|([0-9a-fA-F]{1,4}:){1,3}(:[0-9a-fA-F]{1,4}){1,4}|([0-9a-fA-F]{1,4}:){1,2}(:[0-9a-fA-F]{1,4}){1,5}|[0-9a-fA-F]{1,4}:((:[0-9a-fA-F]{1,4}){1,6})|:((:[0-9a-fA-F]{1,4}){1,7}|:)|fe80:(:[0-9a-fA-F]{0,4}){0,4}%[0-9a-zA-Z]{1,}|::(ffff(:0{1,4}){0,1}:){0,1}((25[0-5]|(2[0-4]|1{0,1}[0-9]){0,1}[0-9])\\.){3,3}(25[0-5]|(2[0-4]|1{0,1}[0-9]){0,1}[0-9])|([0-9a-fA-F]{1,4}:){1,4}:((25[0-5]|(2[0-4]|1{0,1}[0-9]){0,1}[0-9])\\.){3,3}(25[0-5]|(2[0-4]|1{0,1}[0-9]){0,1}[0-9]))"; /** * Regular expression for the domain part of an URL * A host string must be a domain string, an IPv4 address string, or "[", followed by an IPv6 address string, followed by "]". */ private final java.util.regex.Pattern DOMAIN_PATTERN = java.util.regex.Pattern.compile(DOMAIN + "|\\[" + IP_V6_DOMAIN + "\\]", CASE_INSENSITIVE); /** * Regular expression for the domain part of an email address (everything after '@') */ private final java.util.regex.Pattern EMAIL_DOMAIN_PATTERN = java.util.regex.Pattern.compile( DOMAIN + "|\\[" + IP_DOMAIN + "\\]|" + "\\[IPv6:" + IP_V6_DOMAIN + "\\]", CASE_INSENSITIVE ); /** * the minimum value of compare two number for the infinity check when double or float. */ private final OptionalInt LESS_THAN = OptionalInt.of(-1); /** * the empty OptionalInt(value = 0) of compare two number for the infinity check when double or float. */ private final OptionalInt FINITE_VALUE = OptionalInt.empty(); /** * the maximun value of compare two number for the infinity check when double or float. */ private final OptionalInt GREATER_THAN = OptionalInt.of(1); /** * short 0 */ private final short SHORT_ZERO = (short) 0; /** * byte 0 */ private final byte BYTE_ZERO = (byte) 0; /** * to private the constrctor. */ private VerifyProcessor() { collectVerifyClassMethod(); } /** * collect all vaild validation annotation from the methods. */ private void collectVerifyClassMethod() { Method[] declaredMethods = this.getClass().getDeclaredMethods(); List verifyMethods = Arrays.stream(declaredMethods).filter(method -> verifyMethod.equals(method.getName())).collect(Collectors.toList()); isTrue(verifyMethods.isEmpty(), "No any method named 'verify' in VerifyProcessor."); for (Method method : verifyMethods) { Optional> classOptional = Arrays.stream(method.getParameterTypes()).filter(type -> Annotation.class.isAssignableFrom(type)).findFirst(); classOptional.ifPresent(verifyClass -> { if (!method.isAccessible()) { method.setAccessible(true); } verifyMethodMap.put(verifyClass, method); log.info("@interface: {}", verifyClass.getCanonicalName()); }); } isTrue(verifyMethodMap.isEmpty(), "No valid validation annotation was resolved in VerifyProcessor."); } /** * the static inner class to builder a singleton instance. */ private static class Builder { private static VerifyProcessor INSTANCE = new VerifyProcessor(); } /** * get the singleton instance of this */ private static VerifyProcessor processor() { return Builder.INSTANCE; } /** * to do verify by using the singleton instance. * * @param logWrite {@code true} log information, otherwise no log to file. * @param object the target object to verify. * @param fields the list of fields to verify. */ public static void perform(final boolean logWrite, final Object object, final String... fields) { Builder.INSTANCE.logWrite(logWrite).action(object, fields); } /** * to do verify by using the singleton instance. * * @param object the target object to verify. * @param fields the list of fields to verify. */ public static void perform(final Object object, final String... fields) { Builder.INSTANCE.action(object, fields); } /** * 因为单例模式虽然能保证线程安全,但在序列化和反序列化的情况下会出现生成多个对象的情况. * readResolve()方法,其实当JVM从内存中反序列化地"组装"一个新对象时, * 就会自动调用这个readResolve方法来返回指定好的对象, 单例规则也就得到了保证。 * readResolve()的出现允许程序员自行控制通过反序列化得到的对象。 */ protected Object readResolve() throws ObjectStreamException { return Builder.INSTANCE; } /** * setting whether write log to file. * * @param logWrite {@code true} to write log, {@code false} to no. * @return this instance. */ public VerifyProcessor logWrite(boolean logWrite) { this.logWrite = logWrite; return this; } /** * set the verify message in the ThreadLocal.(code : 1) */ private void setVerify(String message, Object... values) { this.code.set(1); this.message.set(format(message, values)); } /** * set the illegal argument message in the ThreadLocal.(code : 2) */ private void setIllegalArg(String message, Object... values) { this.code.set(2); this.message.set(format(message, values)); } /** * get the systemDefaultZone {@link Clock#systemDefaultZone()}. for the date compare. */ private Clock systemDefaultClock() { return Clock.offset(Clock.systemDefaultZone(), Duration.ZERO.abs().negated()); } /** * verify the value is {@code null} or no. * * @param value any value. * @return if return {@code true},that the value is null, otherwise no. */ private boolean isNull(Object value) { return null == value; } /** * verify the value isn't {@code null} or no. * * @param value any value. * @return if return {@code true}, that the value isn't noll, otherwise no. */ private boolean notNull(Object value) { return null != value; } /** * verify the int value is greater than zero. * * @param value any int value. * @return if return {@code true}, the value is greater than zero, otherwise no. */ private boolean gt0(int value) { return value > 0; } /** * verify the int value is less than zero. * * @param value any int value. * @return if return {@code true}, the value is less than zero, otherwise no */ private boolean lt0(int value) { return value < 0; } /** * verify the int value is greater than zero or equal to zero. * * @param value any int value. * @return if return {@code true}, the value is greater than zero or equal to zero, otherwise no */ private boolean gte0(int value) { return value >= 0; } /** * verify the int value is less than zero or equal to zero. * * @param value any int value. * @return if return {@code true}, the value is less than zero or equal to zero, otherwise no */ private boolean lte0(int value) { return value <= 0; } /** * verify the value is instance of the Boolean. * * @param value any value. * @return if return {@code true}, the value is Boolean, otherwise no */ private boolean isBoolean(Object value) { return (value instanceof Boolean); } /** * verify the value is instance of the Number. * * @param value any value. * @return if return {@code true}, the value is Number, otherwise no */ private boolean isNumber(Object value) { return (value instanceof Number); } /** * verify the value is instance of the CharSequence. * * @param value any value. * @return if return {@code true}, the value is CharSequence, otherwise no */ private boolean isCharSequence(Object value) { return (value instanceof CharSequence); } /** * verify the value is instance of the BigDecimal. * * @param value any value. * @return if return {@code true}, the value is BigDecimal, otherwise no */ private boolean isBigDecimal(Object value) { return (value instanceof BigDecimal); } /** * verify the value is instance of the BigInteger. * * @param value any value. * @return if return {@code true}, the value is BigInteger, otherwise no */ private boolean isBigInteger(Object value) { return (value instanceof BigInteger); } /** * verify the value is instance of the Long. * * @param value any value. * @return if return {@code true}, the value is Long, otherwise no */ private boolean isLong(Object value) { return (value instanceof Long); } /** * verify the value is instance of the Integer. * * @param value any value. * @return if return {@code true}, the value is Integer, otherwise no */ private boolean isInteger(Object value) { return (value instanceof Integer); } /** * verify the value is instance of the Double. * * @param value any value. * @return if return {@code true}, the value is Double, otherwise no */ private boolean isDouble(Object value) { return (value instanceof Double); } /** * verify the value is instance of the Float. * * @param value any value. * @return if return {@code true}, the value is Float, otherwise no */ private boolean isFloat(Object value) { return (value instanceof Float); } /** * verify the value is instance of the Byte. * * @param value any value. * @return if return {@code true}, the value is Byte, otherwise no */ private boolean isByte(Object value) { return (value instanceof Byte); } /** * verify the value is instance of the Short. * * @param value any value. * @return if return {@code true}, the value is Short, otherwise no */ private boolean isShort(Object value) { return (value instanceof Short); } /** * verify the value is instance of the String. * * @param value any value. * @return if return {@code true}, the value is String, otherwise no */ private boolean isString(Object value) { return (value instanceof String); } /** * verify the value is instance of the Collection. * * @param value any value. * @return if return {@code true}, the value is Collection, otherwise no */ private boolean isCollection(Object value) { return (value instanceof Collection); } /** * verify the value is instance of the Map. * * @param value any value. * @return if return {@code true}, the value is Map, otherwise no */ private boolean isMap(Object value) { return (value instanceof Map); } /** * verify the value is a Array. * * @param value any value. * @return if return {@code true}, the value is Array, otherwise no */ private boolean isArray(Object value) { return value.getClass().isArray(); } /** * verify the value is instance of the Iterator. * * @param value any value. * @return if return {@code true}, the value is Iterator, otherwise no */ private boolean isIterator(Object value) { return (value instanceof Iterator); } /** * verify the value is instance of the Enumeration. * * @param value any value. * @return if return {@code true}, the value is Enumeration, otherwise no */ private boolean isEnumeration(Object value) { return (value instanceof Enumeration); } /** * verify the value is instance of the TemporalAccessor(java 8). * * @param value any value. * @return if return {@code true}, the value is TemporalAccessor, otherwise no */ private boolean isTemporalAccessor(Object value) { return (value instanceof TemporalAccessor); } /** * verify the value is instance of the java.util.Date. * * @param value any value. * @return if return {@code true}, the value is Date, otherwise no */ private boolean isDate(Object value) { return (value instanceof Date); } /** * verify the value is instance of the java.util.Calendar. * * @param value any value. * @return if return true, the value is Calendar, otherwise no */ private boolean isCalendar(Object value) { return (value instanceof Calendar); } /** * new a BigDecimal object with a CharSequence value. * * @param value any CharSequence value * @return a new {@link BigDecimal} object or null when occur any exception. */ private BigDecimal newBigDecimal(CharSequence value) { try { BigDecimal bd; if (isString(value)) { bd = new BigDecimal((String) value); } else { bd = new BigDecimal(value.toString()); } return bd; } catch (Exception e) { setIllegalArg("Failed to convert '%s' to a valid BigDecimal. Exception: %s", value, e.getMessage()); return null; } } /** * new a BigDecimal object with a Number value, base on the Specific type of the value. * * @param value any Number value * @return a new {@link BigDecimal} object or null when occur any exception. */ private BigDecimal newBigDecimal(Number value) { try { BigDecimal bd; if (isLong(value)) { bd = BigDecimal.valueOf((Long) value); } else if (isBigDecimal(value)) { bd = ((BigDecimal) value); } else if (isBigInteger(value)) { bd = new BigDecimal((BigInteger) value); } else if (isDouble(value)) { bd = BigDecimal.valueOf((Double) value); } else if (isFloat(value)) { bd = BigDecimal.valueOf((Float) value); } else { bd = BigDecimal.valueOf(value.doubleValue()); } return bd; } catch (Exception e) { setIllegalArg("Failed to convert '%s' to a valid BigDecimal. Exception: %s", value, e.getMessage()); return null; } } /** * verify whether a double value is infinity with less or greater. * * @param number any double value. * @param treatNanAs when the value isn't a number, return this value. * @return a OptionalInt value with the result of compare to infinity. * -1 for equal {@link Double.NEGATIVE_INFINITY} * 1 for equal {@link Double.POSITIVE_INFINITY} */ private OptionalInt infinityCheck(Double number, OptionalInt treatNanAs) { OptionalInt result = FINITE_VALUE; if (number == Double.NEGATIVE_INFINITY) { result = LESS_THAN; } else if (number.isNaN()) { result = treatNanAs; } else if (number == Double.POSITIVE_INFINITY) { result = GREATER_THAN; } return result; } /** * verify whether a float vlaue is infinity with less or greater. * * @param number any float value. * @param treatNanAs when the value isn't a number, return this value. * @return a OptionalInt value with the result of compare to infinity. * -1 for equal {@link Float.NEGATIVE_INFINITY} * 1 for equal {@link Float.POSITIVE_INFINITY} */ private OptionalInt infinityCheck(Float number, OptionalInt treatNanAs) { OptionalInt result = FINITE_VALUE; if (number == Float.NEGATIVE_INFINITY) { result = LESS_THAN; } else if (number.isNaN()) { result = treatNanAs; } else if (number == Float.POSITIVE_INFINITY) { result = GREATER_THAN; } return result; } /** * Checks the validity of the domain name used in an email. To be valid it should be either a valid host name, or an * IP address wrapped in []. * * @param domain domain to check for validity * @return {@code true} if the provided string is a valid domain, {@code false} otherwise */ private boolean isValidEmailDomainAddress(String domain) { return isValidDomainAddress(domain, EMAIL_DOMAIN_PATTERN); } /** * Checks validity of a domain name. * * @param domain the domain to check for validity * @return {@code true} if the provided string is a valid domain, {@code false} otherwise */ private boolean isValidDomainAddress(String domain) { return isValidDomainAddress(domain, DOMAIN_PATTERN); } /** * verify validity of a domain name by the pattern. * * @param domain the domain to check for * @param pattern the pattern for the check. * @return if return {@code true} that the domain is valid, otherwise no. */ private boolean isValidDomainAddress(String domain, java.util.regex.Pattern pattern) { // if we have a trailing dot the domain part we have an invalid email address. // the regular expression match would take care of this, but IDN.toASCII drops the trailing '.' if (domain.endsWith(".")) { return false; } Matcher matcher = pattern.matcher(domain); if (!matcher.matches()) { return false; } String asciiString; try { asciiString = IDN.toASCII(domain); } catch (IllegalArgumentException e) { setIllegalArg("Failed to convert domain string to ASCII code. Exception: %s", e.getMessage()); return false; } if (isNull(asciiString) || asciiString.length() > MAX_DOMAIN_PART_LENGTH) { return false; } return true; } /** * verify whether the value isn't {@code null}. * * @param value any object value. * @param annotation the {@link NotNull} annotation get from the field. * @return if return {@code true} that the value isn't null, otherwise no. */ private boolean verify(Object value, NotNull annotation) { if (isNull(annotation)) { return true; } setVerify(annotation.message()); return notNull(value); } /** * verify whether the value is {@code null}. * * @param value any object value. * @param annotation the {@link Null} annotation get from the field. * @return if return {@code true} that the value is null, otherwise no. */ private boolean verify(Object value, Null annotation) { if (isNull(annotation)) { return true; } setVerify(annotation.message()); return isNull(value); } /** * verify whether the value is {@code true}. * * @param value any object value, but only work for {@link Boolean} * @param annotation the {@link AssertTrue} annotation get from the field. * @return if return {@code true} that the value is true, otherwise no. */ private boolean verify(Object value, AssertTrue annotation) { if (isNull(annotation)) { return true; } setVerify(annotation.message()); if (isNull(value)) { return false; } if (!isBoolean(value)) { setIllegalArg("The @AssertTrue only supports Boolean."); return false; } return ((Boolean) value); } /** * verify whether the value is {@code false}. * * @param value any object value, but only work for {@link Boolean} * @param annotation the {@link AssertFalse} annotation get from the field. * @return if return {@code true} that the value is false, otherwise no. */ private boolean verify(Object value, AssertFalse annotation) { if (isNull(annotation)) { return true; } setVerify(annotation.message()); if (isNull(value)) { return false; } if (!isBoolean(value)) { setIllegalArg("The @AssertFalse only supports Boolean."); return false; } return !((Boolean) value); } /** * verify whether the value is less than the bigdecimal maximum value. * * @param value any object value, but only work for {@link Number} and {@link CharSequence} * @param annotation the {@link DecimalMax} annotation get from the field. * @return if return {@code true} that the value is less than the bigdecimal maximum, otherwise no. */ private boolean verify(Object value, DecimalMax annotation) { if (isNull(annotation)) { return true; } setVerify(annotation.message()); if (isNull(value)) { return false; } final boolean isNumber = isNumber(value); final boolean isCharSequence = isCharSequence(value); if (!isNumber && !isCharSequence) { setIllegalArg("The @DecimalMax only supports Number & CharSequence."); return false; } final String maxValue = annotation.value(); if (isNull(maxValue)) { setIllegalArg("The value of @DecimalMax is null, a invalid BigDecimal format."); return false; } final BigDecimal max = newBigDecimal(maxValue); if (isNull(max)) { return false; } final BigDecimal val; if (isNumber) { val = newBigDecimal((Number) value); } else { val = newBigDecimal((CharSequence) value); } if (isNull(val)) { return false; } final int compare = decimalComparator(value, val, max, GREATER_THAN); final boolean inclusive = annotation.inclusive(); //inclusive ? comparisonResult <= 0 : comparisonResult < 0; if (inclusive) { return lte0(compare); } else { return lt0(compare); } } /** * verify whether the value is greater than the bigdecimal minimum value. * * @param value any object value, but only work for {@link Number} and {@link CharSequence} * @param annotation the {@link DecimalMin} annotation get from the field. * @return if return {@code true} that the value is greater than the bigdecimal minimum, otherwise no. */ private boolean verify(Object value, DecimalMin annotation) { if (isNull(annotation)) { return true; } setVerify(annotation.message()); if (isNull(value)) { return false; } final boolean isNumber = isNumber(value); final boolean isCharSequence = isCharSequence(value); if (!isNumber && !isCharSequence) { setIllegalArg("The @DecimalMin only supports Number & CharSequence."); return false; } final String minValue = annotation.value(); if (isNull(minValue)) { setIllegalArg("The value of @DecimalMin is null, a invalid BigDecimal format."); return false; } final BigDecimal min = newBigDecimal(minValue); if (isNull(min)) { return false; } final BigDecimal val; if (isNumber) { val = newBigDecimal((Number) value); } else { val = newBigDecimal((CharSequence) value); } if (isNull(val)) { return false; } final int compare = decimalComparator(value, val, min, LESS_THAN); final boolean inclusive = annotation.inclusive(); //inclusive ? comparisonResult >= 0 : comparisonResult > 0; if (inclusive) { return gte0(compare); } else { return gt0(compare); } } /** * compare two bigdecimal value * * @param value any object value * @param val the bigdecimal value transform from the object value * @param bounds the boundary of the maximum or minimum * @param treatNanAs return for the value isn't number when check for infinity * @return return {@code -1} for the val is less than boundary, {@code 0} for equal,{@code 1} for greater than boundary. */ private int decimalComparator(Object value, BigDecimal val, BigDecimal bounds, OptionalInt treatNanAs) { int compare; if (isLong(value) || isBigInteger(value) || isBigDecimal(value)) { compare = val.compareTo(bounds); } else if (isDouble(value)) { Double v = (Double) value; OptionalInt infinity = infinityCheck(v, treatNanAs); if (infinity.isPresent()) { compare = infinity.getAsInt(); } else { compare = val.compareTo(bounds); } } else if (isFloat(value)) { Float v = (Float) value; OptionalInt infinity = infinityCheck(v, treatNanAs); if (infinity.isPresent()) { compare = infinity.getAsInt(); } else { compare = val.compareTo(bounds); } } else { compare = val.compareTo(bounds); } return compare; } /** * verify whether the value isn't null and not empty. * * @param value any object value,but only work for {@link CharSequence},{@link Collection},{@link Map},{@link Array},{@link Iterator}and {@link Enumeration} * @param annotation the {@link NotEmpty} annotation get from the field. * @return if return {@code true} that the value isn't empty(with some length or size), otherwise no. */ private boolean verify(Object value, NotEmpty annotation) { if (isNull(annotation)) { return true; } setVerify(annotation.message()); if (isNull(value)) { return false; } final int length = length(value); if(-1 == length){ setIllegalArg("The @NotEmpty only supports CharSequence & Collection & Map & Array & Iterator & Enumeration."); return false; } // length > 0 return gt0(length); } /** * get the length or size of the value. * * @param value any object value * @return the length or size of the value when it's working, but return zero when the value is null. */ private int length(Object value) { if (isNull(value)) { return 0; } if (isCharSequence(value)) { return ((CharSequence) value).length(); } if (isCollection(value)) { return ((Collection) value).size(); } if (isMap(value)) { return ((Map) value).size(); } if (isArray(value)) { return Array.getLength(value); } int count; if (isIterator(value)) { Iterator iter = (Iterator) value; count = 0; while (iter.hasNext()) { count++; iter.next(); } return count; } if (isEnumeration(value)) { Enumeration enumeration = (Enumeration) value; count = 0; while (enumeration.hasMoreElements()) { count++; enumeration.nextElement(); } return count; } return -1; } /** * verify whether the value satisfy the size with maximum and minimun. * * @param value any object value, but only work for {@link CharSequence},{@link Collection},{@link Map},{@link Array},{@link Iterator}and {@link Enumeration} * @param annotation the {@link Size} annotation get from the field. * @return if return {@code true} that the value satisfy the size, otherwise no. */ private boolean verify(Object value, Size annotation) { if (isNull(annotation)) { return true; } final int min = annotation.min(); final int max = annotation.max(); String message = annotation.message(); if (notBlank(message)) { if(message.contains("{min}")){ message = message.replace("{min}", String.valueOf(min)); } if(message.contains("{max}")){ message = message.replace("{max}", String.valueOf(max)); } } setVerify(message); if (isNull(value)) { return false; } if (lt0(min)) { setIllegalArg("The min (%s) parameter of @Size cannot be negative.", min); return false; } if (lt0(max)) { setIllegalArg("The max (%s) parameter of @Size cannot be negative.", max); return false; } if (min > max) { setIllegalArg("The min (%s) and max (%s) length of @Size cannot be negative.", min, max); return false; } final int length = length(value); if(-1 == length){ setIllegalArg("The @Size only supports CharSequence & Collection & Map & Array & Iterator & Enumeration."); return false; } //size >= min && size <= max return length >= min && length <= max; } /** * verify that the {@code Number} being validated matches the pattern * * @param value any object value, but only work for {@link CharSequence} and {@link Number} * @param annotation the {@link Digits} annotation get from the field. * @return if return {@code true} that the value is matches pattern, otherwise no. */ private boolean verify(Object value, Digits annotation) { if (isNull(annotation)) { return true; } final int maxInteger = annotation.integer(); final int maxFraction = annotation.fraction(); String message = annotation.message(); if (notBlank(message)) { if(message.contains("{integer}")){ message = message.replace("{integer}", String.valueOf(maxInteger)); } if(message.contains("{fraction}")){ message = message.replace("{fraction}", String.valueOf(maxFraction)); } } setVerify(message); if (isNull(value)) { return false; } final boolean isNumber = isNumber(value); final boolean isCharSequence = isCharSequence(value); if (!isNumber && !isCharSequence) { setIllegalArg("The @Digits only supports Number & CharSequence."); return false; } if (lt0(maxInteger)) { setIllegalArg("The length of the integer '%s' part cannot be negative.", maxInteger); return false; } if (lt0(maxFraction)) { setIllegalArg("The length of the fraction '%s' part cannot be negative.", maxFraction); return false; } BigDecimal val; if (isNumber) { if (isBigDecimal(value)) { val = (BigDecimal) value; } else { val = newBigDecimal(value.toString()); if (isNull(val)) { return false; } val = val.stripTrailingZeros(); } } else { val = newBigDecimal((CharSequence) value); } if (isNull(val)) { return false; } int integerPart = val.precision() - val.scale(); int fractionPart = val.scale() < 0 ? 0 : val.scale(); //maxInteger >= integerPart && maxFraction >= fractionPart return maxInteger >= integerPart && maxFraction >= fractionPart; } /** * verify that a character sequence is not {@code null} nor empty after removing any leading or trailing whitespace. * * @param value any object value, but only work for {@link CharSequence} * @param annotation the {@link NotBlank} annotation get from the field. * @return if return {@code true} that the value isn't blank, otherwise no. */ private boolean verify(Object value, NotBlank annotation) { if (isNull(annotation)) { return true; } setVerify(annotation.message()); if (isNull(value)) { return false; } if (!isCharSequence(value)) { setIllegalArg("The @NotBlank only supports CharSequence."); return false; } return ((CharSequence) value).toString().trim().isEmpty(); } /** * verify whether the value is a valid email address. * * @param value any object value, but only work for {@link CharSequence} * @param annotation the {@link Email} annotation get from the field. * @return if return {@code true} that the value is a valid email address, otherwise no. */ private boolean verify(Object value, Email annotation) { if (isNull(annotation)) { return true; } setVerify(annotation.message()); if (isNull(value)) { return false; } if (!isCharSequence(value)) { setIllegalArg("The @Email only supports CharSequence."); return false; } //@Email need trim final String val = ((CharSequence) value).toString().trim(); if (val.isEmpty()) { return false; } // cannot split email string at @ as it can be a part of quoted local part of email. // so we need to split at a position of last @ present in the string: int splitPosition = val.lastIndexOf('@'); if (lt0(splitPosition)) { return false; } String localPart = val.substring(0, splitPosition); String domainPart = val.substring(splitPosition + 1); if (localPart.length() > MAX_LOCAL_PART_LENGTH) { return false; } if (!LOCAL_PART_PATTERN.matcher(localPart).matches()) { return false; } if (!isValidEmailDomainAddress(domainPart)) { return false; } final Pattern.Flag[] flags = annotation.flags(); final String regexp = annotation.regexp(); int intFlag = 0; for (Pattern.Flag flag : flags) { intFlag = intFlag | flag.getValue(); } java.util.regex.Pattern pattern = null; // we only apply the regexp if there is one to apply if (!".*".equals(regexp) || flags.length > 0) { try { pattern = java.util.regex.Pattern.compile(regexp, intFlag); } catch (PatternSyntaxException e) { setIllegalArg("Failed to Compile the regexp and flag for @Email. Exception: %s", e.getMessage()); return false; } } if (isNull(pattern)) { setIllegalArg("The regexp for @Email is Invalid regular expression."); return false; } return pattern.matcher(val).matches(); } /** * verify whether the value is matches the pattern. * * @param value any object value, but only work for {@link CharSequence} * @param annotation the {@link Pattern} annotation get from the field. * @return if return {@code true} that the value is mathches the pattern, otherwise no. */ private boolean verify(Object value, Pattern annotation) { if (isNull(annotation)) { return true; } setVerify(annotation.message()); if (isNull(value)) { return false; } if (!isCharSequence(value)) { setIllegalArg("The @Pattern only supports CharSequence."); return false; } //@Pattern no trim final String val = ((CharSequence) value).toString(); if (val.isEmpty()) { return false; } final Pattern.Flag[] flags = annotation.flags(); final String regexp = annotation.regexp(); int intFlag = 0; for (Pattern.Flag flag : flags) { intFlag = intFlag | flag.getValue(); } java.util.regex.Pattern pattern; try { pattern = java.util.regex.Pattern.compile(regexp, intFlag); } catch (PatternSyntaxException e) { setIllegalArg("Failed to Compile the regexp and flag for @Pattern. Exception: %s", e.getMessage()); return false; } if (isNull(pattern)) { setIllegalArg("The regexp for @Pattern is Invalid regular expression."); return false; } return pattern.matcher(val).matches(); } /** * verify whether the value less then or equal the maximum value. * * @param value any object value, but only work for {@link CharSequence} and {@link Number} * @param annotation the {@link Max} annotation get from the field. * @return if return {@code ture} that the value is less than or equal the maximum, otherwise no. */ private boolean verify(Object value, Max annotation) { if (isNull(annotation)) { return true; } setVerify(annotation.message()); if (isNull(value)) { return false; } final boolean isNumber = isNumber(value); final boolean isCharSequence = isCharSequence(value); if (!isCharSequence && !isNumber) { setIllegalArg("The @Max only supports Number & CharSequence."); return false; } final long max = annotation.value(); final int compare; if (isNumber) { compare = numberComparator((Number) value, max, GREATER_THAN); } else { String v = ((CharSequence) value).toString().trim(); if (v.isEmpty()) { return false; } BigDecimal val = newBigDecimal(v); if (isNull(val)) { return false; } compare = val.compareTo(BigDecimal.valueOf(max)); } //compare <= 0 return lte0(compare); } /** * verify whether the value greater then or equal the minimum value. * * @param value any object value, but only work for {@link CharSequence} and {@link Number} * @param annotation the {@link Max} annotation get from the field. * @return if return {@code ture} that the value is less than or equal the minimum, otherwise no. */ private boolean verify(Object value, Min annotation) { if (isNull(annotation)) { return true; } setVerify(annotation.message()); if (isNull(value)) { return false; } final boolean isNumber = isNumber(value); final boolean isCharSequence = isCharSequence(value); if (!isCharSequence && !isNumber) { setIllegalArg("The @Min only supports Number & CharSequence."); return false; } final long min = annotation.value(); final int compare; if (isNumber) { compare = numberComparator((Number) value, min, LESS_THAN); } else { String v = ((CharSequence) value).toString().trim(); if (v.isEmpty()) { return false; } BigDecimal val = newBigDecimal(v); if (isNull(val)) { return false; } compare = val.compareTo(BigDecimal.valueOf(min)); } //compare >= 0 return gte0(compare); } /** * compare two number value with less than or greater than. * * @param value any number value * @param bounds the other value * @param treatNanAs return when the value is {@code NaN}. * @return {@code -1} value less than limit, {@code 0} value equal limit, {@code 1} value greater than limit. */ private int numberComparator(Number value, long bounds, OptionalInt treatNanAs) { int compare; if (isLong(value)) { compare = ((Long) value).compareTo(bounds); } else if (isDouble(value)) { Double val = (Double) value; OptionalInt infinity = infinityCheck(val, treatNanAs); if (infinity.isPresent()) { compare = infinity.getAsInt(); } else { compare = Double.compare(val, bounds); } } else if (isFloat(value)) { Float val = (Float) value; OptionalInt infinity = infinityCheck(val, treatNanAs); if (infinity.isPresent()) { compare = infinity.getAsInt(); } else { compare = Float.compare(val, bounds); } } else if (isBigDecimal(value)) { compare = ((BigDecimal) value).compareTo(BigDecimal.valueOf(bounds)); } else if (isBigInteger(value)) { compare = ((BigInteger) value).compareTo(BigInteger.valueOf(bounds)); } else { compare = Long.compare(value.longValue(), bounds); } return compare; } /** * get the sign number of the value. * * @param value any number value * @param treatNanAs return when the value equal infinity. * @return {@code -1} the value less than zero, {@code 0} the value equal zero, {@code 1} the value greater than zero. */ private int signum(Number value, OptionalInt treatNanAs) { int signum; if (isLong(value)) { signum = Long.signum((Long) value); } else if (isInteger(value)) { signum = Integer.signum((Integer) value); } else if (isBigDecimal(value)) { signum = ((BigDecimal) value).signum(); } else if (isBigInteger(value)) { signum = ((BigInteger) value).signum(); } else if (isDouble(value)) { Double val = (Double) value; OptionalInt infinity = infinityCheck(val, treatNanAs); if (infinity.isPresent()) { signum = infinity.getAsInt(); } else { signum = val.compareTo(0D); } } else if (isFloat(value)) { Float val = (Float) value; OptionalInt infinity = infinityCheck(val, treatNanAs); if (infinity.isPresent()) { signum = infinity.getAsInt(); } else { signum = val.compareTo(0F); } } else if (isByte(value)) { signum = ((Byte) value).compareTo(BYTE_ZERO); } else if (isShort(value)) { signum = ((Short) value).compareTo(SHORT_ZERO); } else { signum = Double.compare(value.doubleValue(), 0D); } return signum; } /** * verify whether the value * * @param value any object value, but only work for {@link Number} * @param annotation the {@link Negative} annotation get from the field. * @return */ private boolean verify(Object value, Negative annotation) { if (isNull(annotation)) { return true; } setVerify(annotation.message()); if (isNull(value)) { return false; } if (!isNumber(value)) { setIllegalArg("The @Negative only supports Number."); return false; } return lt0(signum((Number) value, GREATER_THAN)); } /** * @param value any object value, but only work for {@link Number} * @param annotation the {@link NegativeOrZero} annotation get from the field. * @return */ private boolean verify(Object value, NegativeOrZero annotation) { if (isNull(annotation)) { return true; } setVerify(annotation.message()); if (isNull(value)) { return false; } if (!isNumber(value)) { setIllegalArg("The @NegativeOrZero only supports Number."); return false; } return lte0(signum((Number) value, GREATER_THAN)); } /** * @param value any object value, but only work for {@link Number} * @param annotation the {@link Positive} annotation get from the field. * @return */ private boolean verify(Object value, Positive annotation) { if (isNull(annotation)) { return true; } setVerify(annotation.message()); if (isNull(value)) { return false; } if (!isNumber(value)) { setIllegalArg("The @Positive only supports Number."); return false; } return gt0(signum((Number) value, LESS_THAN)); } /** * @param value any object value, but only work for {@link Number} * @param annotation the {@link PositiveOrZero} annotation get from the field. * @return */ private boolean verify(Object value, PositiveOrZero annotation) { if (isNull(annotation)) { return true; } setVerify(annotation.message()); if (isNull(value)) { return false; } if (!isNumber(value)) { setIllegalArg("The @PositiveOrZero only supports Number."); return false; } return gte0(signum((Number) value, LESS_THAN)); } /** * compare the TemporalAccessor value with now , verify whether the value is before or after now. * * @param value any object value, but only work for {@link TemporalAccessor}, {@link Date} and {@link Calendar} * @param isTemporalAccessor whether the value is instance of {@link TemporalAccessor} * @param isDate whether the value is instance of {@link Date} * @return {@code -1} the value is before now, {@code 0} is now, {@code 1} after now. otherwise {@code -2} not support the type. */ private int dateComparator(Object value, boolean isTemporalAccessor, boolean isDate) { Clock clock = systemDefaultClock(); int compare; if (isTemporalAccessor) { if (value instanceof Instant) { compare = ((Instant) value).compareTo(Instant.now(clock)); } else if (value instanceof LocalDateTime) { compare = ((LocalDateTime) value).compareTo(LocalDateTime.now(clock)); } else if (value instanceof LocalDate) { compare = ((LocalDate) value).compareTo(LocalDate.now(clock)); } else if (value instanceof LocalTime) { compare = ((LocalTime) value).compareTo(LocalTime.now(clock)); } else if (value instanceof MonthDay) { compare = ((MonthDay) value).compareTo(MonthDay.now(clock)); } else if (value instanceof HijrahDate) { compare = ((HijrahDate) value).compareTo(HijrahDate.now(clock)); } else if (value instanceof JapaneseDate) { compare = ((JapaneseDate) value).compareTo(JapaneseDate.now(clock)); } else if (value instanceof MinguoDate) { compare = ((MinguoDate) value).compareTo(MinguoDate.now(clock)); } else if (value instanceof OffsetDateTime) { compare = ((OffsetDateTime) value).compareTo(OffsetDateTime.now(clock)); } else if (value instanceof OffsetTime) { compare = ((OffsetTime) value).compareTo(OffsetTime.now(clock)); } else if (value instanceof ThaiBuddhistDate) { compare = ((ThaiBuddhistDate) value).compareTo(ThaiBuddhistDate.now(clock)); } else if (value instanceof Year) { compare = ((Year) value).compareTo(Year.now(clock)); } else if (value instanceof YearMonth) { compare = ((YearMonth) value).compareTo(YearMonth.now(clock)); } else if (value instanceof ZonedDateTime) { compare = ((ZonedDateTime) value).compareTo(ZonedDateTime.now(clock)); }else{ setIllegalArg("The '%s' is not a supported TemporalAccessor class temporarily.", value.getClass().getCanonicalName()); compare = Integer.MAX_VALUE; } } else if (isDate) { Date val = (Date) value; compare = val.toInstant().compareTo(Instant.now(clock)); } else { Calendar val = (Calendar) value; compare = val.toInstant().compareTo(Instant.now(clock)); } return compare; } /** * verify whether the value is a future time. * * @param value any object value, but only work for {@link TemporalAccessor}, {@link Date} and {@link Calendar} * @param annotation the {@link Future} annotation get from the field. * @return if return {@code true} that the value is a future time, otherwise no. */ private boolean verify(Object value, Future annotation) { if (isNull(annotation)) { return true; } setVerify(annotation.message()); if (isNull(value)) { return false; } final boolean isTemporalAccessor = isTemporalAccessor(value); final boolean isDate = isDate(value); final boolean isCalendar = isCalendar(value); if (!isTemporalAccessor && !isDate && !isCalendar) { setIllegalArg("The @Future only supports TemporalAccessor & Calendar & Date."); return false; } final int compare = dateComparator(value, isTemporalAccessor, isDate); if (Integer.MAX_VALUE == compare) { return false; } //compare > 0 return gt0(compare); } /** * verify whether the value is a future time or present. * * @param value any object value, but only work for {@link TemporalAccessor}, {@link Date} and {@link Calendar} * @param annotation the {@link FutureOrPresent} annotation get from the field. * @return if return {@code true} that the value is a future time or present, otherwise no. */ private boolean verify(Object value, FutureOrPresent annotation) { if (isNull(annotation)) { return true; } setVerify(annotation.message()); if (isNull(value)) { return false; } final boolean isTemporalAccessor = isTemporalAccessor(value); final boolean isDate = isDate(value); final boolean isCalendar = isCalendar(value); if (!isTemporalAccessor && !isDate && !isCalendar) { setIllegalArg("The @FutureOrPresent only supports TemporalAccessor & Calendar & Date."); return false; } final int compare = dateComparator(value, isTemporalAccessor, isDate); if (Integer.MAX_VALUE == compare) { return false; } //compare >= 0 return gte0(compare); } /** * verify whether the value is a past time. * * @param value any object value, but only work for {@link TemporalAccessor}, {@link Date} and {@link Calendar} * @param annotation the {@link Past} annotation get from the field. * @return if return {@code true} that the value is a past time, otherwise no. */ private boolean verify(Object value, Past annotation) { if (isNull(annotation)) { return true; } setVerify(annotation.message()); if (isNull(value)) { return false; } final boolean isTemporalAccessor = isTemporalAccessor(value); final boolean isDate = isDate(value); final boolean isCalendar = isCalendar(value); if (!isTemporalAccessor && !isDate && !isCalendar) { setIllegalArg("The @Past only supports TemporalAccessor & Calendar & Date."); return false; } final int compare = dateComparator(value, isTemporalAccessor, isDate); if (Integer.MAX_VALUE == compare) { return false; } //compare < 0 return lt0(compare); } /** * verify whether the value is a future time or present. * * @param value any object value, but only work for {@link TemporalAccessor}, {@link Date} and {@link Calendar} * @param annotation the {@link Future} annotation get from the field. * @return if return {@code true} that the value is a past time or present, otherwise no. */ private boolean verify(Object value, PastOrPresent annotation) { if (isNull(annotation)) { return true; } setVerify(annotation.message()); if (isNull(value)) { return false; } final boolean isTemporalAccessor = isTemporalAccessor(value); final boolean isDate = isDate(value); final boolean isCalendar = isCalendar(value); if (!isTemporalAccessor && !isDate && !isCalendar) { setIllegalArg("The @PastOrPresent only supports TemporalAccessor & Calendar & Date."); return false; } final int compare = dateComparator(value, isTemporalAccessor, isDate); if (Integer.MAX_VALUE == compare) { return false; } //compare <= 0 return lte0(compare); } /** * get field by the names * * @param object the target object * @param fields the names of field * @return list of field */ private List fieldFilter(final Object object, final String... fields) { Class objectClass = object.getClass(); // get fields, if has specify fields, otherwise get all fields on the object. if (isEmpty(fields)) { return Collections.unmodifiableList(Arrays.asList(objectClass.getDeclaredFields())); } List verifyFields = new ArrayList<>(); for (String field : fields) { try { Field verifyField = objectClass.getDeclaredField(field); verifyFields.add(verifyField); } catch (NoSuchFieldException e) { throw newIllegalArgException("No such field '%s' in : %s.", field, objectClass.getCanonicalName()); } } //if vaild fields is empty if (verifyFields.isEmpty()) { throw newIllegalArgException("No field specify to verify."); } return Collections.unmodifiableList(verifyFields); } /** * get all validation annotation on the field * * @param field the target field * @return list of validation annotation */ private List annotationFilter(final Field field) { if (!field.isAccessible()) { field.setAccessible(true); } //get all annotation of the field Annotation[] annotations = field.getDeclaredAnnotations(); if (isEmpty(annotations)) { return null; } //filter the validation annotation List verifyAnnos = Arrays.stream(annotations).filter(anno -> verifyMethodMap.keySet().contains(anno.annotationType())).collect(Collectors.toList()); if (verifyAnnos.isEmpty()) { return null; } return Collections.unmodifiableList(verifyAnnos); } /** * get value of the field on the object * * @param object the target object * @param field the field * @return the value */ private Object getFieldValue(final Object object, final Field field) { final Object value; try { value = field.get(object); } catch (IllegalAccessException e) { throw newIllegalArgException("Illegal Access '%s' in : %s.", field.getName(), object.getClass().getCanonicalName()); } return value; } private MethodHandle methodFilter(final MethodHandles.Lookup lookup, final String methodName, final MethodType methodType) { final MethodHandle methodHandle; try { methodHandle = lookup.findVirtual(lookup.lookupClass(), methodName, methodType); } catch (NoSuchMethodException | IllegalAccessException e) { throw newIllegalArgException("No such method %s%s in VerifyProcessor.", methodName, methodType); } return methodHandle; } private boolean invokeMethod(final Object target, final MethodHandle methodHandle, final Object... args) { final boolean invoke; try { invoke = (boolean) methodHandle.bindTo(target).invokeWithArguments(args); } catch (Throwable throwable) { throw newIllegalArgException("Invoke the target method [%s] failed.", methodHandle); } return invoke; } /** * invoke the target method to verify. */ private boolean invokeMethod(final Object target, final Method method, final Object value, final Annotation annotation) { final boolean invoke; try { invoke = (boolean) method.invoke(target, value, annotation); } catch (IllegalAccessException | InvocationTargetException e) { throw newIllegalArgException("Invoke the target method [%s(Object,%s)] failed.", method.getName(), annotation.annotationType().getName()); } return invoke; } /** * verify the fields of the object base on the annotation present on it. * if it is not valid that will throw a {@link VerifyException}. * or a {@link IllegalArgumentException} when argument is illegal. * * @param object any object value. with some validation annotation in {@code javax.validation.constraints.*} * @param fields the list of fields to verify. if no, that will verify all fields in the object. */ private void action(final Object object, final String... fields) { isNull(object, "The object to verify must not be null."); List verifyFields = fieldFilter(object, fields); for (Field verifyField : verifyFields) { //filter the validation annotation List verifyAnnos = annotationFilter(verifyField); if (isEmpty(verifyAnnos)) continue; //get value. Object value = getFieldValue(object, verifyField); for (Annotation verifyAnno : verifyAnnos) { Class annotationType = verifyAnno.annotationType(); //get method Method method = verifyMethodMap.get(annotationType); // result. boolean invoke = invokeMethod(this, method, value, verifyAnno); if (invoke) continue; if (logWrite) { info(verifyField, value, method, verifyAnno); } if(1 == code.get()){ throw newVerifyException(message.get()); }else{ throw newIllegalArgException(message.get()); } } } } /** * the format of the {@code info} level log. * * @param validField the field to valid. * @param value the value of the field. * @param method the method to action * @param verifyAnno the validation annotation of the field. */ private void info(Field verifyField, Object value, Method method, Annotation verifyAnno) { log.info("###| FIELD : {}", verifyField); log.info("###| FIELD_VALUE : {}", value); log.info("###| METHOD : (false) {}(Object,{})", method.getName(), verifyAnno.annotationType().getName()); } /** * create a {@link IllegalArgumentException} with the message. * * @param message the message of exception. * @return a {@link IllegalArgumentException} */ private IllegalArgumentException newIllegalArgException(String message, Object... values) { return new IllegalArgumentException(format(message, values)); } /** * create a {@link VerifyException} with the message. * * @param message the message of exception. * @return a {@link VerifyException} */ private VerifyException newVerifyException(String message, Object... values) { return new VerifyException(format(message, values)); } /** * format the message with some values * * @param message the message string. * @param values some values. * @return the message has format if necessary */ private String format(String message, Object... values) { if(message.contains("%s") && null != values && values.length > 0) { return String.format(message, values); } return message; } /** * verify whether the expression is {@code true}. * * @param expression any boolean expression. * @param message if {@code true} that throw a {@link IllegalArgumentException} with message. */ private void isNull(Object object, String message) { if (null == object) { throw newIllegalArgException(message); } } private void isTrue(boolean expression, String message) { if (expression) { throw newIllegalArgException(message); } } public boolean isEmpty(T[] array) { return array == null || array.length == 0; } public boolean isEmpty(Iterable iterable) { return null == iterable || isEmpty(iterable.iterator()); } public boolean isEmpty(Iterator Iterator) { return null == Iterator || !Iterator.hasNext(); } public boolean notBlank(String value) { return null != value && !value.trim().isEmpty(); } }