Custom Annotation-driven Formatting Spring MVC

Issue

I am migrating a java EE application to spring boot and i got stuck at a converting problem.
Now whether its good or not i stored my currencies as Long (its german euro).
I wrote a custom jsf converter that does something like that:

Long -> String

22 -> 00,22

3310 -> 33,10

String -> Long

3 -> 3

22,11 -> 2211

Now Spring MVC was one reason to move away from JSF.
I would like to make use of 303 Beanvalidation, with Spring MVC (@Valid @ModelAttribute, BindingResult which works fine for @Pattern e.g)

Now i cant use @NumberFormat(style=Style.Currency), which would do what I want, if I have not stored my currency as long.

I wrote a custom Formatter and registered it to FormatterRegistry

public class LongCurrencyFormatter implements Formatter<Long>{

@Getter
private static final long serialVersionUID = 1L;

@Override
public String print(Long arg0, Locale arg1) {
  //logic removed for shorter post
}

@Override
public Long parse(String arg0, Locale arg1) throws ParseException {
    //logic removed for shorter post
}
}

to this point everthing is working, but now every long is converted. What I think is right.
So after some research I looked into 6.6.2 Annotation-driven Formatting
http://docs.spring.io/spring/docs/current/spring-framework-reference/html/validation.html

I created as in the documentation an AnnotationFormatterFactory

public class LongCurrencyFormatAnnotationFormatterFactory 
implements AnnotationFormatterFactory<LongCurrency> {

@Override
public Set<Class<?>> getFieldTypes() {

    Set<Class<?>> setTypes = new HashSet<Class<?>>();
    setTypes.add(Long.class);
    return setTypes;
}

@Override
public Parser<?> getParser(LongCurrency annotation, Class<?> fieldType) {
    return new LongCurrencyFormatter();
}

@Override
public Printer<?> getPrinter(LongCurrency annotation, Class<?> fieldType) {
    return new LongCurrencyFormatter();
}


}

My annotation:

public @interface LongCurrency {

}

My Bean:

public class Costunit {

  //other attributes

  @LongCurrency
  private long value; 
}

sadly it is not working :
Failed to convert property value of type java.lang.String to required type long for property value; nested exception is java.lang.NumberFormatException: For input string: “22,00”

Sorry for the long post, any idea what i did wrong ? Or any better Solution to bind a formatter to only one controller? A Databasemirgration should be the very least option.

Thank you!

EDIT1: full Formatter code (works but could be better of course)

import java.text.DecimalFormat;
import java.text.DecimalFormatSymbols;
import java.text.ParseException;
import java.util.Locale;
import java.util.regex.Pattern;

import lombok.Getter;

import org.springframework.format.Formatter;

public class LongCurrencyFormatter implements Formatter<Long>{

@Getter
private static final long serialVersionUID = 1L;

@Override
public String print(Long arg0, Locale arg1) {

    String returnValue = arg0.toString();
    boolean minusChar =  returnValue.startsWith("-");
    returnValue = returnValue.replace("-", "");

    if (returnValue.length() > 2) {

        String tempStr = returnValue.substring(0, returnValue.length()-2);
        Long val = Long.parseLong(tempStr);

        DecimalFormat df = new DecimalFormat();
        df.setDecimalFormatSymbols(DecimalFormatSymbols.getInstance(Locale.GERMAN)); 

        String output = df.format(val) + "," + 
        returnValue.substring(returnValue.length()-2);
        returnValue = output;

    } else {

        if(returnValue.length() == 1) {
            returnValue = "0,0"+returnValue;
        } else {
            returnValue = "0,"+returnValue;
        }

    }

    if(minusChar) {
        returnValue = "-" + returnValue;
    }

    return returnValue;
}

@Override
public Long parse(String arg0, Locale arg1) throws ParseException {

    Long returnLong = null;

    // 1Test :only one - in front, only digits and "." and one "," , and
    // only 2 digits behind ","
    // if "," only 2 not 1 digit behind
    if (!isValidateLongCurrency(arg0)) {

        returnLong = 0L;

    } else {

        String valueFiltered = arg0.replace(".", "");

        // 2: add 2 00 if no ",":
        if (!valueFiltered.contains(",")) {
            valueFiltered += "00";
        }
        else {

            //E,C or E,CC
            String[] splittedValue = valueFiltered.split(",");
            if(splittedValue[splittedValue.length-1].length() == 1) {
                valueFiltered = valueFiltered + 0; 
            }

            valueFiltered = valueFiltered.replace(",", "");
        }
        try {
            returnLong = new Long(valueFiltered);
        } catch (NumberFormatException numEx) {

        }
    }
    return returnLong;
}

private boolean isValidateLongCurrency(String value) {
    boolean returnValue = true;
    String valueFiltered = value.replace(".", "");

    //Euro
    String regEx = "^-?[1-9][0-9]*(,[0-9][0-9]?)?$|^-?[0-9](,[0-9][0-9]?)?$|^$";

    returnValue = Pattern.matches( regEx, valueFiltered ) ;


    return returnValue;
}
}

EDIT 2, now its works

Changes made:

import java.lang.annotation.*;

@Target(value={ElementType.METHOD,ElementType.FIELD,ElementType.PARAMETER})
@Retention(value=RetentionPolicy.RUNTIME)
public @interface LongCurrency {

}

@Override
public void addFormatters(FormatterRegistry registry) {
    super.addFormatters(registry);
    registry.addFormatterForFieldAnnotation(new 
    LongCurrencyFormatAnnotationFormatterFactory());
}

Thanks to M. Deinum

Solution

For starters your annotation isn’t there anymore. You need to make sure it is retained at runtime, by default annotations are removed. For this add the @Retention meta annotation on your annotation. You probably also want to add the @Target annotation to specify on which types it can be set.

@Target({ElementType.METHOD, ElementType.FIELD, ElementType.PARAMETER, ElementType.ANNOTATION_TYPE})
@Retention(RetentionPolicy.RUNTIME)
public @interface LongCurrency {}

Next make sure that you have registered your LongCurrencyFormatAnnotationFormatterFactory properly. If you don’t register it it will not be used.

@Override 
public void addFormatters(FormatterRegistry registry) {
    registry.addFormatterForFieldAnnotation(new LongCurrencyFormatAnnotationFormatterFactory()); 
}

Both changes should make that your formatter is going to be called/used.

Answered By – M. Deinum

This Answer collected from stackoverflow, is licensed under cc by-sa 2.5 , cc by-sa 3.0 and cc by-sa 4.0

Leave a Reply

(*) Required, Your email will not be published