JPA year validation throwing exception for years that are not exactly 4 digits long

Viewed 443

I want to validate form in Spring application using JPA and I have a problem with field production year. The validation succeeds when I use a year with exactly 4 digits, like 2010. However, when I enter into form value less than 1000 or greater than 9999 (value with not 4 digits) I get this exception:

Failed to convert property value of type java.lang.String to 
required type java.time.Year for property productionYear; 
nested exception is org.springframework.core.convert.ConversionFailedException: 
Failed to convert from type [java.lang.String] to 
type [@javax.persistence.Convert @javax.persistence.Column 
@javax.validation.constraints.NotNull @com.kucharek.motorcycleshop.data.ProductionYearConstraint 
java.time.Year] for value 12345; 
nested exception is java.lang.IllegalArgumentException: 
Parse attempt failed for value [12345]

SQL declaration of that field:

production_year year not null
check (production_year between
  year("1885-01-01") and year(current_date())
)

Java declaration in the entity class:

@Convert(converter = YearAttributeConverter.class)
@Column(name = "production_year")
@NotNull(message = "Production year is obligatory")
@ProductionYearConstraint(message = "Production year should be between 1885 and current year")
private Year productionYear;

Converter class:

@Converter(autoApply = true)
public class YearAttributeConverter
        implements AttributeConverter<Year, Short> {

    @Override
    public Short convertToDatabaseColumn(Year attribute) {
        return (short) attribute.getValue();
    }

    @Override
    public Year convertToEntityAttribute(Short dbData) {
        return Year.of(dbData);
    }
}

Annotation for validation:

@Constraint(validatedBy = ProductionYearValidator.class)
@Target(ElementType.FIELD)
@Retention(RetentionPolicy.RUNTIME)
public @interface ProductionYearConstraint {
    String message() default "Invalid production year";
    Class<?>[] groups() default {};
    Class<? extends Payload>[] payload() default {};
}

Validator class:

public class ProductionYearValidator
    implements ConstraintValidator<ProductionYearConstraint, Year> {

    @Override
    public boolean isValid(Year year, ConstraintValidatorContext context) {
        return year != null && year.getValue() >= 1885 &&
                year.getValue() < Year.now().getValue() ;
    }
}

What did I do wrong? Should I change type of data in database or in model? How can I handle exception and print to user my custom error message instead of print stack trace?

3 Answers

I've solved this issue with custom messages file.

Configuration class to configure custom messages file messages.yml:

@Configuration
    public class MessageSourceConfiguration {

    @Bean
    public MessageSource messageSource() throws IOException {
        ResourceBundleMessageSource messageSource = new ResourceBundleMessageSource();
        messageSource.setCommonMessages(yamlProperties());
        return messageSource;
   }

    @Bean
    public Properties yamlProperties() throws IOException {
        YamlPropertiesFactoryBean bean = new YamlPropertiesFactoryBean();
        bean.setResources(new ClassPathResource("messages.yml"));
        return bean.getObject();
    }
}

File resources/messages.yml:

typeMismatch:
  motorcycle:
    productionYear: Production year should be number between 1885 and current year

If you read the documentation, i.e. the javadoc of Year.parse(text), you will find:

The string must represent a valid year. Years outside the range 0000 to 9999 must be prefixed by the plus or minus symbol.

The real question is, do you actually want to support years outside the 0000 to 9999 range?

Can the database actually store such a year value? Doesn't actually matter, since the check (production_year between year("1885-01-01") and year(current_date())) restricts year to the 1885 to 2020 range anyway, so why is the parse issue a problem for you?

But, if you do want to support such a year value, you need to supply a String to Time converter, using:

Year.parse(yearString, DateTimeFormatter.ofPattern("u"))

One solution is to upgrade your application to Java 16 or later.

Prior to Java 16, the Year.parse(CharSequence text) method required the year text to be exactly four digits long (with zero padding allowed at the front) — i.e. between 0000 and 9999.

The string must represent a valid year. Years outside the range 0000 to 9999 must be prefixed by the plus or minus symbol.

It looks like the underlying libraries are using this method on the input Year when determining the error message, which is causing the exception you are experiencing.

Starting with Java 16, this four-digit restriction has been lifted, as shown in the parse method Javadocs:

The string must represent a valid year.

This change was made per JDK-8245448 in the Java bug tracker.

Related