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
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
Answer Checked By - Timothy Miller (JavaFixing Admin)