/*
* Beanfabrics Framework Copyright (C) by Michael Karneim, beanfabrics.org
* Use is subject to license terms. See license.txt.
*/
package org.beanfabrics.model;
import java.math.BigDecimal;
import java.math.BigInteger;
import java.text.DecimalFormat;
import java.text.NumberFormat;
import java.text.ParsePosition;
import java.util.Locale;
import java.util.ResourceBundle;
import org.beanfabrics.util.ResourceBundleFactory;
import org.beanfabrics.validation.ValidationRule;
import org.beanfabrics.validation.ValidationState;
/**
* The {@link BigDecimalPM} is a {@link PresentationModel} that contains a
* {@link BigDecimal} value.
* <p>
* This class is the base class for any other 'numeric' presentation models in
* Beanfabrics since it has the most generic validation rule (see
* {@link BigDecimalValidationRule}) and content conversion methods.
*
* @author Michael Karneim
*/
public class BigDecimalPM extends TextPM implements IBigDecimalPM {
private static final String KEY_MESSAGE_INVALID_NUMBER = "message.invalidNumber";
private final ResourceBundle resourceBundle = ResourceBundleFactory.getBundle(BigDecimalPM.class);
private IFormat<BigDecimal> format;
/**
* Constructs a {@link BigDecimalPM}.
*/
public BigDecimalPM() {
format = createDefaultFormat();
// Please note: to disable default validation rules just call getValidator().clear();
getValidator().add(new BigDecimalValidationRule());
}
/**
* Reformats the text value by first parsing it and the formatting it using
* this PM's format.
*
* @see #setFormat(DecimalFormat)
*/
@Override
public void reformat() {
try {
setBigDecimal(getBigDecimal());
} catch (ConversionException ex) {
// ignore
}
}
/**
* Creates the default format for this PM. This method is called from the
* constructor and usually returns a {@link BigDecimalPM.Format} with the
* default {@link DecimalFormat} of the current {@link Locale}.
*
* @return the default format for this PM
*/
protected IFormat<BigDecimal> createDefaultFormat() {
return new Format(getDecimalFormat(Locale.getDefault()));
}
/**
* Factory method for creating a {@link DecimalFormat} for the specified
* {@link Locale}.
*
* @param locale
* @return the new {@link DecimalFormat}.
*/
protected static DecimalFormat getDecimalFormat(Locale locale) {
DecimalFormat result = (DecimalFormat)NumberFormat.getInstance(locale);
result.setParseBigDecimal(true);
return result;
}
/**
* Returns the {@link IFormat} of this PM used for converting between
* {@link BigDecimal} and {@link String} values.
*
* @return the format
*/
public IFormat<BigDecimal> getFormat() {
return format;
}
/**
* Sets the format of this PM and reformats the text value. The format is
* used for converting between {@link BigDecimal} and {@link String} values.
*
* @param newFormat the new format
*/
public void setFormat(IFormat<BigDecimal> newFormat) {
if (newFormat == null) {
throw new IllegalArgumentException("newFormat == null");
}
IFormat<BigDecimal> oldFormat = format;
if (oldFormat == newFormat) {
return;
}
boolean doReformat;
BigDecimal oldValue = null;
try {
oldValue = getBigDecimal();
doReformat = true;
} catch (ConversionException ex) {
doReformat = false;
}
format = newFormat;
revalidate();
getPropertyChangeSupport().firePropertyChange("format", oldFormat, newFormat); //$NON-NLS-1$
if (doReformat) {
setBigDecimal(oldValue);
}
}
/** {@inheritDoc} */
public void setBigDecimal(BigDecimal value) {
if (value == null) {
setText(null);
} else {
String str = format.format(value);
setText(str);
}
}
/** {@inheritDoc} */
public BigDecimal getBigDecimal()
throws ConversionException {
if (isEmpty()) {
return null;
}
String str = getText();
BigDecimal result = convert(str);
return result;
}
/**
* Sets the default value of this PM to the given {@link BigDecimal} value.
*
* @param value
* @see #setDefaultText(String)
*/
public void setDefaultBigDecimal(BigDecimal value) {
if (value == null) {
setDefaultText(null);
} else {
setDefaultText(format.format(value));
}
}
/**
* Sets the value of this PM to the giben {@link BigInteger} value.
*
* @param value
* @see #setText(String)
*/
public void setBigInteger(BigInteger value) {
if (value == null) {
setBigDecimal(null);
} else {
setBigDecimal(new BigDecimal(value));
}
}
/**
* Returns the value of this PM as a {@link BigInteger}.
*
* @return the value of this PM as a {@link BigInteger}
* @throws ConversionException if the text value can't be converted into a
* valid {@link BigInteger}
*/
public BigInteger getBigInteger()
throws ConversionException {
if (isEmpty()) {
return null;
} else {
try {
return getBigDecimal().toBigIntegerExact();
} catch (ArithmeticException ex) {
throw new ConversionException(ex);
}
}
}
/**
* Sets the default value of this PM to the given {@link BigInteger} value.
*
* @param value
* @see #setDefaultText(String)
*/
public void setDefaultBigInteger(BigInteger value) {
if (value == null) {
setDefaultBigDecimal(null);
} else {
setDefaultBigDecimal(new BigDecimal(value));
}
}
/**
* Converts the given text into a {@link BigDecimal}.
*
* @param text
* @return a BigDecimal representation of the given text
* @throws ConversionException if the text value can't be converted into a
* valid {@link BigDecimal}
*/
private BigDecimal convert(String text)
throws ConversionException {
return format.parse(text);
}
/**
* This rule evaluates to invalid if the PM's value can't be converted into
* a {@link BigDecimal}.
*
* @author Michael Karneim
*/
public class BigDecimalValidationRule implements ValidationRule {
/** {@inheritDoc} */
public ValidationState validate() {
if (isEmpty()) {
return null;
}
try {
convert(getText());
return null;
} catch (ConversionException ex) {
String message = resourceBundle.getString(KEY_MESSAGE_INVALID_NUMBER);
return new ValidationState(message);
}
}
}
@Override
public Comparable<?> getComparable() {
return new BigDecimalComparable();
}
/**
* The {@link BigDecimalComparable} delegates the comparison to the model's
* BigDecimal value if available, or otherwise falls back to the
* <code>super.compareTo(...)</code>.
*
* @author Michael Karneim
*/
protected class BigDecimalComparable extends TextComparable {
BigDecimal bd;
/**
* Constructs a {@link BigDecimalComparable}.
*/
public BigDecimalComparable() {
if (!isEmpty()) {
try {
bd = getBigDecimal();
} catch (ConversionException ex) {
// ignore
bd = null;
}
}
}
@Override
public int compareTo(Object o) {
if (o == null) {
throw new IllegalArgumentException("o==null");
}
if (!(o instanceof BigDecimalComparable)) {
throw new IllegalArgumentException("incompatible comparable class: " + o.getClass().getName());
}
BigDecimalComparable oc = (BigDecimalComparable)o;
if (bd == null) {
if (oc.bd == null) {
return super.compareTo(o);
} else {
return -1;
}
} else {
if (oc.bd == null) {
return 1;
} else {
return bd.compareTo(oc.bd);
}
}
}
@Override
public boolean equals(Object o) {
if (this == o) {
return true;
}
if (!super.equals(o)) {
return false;
}
if (o == null) {
return false;
}
if (o.getClass() != getClass()) {
return false;
}
BigDecimalComparable castedObj = (BigDecimalComparable)o;
return ((bd == null ? castedObj.bd == null : bd.equals(castedObj.bd)));
}
@Override
public int hashCode() {
return bd.hashCode();
}
}
/**
* The {@link BigDecimalPM.Format} is a {@link IFormat} for converting
* between {@link BigDecimal} and {@link String}.
*/
public static class Format implements IFormat<BigDecimal> {
private static final String ALLOWED_SYMBOLS = "0#.,E;-";
private final DecimalFormat strictFormat;
private final DecimalFormat simplifiedFormat;
/**
* Creates a {@link BigDecimalPM.Format} using the given
* {@link DecimalFormat}.
*
* @param format
*/
public Format(DecimalFormat format) {
strictFormat = (DecimalFormat)format.clone();
strictFormat.setParseBigDecimal(true);
simplifiedFormat = createSimplifiedNumberFormat(strictFormat);
}
/**
* Creates a {@link BigDecimalPM.Format} using the given two formats.
*
* @param aStrictFormat the strict format defines, how to format a
* BigDecimal into a String and how to parse it
* @param aSimplifiedFormat the simplified format defines, how to parse
* a String if the strict format fails to parse it
*/
public Format(DecimalFormat aStrictFormat, DecimalFormat aSimplifiedFormat) {
strictFormat = (DecimalFormat)aStrictFormat.clone();
strictFormat.setParseBigDecimal(true);
simplifiedFormat = (DecimalFormat)aSimplifiedFormat.clone();
simplifiedFormat.setParseBigDecimal(true);
}
public DecimalFormat getStrictFormat() {
return strictFormat;
}
public DecimalFormat getSimplifiedFormat() {
return simplifiedFormat;
}
/** {@inheritDoc} */
public BigDecimal parse(String text)
throws ConversionException {
BigDecimal result;
try {
result = convert(strictFormat, text);
} catch (ConversionException ex) {
result = convert(simplifiedFormat, text);
}
return result;
}
/** {@inheritDoc} */
public String format(BigDecimal value) {
if (value == null) {
return null;
} else {
String result = strictFormat.format(value);
return result;
}
}
/**
* Converts the given text into a {@link BigDecimal}.
*
* @param text
* @return a BigDecimal representation of the given text
* @throws ConversionException if the text value can't be converted into
* a valid {@link BigDecimal}
*/
private BigDecimal convert(DecimalFormat format, String text)
throws ConversionException {
if (format.isParseBigDecimal() == false) {
throw new IllegalStateException("format must parse BigDecimal");
}
text = text.trim();
if (text == null || text.length() == 0) {
return null;
}
ParsePosition pos = new ParsePosition(0);
BigDecimal result = (BigDecimal)format.parse(text, pos);
if (result != null && pos.getIndex() == text.length()) {
return result;
} else {
throw new ConversionException("Can't convert '" + text + "' to BigDecimal");
}
}
/**
* Creates a simplified version of the given format. A format is 'simplified' if it contains only those formatting
* symbols that have to do with the numeric representation. Any literals are removed.
*
* @param aFormat
* @return a simplified version of the given format
*/
public DecimalFormat createSimplifiedNumberFormat(DecimalFormat aFormat) {
// replace all 'bad' characters
String pattern = aFormat.toPattern();
StringBuilder builder = new StringBuilder();
boolean escaped = false;
for (char c : pattern.toCharArray()) {
if (escaped) {
if (c == '\'') {
escaped = false;
}
continue;
}
if (c == '\'') {
escaped = true;
continue;
}
if (ALLOWED_SYMBOLS.indexOf(c) > -1) {
builder.append(c);
}
}
// This can never happen since DecimalFormat throws an IllegalArgumentException: MalformedPattern
assert escaped == false : "Apostrophe must not be the last character in pattern: " + pattern;
String simplifiedPattern = builder.toString();
DecimalFormat simplifiedFormat = (DecimalFormat) aFormat.clone();
simplifiedFormat.applyPattern(simplifiedPattern);
return simplifiedFormat;
}
}
}