/* * Copyright (c) 2005-2011 Grameen Foundation USA * All rights reserved. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or * implied. See the License for the specific language governing * permissions and limitations under the License. * * See also http://www.apache.org/licenses/LICENSE-2.0.html for an * explanation of the license and how it is applied. */ package org.mifos.framework.util; import java.text.DateFormat; import java.text.DecimalFormat; import java.text.DecimalFormatSymbols; import java.text.NumberFormat; import java.text.ParsePosition; import java.text.SimpleDateFormat; import java.util.ArrayList; import java.util.List; import java.util.Locale; import org.mifos.application.master.business.MifosCurrency; import org.mifos.config.AccountingRules; import org.mifos.framework.util.helpers.ConversionError; import org.mifos.framework.util.helpers.DoubleConversionResult; import org.mifos.framework.util.helpers.Money; /** * Localization is UI concern, it should never be used beyond controller/action layer */ public class LocalizationConverter { /** * Only support English number format */ private Locale locale = Locale.UK; private DecimalFormat decimalFormat; private DecimalFormat decimalFormatForMoney; private DecimalFormat decimalFormatForInterest; private String dateSeparator; private char decimalFormatSymbol; private short digitsAfterDecimalForMoney; private short multipleDigitsAfterDecimalForMoney; private short digitsBeforeDecimalForMoney; private short digitsAfterDecimalForInterest; private short digitsBeforeDecimalForInterest; private short digitsBeforeDecimalForCashFlowValidations; private short digitsAfterDecimalForCashFlowValidations; private char minusSign; public LocalizationConverter() { this(Money.getDefaultCurrency()); } public LocalizationConverter(MifosCurrency currency) { digitsAfterDecimalForMoney = AccountingRules.getDigitsAfterDecimal(currency); multipleDigitsAfterDecimalForMoney=AccountingRules.getMultpleDigitsAfterDecimal(currency); digitsBeforeDecimalForMoney = AccountingRules.getDigitsBeforeDecimal(); digitsAfterDecimalForInterest = AccountingRules.getDigitsAfterDecimalForInterest(); digitsBeforeDecimalForInterest = AccountingRules.getDigitsBeforeDecimalForInterest(); digitsBeforeDecimalForCashFlowValidations = AccountingRules.getDigitsBeforeDecimalForCashFlowValidations(); digitsAfterDecimalForCashFlowValidations = AccountingRules.getDigitsAfterDecimalForCashFlowValidations(); loadDecimalFormats(); dateSeparator = getDateSeparator(); } /** * Unit Test only * * @param digitsAfterDecimalForMoney * @param digitsBeforeDecimalForMoney * @param digitsAfterDecimalForInterest * @param digitsBeforeDecimalForInterest * @param digitsBeforeDecimalForCashFlowValidations * @param digitsAfterDecimalForCashFlowValidations */ protected LocalizationConverter(Short digitsAfterDecimalForMoney, Short digitsBeforeDecimalForMoney, Short digitsAfterDecimalForInterest, Short digitsBeforeDecimalForInterest, Short digitsBeforeDecimalForCashFlowValidations, Short digitsAfterDecimalForCashFlowValidations) { this.digitsAfterDecimalForMoney = digitsAfterDecimalForMoney; this.digitsBeforeDecimalForMoney = digitsBeforeDecimalForMoney; this.digitsAfterDecimalForInterest = digitsAfterDecimalForInterest; this.digitsBeforeDecimalForInterest = digitsBeforeDecimalForInterest; this.digitsBeforeDecimalForCashFlowValidations = digitsBeforeDecimalForCashFlowValidations; this.digitsAfterDecimalForCashFlowValidations = digitsAfterDecimalForCashFlowValidations; loadDecimalFormats(); dateSeparator = getDateSeparator(); } public void setCurrentLocale(Locale locale) { this.locale = locale; loadDecimalFormats(); dateSeparator = getDateSeparator(); } private boolean isLocaleSupported(Locale[] locales, Locale locale) { Locale tempLocale; boolean find = false; for (Locale locale2 : locales) { tempLocale = locale2; if (tempLocale.getCountry().equals(locale.getCountry()) && (tempLocale.getLanguage().equals(locale.getLanguage()))) { find = true; break; } } return find; } private DecimalFormat buildDecimalFormat(Short digitsBefore, Short digitsAfter, DecimalFormat decimalFormat, Boolean allowTrailingZero) { StringBuilder pattern = new StringBuilder(); for (short i = 0; i < digitsBefore; i++) { pattern.append('#'); } pattern.append(decimalFormat.getDecimalFormatSymbols().getDecimalSeparator()); for (short i = 0; i < digitsAfter; i++) { pattern.append('#'); } decimalFormat.applyLocalizedPattern(pattern.toString()); decimalFormat.setDecimalSeparatorAlwaysShown(false); if (allowTrailingZero) { decimalFormat.setMinimumFractionDigits(digitsAfter); } return decimalFormat; } private void loadDecimalFormats() { if (locale == null) { throw new RuntimeException("The current locale is not set for LocalizationConverter."); } // use this English locale for decimal format for 1.1 release boolean localeSupported = isLocaleSupported(NumberFormat.getAvailableLocales(), locale); if (!localeSupported) { throw new RuntimeException("NumberFormat class doesn't support this country code: " + locale.getCountry() + " and language code: " + locale.getLanguage()); } NumberFormat format = DecimalFormat.getInstance(locale); if (format instanceof DecimalFormat) { decimalFormat = (DecimalFormat) format; decimalFormatForMoney = buildDecimalFormat(digitsBeforeDecimalForMoney, digitsAfterDecimalForMoney, (DecimalFormat) decimalFormat.clone(), Boolean.TRUE); decimalFormatForInterest = buildDecimalFormat(digitsBeforeDecimalForInterest, digitsAfterDecimalForInterest, (DecimalFormat) decimalFormat.clone(), Boolean.FALSE); DecimalFormatSymbols decimalFormatSymbols = decimalFormat.getDecimalFormatSymbols(); decimalFormatSymbol = decimalFormatSymbols.getDecimalSeparator(); minusSign = decimalFormatSymbols.getMinusSign(); } } public DoubleConversionResult parseDoubleForMoney(String doubleStr) { DoubleConversionResult result = new DoubleConversionResult(); if (doubleStr == null) { List<ConversionError> errors = new ArrayList<ConversionError>(); errors.add(ConversionError.CONVERSION_ERROR); result.setErrors(errors); return result; } List<ConversionError> errors = checkDigits(digitsBeforeDecimalForMoney, digitsAfterDecimalForMoney, ConversionError.EXCEEDING_NUMBER_OF_DIGITS_BEFORE_DECIMAL_SEPARATOR_FOR_MONEY, ConversionError.EXCEEDING_NUMBER_OF_DIGITS_AFTER_DECIMAL_SEPARATOR_FOR_MONEY, doubleStr, false); result.setErrors(errors); if (errors.size() > 0) { return result; } try { result.setDoubleValue(getDoubleValueForCurrentLocale(doubleStr)); } catch (Exception ex) { // after all the checkings this is not likely to happen, but just in // case result.getErrors().add(ConversionError.CONVERSION_ERROR); } return result; } public DoubleConversionResult parseDoubleDecimalForMoney(String doubleStr) { DoubleConversionResult result = new DoubleConversionResult(); if (doubleStr == null) { List<ConversionError> errors = new ArrayList<ConversionError>(); errors.add(ConversionError.CONVERSION_ERROR); result.setErrors(errors); return result; } List<ConversionError> errors = checkDigits(digitsBeforeDecimalForMoney, multipleDigitsAfterDecimalForMoney, ConversionError.EXCEEDING_NUMBER_OF_DIGITS_BEFORE_DECIMAL_SEPARATOR_FOR_MONEY, ConversionError.EXCEEDING_NUMBER_OF_DIGITS_AFTER_DECIMAL_SEPARATOR_FOR_MONEY, doubleStr, false); result.setErrors(errors); if (errors.size() > 0) { return result; } try { result.setDoubleValue(getDoubleValueForCurrentLocale(doubleStr)); } catch (Exception ex) { // after all the checkings this is not likely to happen, but just in // case result.getErrors().add(ConversionError.CONVERSION_ERROR); } return result; } public DoubleConversionResult parseDoubleForInstallmentTotalAmount(String totalAmountStr) { DoubleConversionResult result = new DoubleConversionResult(); if (totalAmountStr == null) { List<ConversionError> errors = new ArrayList<ConversionError>(); errors.add(ConversionError.CONVERSION_ERROR); result.setErrors(errors); return result; } List<ConversionError> errors = checkDigits(digitsBeforeDecimalForMoney, digitsAfterDecimalForMoney, ConversionError.EXCEEDING_NUMBER_OF_DIGITS_BEFORE_DECIMAL_SEPARATOR_FOR_MONEY, ConversionError.EXCEEDING_NUMBER_OF_DIGITS_AFTER_DECIMAL_SEPARATOR_FOR_MONEY, totalAmountStr, true); result.setErrors(errors); if (errors.size() > 0) { return result; } try { result.setDoubleValue(getDoubleValueForCurrentLocale(totalAmountStr)); } catch (Exception ex) { // after all the checkings this is not likely to happen, but just in // case result.getErrors().add(ConversionError.CONVERSION_ERROR); } return result; } public DoubleConversionResult parseDoubleForInterest(String doubleStr) { DoubleConversionResult result = new DoubleConversionResult(); if (doubleStr == null) { List<ConversionError> errors = new ArrayList<ConversionError>(); errors.add(ConversionError.CONVERSION_ERROR); result.setErrors(errors); return result; } List<ConversionError> errors = checkDigits(digitsBeforeDecimalForInterest, digitsAfterDecimalForInterest, ConversionError.EXCEEDING_NUMBER_OF_DIGITS_BEFORE_DECIMAL_SEPARATOR_FOR_INTEREST, ConversionError.EXCEEDING_NUMBER_OF_DIGITS_AFTER_DECIMAL_SEPARATOR_FOR_INTEREST, doubleStr, false); result.setErrors(errors); if (errors.size() > 0) { return result; } return validateTheValueIsWithinTheRange(ConversionError.INTEREST_OUT_OF_RANGE, AccountingRules.getMinInterest(), AccountingRules.getMaxInterest(), result, errors, doubleStr); } public DoubleConversionResult parseDoubleForCashFlowValidations(String doubleStr, ConversionError cashFlowValidationOutOfRange, Double minimumLimit, Double maximumLimit) { DoubleConversionResult result = new DoubleConversionResult(); if (doubleStr == null) { List<ConversionError> errors = new ArrayList<ConversionError>(); errors.add(ConversionError.CONVERSION_ERROR); result.setErrors(errors); return result; } List<ConversionError> errors = checkDigits(digitsBeforeDecimalForCashFlowValidations, digitsAfterDecimalForCashFlowValidations, ConversionError.EXCEEDING_NUMBER_OF_DIGITS_BEFORE_DECIMAL_SEPARATOR_FOR_CASHFLOW_VALIDATION, ConversionError.EXCEEDING_NUMBER_OF_DIGITS_AFTER_DECIMAL_SEPARATOR_FOR_CASHFLOW_VALIDATION, doubleStr, false); result.setErrors(errors); if (errors.size() > 0) { return result; } return validateTheValueIsWithinTheRange(cashFlowValidationOutOfRange, minimumLimit, maximumLimit, result, errors, doubleStr); } private DoubleConversionResult validateTheValueIsWithinTheRange(ConversionError outOfRangeConversionError, Double minimumLimit, Double maximumLimit, DoubleConversionResult result, List<ConversionError> errors, String validationValue) { try { Double value = getDoubleValueForPercent(validationValue); if ((value > maximumLimit) || (value < minimumLimit)) { errors.add(outOfRangeConversionError); } else { result.setDoubleValue(value); } } catch (Exception ex) { result.getErrors().add(ConversionError.CONVERSION_ERROR); } return result; } public char getDecimalFormatSymbol() { return decimalFormatSymbol; } private List<ConversionError> checkDigits(Short digitsBefore, Short digitsAfter, ConversionError errorDigitsBefore, ConversionError errorDigitsAfter, String number, boolean allowNegativeValue) { List<ConversionError> errors = new ArrayList<ConversionError>(); ConversionError error; for (int i = 0; i < number.length(); i++) { if (!Character.isDigit(number.charAt(i))) { char charAt = number.charAt(i); if (charAt == decimalFormatSymbol || (allowNegativeValue && charAt == minusSign)) { continue; } error = ConversionError.NOT_ALL_NUMBER; errors.add(error); return errors; } } int index = number.indexOf(decimalFormatSymbol); if (index < 0) { if (number.length() > digitsBefore) { error = errorDigitsBefore; errors.add(error); } } else { String digitsAfterNum = number.substring(index + 1, number.length()); if (digitsAfterNum.length() > digitsAfter) { error = errorDigitsAfter; errors.add(error); } String digitsBeforeNum = number.substring(0, index); if (digitsBeforeNum.length() > digitsBefore) { error = errorDigitsBefore; errors.add(error); } } return errors; } private Double getDoubleValueForPercent(String doubleValueString) { if (decimalFormatForInterest == null) { loadDecimalFormats(); } Double dNum = null; try { ParsePosition pp = new ParsePosition(0); Number num = decimalFormatForInterest.parse(doubleValueString, pp); if ((doubleValueString.length() != pp.getIndex()) || (num == null)) { throw new NumberFormatException("The format of the number is invalid. index " + pp.getIndex() + " locale " + locale.getCountry() + " " + locale.getLanguage()); } dNum = num.doubleValue(); } catch (Exception e) { throw new NumberFormatException(e.getMessage() + " .Number " + doubleValueString); } return dNum; } public Double getDoubleValueForCurrentLocale(String doubleValueString) { Double dNum = null; try { ParsePosition pp = new ParsePosition(0); Number num = decimalFormatForMoney.parse(doubleValueString, pp); if ((doubleValueString.length() != pp.getIndex()) || (num == null)) { throw new NumberFormatException("The format of the number is invalid. index " + pp.getIndex() + " locale " + locale.getCountry() + " " + locale.getLanguage()); } dNum = num.doubleValue(); } catch (Exception e) { throw new NumberFormatException(e.getMessage() + " .Number " + doubleValueString); } return dNum; } public String getDoubleStringForMoney(Double dNumber) { return decimalFormatForMoney.format(dNumber); } public String getDoubleStringForInterest(Double dNumber) { return decimalFormatForInterest.format(dNumber); } public String getDoubleValueString(Double dNumber) { return decimalFormat.format(dNumber); } public String getDateSeparatorForCurrentLocale() { return dateSeparator; } private String getDateSeparator() { Locale[] locales = DateFormat.getAvailableLocales(); if (!isLocaleSupported(locales, locale)) { throw new RuntimeException("DateFormat class doesn't support this country code: " + locale.getCountry() + " and language code: " + locale.getLanguage()); } return getDateSeparator(locale, DateFormat.SHORT); } public String getDateSeparator(Locale locale, int dateFormat) { String separator = ""; DateFormat format = DateFormat.getDateInstance(dateFormat, locale); String now = format.format(new DateTimeService().getCurrentJavaDateTime()); char chArray[] = now.toCharArray(); for (char element : chArray) { if (Character.isDigit(element) == false) { separator = String.valueOf(element); break; } } return separator; } public DateFormat getDateFormat() { Locale[] locales = DateFormat.getAvailableLocales(); if (!isLocaleSupported(locales, locale)) { throw new RuntimeException("DateFormat class doesn't support this country code: " + locale.getCountry() + " and language code: " + locale.getLanguage()); } DateFormat format = DateFormat.getDateInstance(DateFormat.SHORT, locale); return format; } /** * Use this if you want the year part to have 4 digits **/ public DateFormat getDateFormatWithFullYear() { DateFormat dateFormat = getDateFormat(); if (SimpleDateFormat.class.equals(dateFormat.getClass())) { return new SimpleDateFormat(((SimpleDateFormat) dateFormat).toPattern().replace("yy", "yyyy"), locale); } return dateFormat; } }