001/*
002 *  Licensed to the Apache Software Foundation (ASF) under one
003 *  or more contributor license agreements.  See the NOTICE file
004 *  distributed with this work for additional information
005 *  regarding copyright ownership.  The ASF licenses this file
006 *  to you under the Apache License, Version 2.0 (the
007 *  "License"); you may not use this file except in compliance
008 *  with the License.  You may obtain a copy of the License at
009 *
010 *        http://www.apache.org/licenses/LICENSE-2.0
011 *
012 *  Unless required by applicable law or agreed to in writing,
013 *  software distributed under the License is distributed on an
014 *  "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
015 *  KIND, either express or implied.  See the License for the
016 *  specific language governing permissions and limitations
017 *  under the License.
018 */
019
020package org.apache.isis.core.progmodel.facets.value.money;
021
022import java.text.DecimalFormat;
023import java.text.NumberFormat;
024import java.text.ParseException;
025import java.util.Currency;
026
027import org.apache.isis.applib.adapters.EncoderDecoder;
028import org.apache.isis.applib.adapters.Parser;
029import org.apache.isis.applib.profiles.Localization;
030import org.apache.isis.applib.value.Money;
031import org.apache.isis.core.commons.config.ConfigurationConstants;
032import org.apache.isis.core.commons.config.IsisConfiguration;
033import org.apache.isis.core.metamodel.adapter.ObjectAdapter;
034import org.apache.isis.core.metamodel.facetapi.Facet;
035import org.apache.isis.core.metamodel.facetapi.FacetHolder;
036import org.apache.isis.core.metamodel.facets.object.parseable.TextEntryParseException;
037import org.apache.isis.core.metamodel.facets.properties.defaults.PropertyDefaultFacet;
038import org.apache.isis.core.progmodel.facets.object.value.ValueSemanticsProviderAndFacetAbstract;
039import org.apache.isis.core.progmodel.facets.object.value.ValueSemanticsProviderContext;
040
041public class MoneyValueSemanticsProvider extends ValueSemanticsProviderAndFacetAbstract<Money> implements MoneyValueFacet {
042
043    private static Class<? extends Facet> type() {
044        return MoneyValueFacet.class;
045    }
046
047    private static final NumberFormat DEFAULT_NUMBER_FORMAT;
048    private static final NumberFormat DEFAULT_CURRENCY_FORMAT;
049    private static final String LOCAL_CURRENCY_CODE;
050    private static final int TYPICAL_LENGTH = 18;
051    private static final Money DEFAULT_VALUE = null; // no default
052
053    private final String defaultCurrencyCode;
054
055    static {
056        DEFAULT_NUMBER_FORMAT = NumberFormat.getNumberInstance();
057        DEFAULT_CURRENCY_FORMAT = NumberFormat.getCurrencyInstance();
058        DEFAULT_NUMBER_FORMAT.setMinimumFractionDigits(DEFAULT_CURRENCY_FORMAT.getMinimumFractionDigits());
059        DEFAULT_NUMBER_FORMAT.setMaximumFractionDigits(DEFAULT_CURRENCY_FORMAT.getMaximumFractionDigits());
060        LOCAL_CURRENCY_CODE = getDefaultCurrencyCode();
061    }
062
063    static final boolean isAPropertyDefaultFacet() {
064        return PropertyDefaultFacet.class.isAssignableFrom(MoneyValueSemanticsProvider.class);
065    }
066
067    private static String getDefaultCurrencyCode() {
068        try {
069            return DEFAULT_CURRENCY_FORMAT.getCurrency().getCurrencyCode();
070        } catch (final UnsupportedOperationException e) {
071            return "";
072        }
073    }
074
075    /**
076     * Required because implementation of {@link Parser} and
077     * {@link EncoderDecoder}.
078     */
079    public MoneyValueSemanticsProvider() {
080        this(null, null, null);
081    }
082
083    public MoneyValueSemanticsProvider(final FacetHolder holder, final IsisConfiguration configuration, final ValueSemanticsProviderContext context) {
084        super(type(), holder, Money.class, TYPICAL_LENGTH, Immutability.IMMUTABLE, EqualByContent.HONOURED, DEFAULT_VALUE, configuration, context);
085
086        final String property = ConfigurationConstants.ROOT + "value.money.currency";
087        defaultCurrencyCode = configuration.getString(property, LOCAL_CURRENCY_CODE);
088    }
089
090    // //////////////////////////////////////////////////////////////////
091    // Parser
092    // //////////////////////////////////////////////////////////////////
093
094    @Override
095    protected Money doParse(final Object context, final String text) {
096        final String entry = text.trim();
097        final int pos = entry.lastIndexOf(' ');
098        if (endsWithCurrencyCode(entry, pos)) {
099            final String value = entry.substring(0, pos);
100            final String code = entry.substring(pos + 1);
101            return parseNumberAndCurrencyCode(value, code);
102        } else {
103            return parseDerivedValue(context, entry);
104        }
105    }
106
107    private boolean endsWithCurrencyCode(final String entry, final int pos) {
108        final String suffix = entry.substring(pos + 1);
109        final boolean isCurrencyCode = suffix.length() == 3 && Character.isLetter(suffix.charAt(0)) && Character.isLetter(suffix.charAt(1)) && Character.isLetter(suffix.charAt(2));
110        return isCurrencyCode;
111    }
112
113    private Money parseDerivedValue(final Object original, final String entry) {
114        Money money = (Money) original;
115        if (money == null || money.getCurrency().equals(LOCAL_CURRENCY_CODE)) {
116            try {
117                final double value = DEFAULT_CURRENCY_FORMAT.parse(entry).doubleValue();
118                money = new Money(value, LOCAL_CURRENCY_CODE);
119                return money;
120            } catch (final ParseException ignore) {
121            }
122        }
123
124        try {
125            final double value = DEFAULT_NUMBER_FORMAT.parse(entry).doubleValue();
126            final String currencyCode = money == null ? defaultCurrencyCode : money.getCurrency();
127            money = new Money(value, currencyCode);
128            return money;
129        } catch (final ParseException ex) {
130            throw new TextEntryParseException("Not a distinguishable money value " + entry, ex);
131        }
132    }
133
134    private Money parseNumberAndCurrencyCode(final String amount, final String code) {
135        final String currencyCode = code.toUpperCase();
136        try {
137            Currency.getInstance(currencyCode.toUpperCase());
138        } catch (final IllegalArgumentException e) {
139            throw new TextEntryParseException("Invalid currency code " + currencyCode, e);
140        }
141        try {
142            final Money money = new Money(DEFAULT_NUMBER_FORMAT.parse(amount).doubleValue(), currencyCode);
143            return money;
144        } catch (final ParseException e) {
145            throw new TextEntryParseException("Invalid money entry", e);
146        }
147    }
148
149    @Override
150    public String titleString(final Object object, final Localization localization) {
151        if (object == null) {
152            return "";
153        }
154        final Money money = (Money) object;
155        final boolean localCurrency = LOCAL_CURRENCY_CODE.equals(money.getCurrency());
156        if (localCurrency) {
157            return DEFAULT_CURRENCY_FORMAT.format(money.doubleValue());
158        } else {
159            return DEFAULT_NUMBER_FORMAT.format(money.doubleValue()) + " " + money.getCurrency();
160        }
161    }
162
163    @Override
164    public String titleStringWithMask(final Object value, final String usingMask) {
165        if (value == null) {
166            return "";
167        }
168        final Money money = (Money) value;
169        return new DecimalFormat(usingMask).format(money.doubleValue());
170    }
171
172    // //////////////////////////////////////////////////////////////////
173    // EncoderDecoder
174    // //////////////////////////////////////////////////////////////////
175
176    @Override
177    protected String doEncode(final Object object) {
178        final Money money = (Money) object;
179        final String value = String.valueOf(money.doubleValue()) + " " + money.getCurrency();
180        return value;
181    }
182
183    @Override
184    protected Money doRestore(final String data) {
185        final String dataString = data;
186        final int pos = dataString.indexOf(' ');
187        final String amount = dataString.substring(0, pos);
188        final String currency = dataString.substring(pos + 1);
189        return new Money(Double.valueOf(amount).doubleValue(), currency);
190    }
191
192    // //////////////////////////////////////////////////////////////////
193    // MoneyValueFacet
194    // //////////////////////////////////////////////////////////////////
195
196    @Override
197    public float getAmount(final ObjectAdapter object) {
198        final Money money = (Money) object.getObject();
199        if (money == null) {
200            return 0.0f;
201        } else {
202            return money.floatValue();
203        }
204    }
205
206    @Override
207    public String getCurrencyCode(final ObjectAdapter object) {
208        final Money money = (Money) object.getObject();
209        if (money == null) {
210            return "";
211        } else {
212            return money.getCurrency();
213        }
214    }
215
216    @Override
217    public ObjectAdapter createValue(final float amount, final String currencyCode) {
218        return getAdapterManager().adapterFor(new Money(amount, currencyCode));
219    }
220
221    // /////// toString ///////
222
223    @Override
224    public String toString() {
225        return "MoneyValueSemanticsProvider: " + getDefaultCurrencyCode();
226    }
227
228}