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    
020    package org.apache.isis.core.progmodel.facets.value.money;
021    
022    import java.text.DecimalFormat;
023    import java.text.NumberFormat;
024    import java.text.ParseException;
025    import java.util.Currency;
026    
027    import org.apache.isis.applib.adapters.EncoderDecoder;
028    import org.apache.isis.applib.adapters.Parser;
029    import org.apache.isis.applib.profiles.Localization;
030    import org.apache.isis.applib.value.Money;
031    import org.apache.isis.core.commons.config.ConfigurationConstants;
032    import org.apache.isis.core.commons.config.IsisConfiguration;
033    import org.apache.isis.core.metamodel.adapter.ObjectAdapter;
034    import org.apache.isis.core.metamodel.facetapi.Facet;
035    import org.apache.isis.core.metamodel.facetapi.FacetHolder;
036    import org.apache.isis.core.metamodel.facets.object.parseable.TextEntryParseException;
037    import org.apache.isis.core.metamodel.facets.properties.defaults.PropertyDefaultFacet;
038    import org.apache.isis.core.progmodel.facets.object.value.ValueSemanticsProviderAndFacetAbstract;
039    import org.apache.isis.core.progmodel.facets.object.value.ValueSemanticsProviderContext;
040    
041    public class MoneyValueSemanticsProvider extends ValueSemanticsProviderAndFacetAbstract<Money> implements
042        MoneyValueFacet {
043    
044        private static Class<? extends Facet> type() {
045            return MoneyValueFacet.class;
046        }
047    
048        private static final NumberFormat DEFAULT_NUMBER_FORMAT;
049        private static final NumberFormat DEFAULT_CURRENCY_FORMAT;
050        private static final String LOCAL_CURRENCY_CODE;
051        private static final int TYPICAL_LENGTH = 18;
052        private static final boolean IMMUTABLE = true;
053        private static final boolean EQUAL_BY_CONTENT = true;
054        private static final Money DEFAULT_VALUE = null; // no default
055    
056        private final String defaultCurrencyCode;
057    
058        static {
059            DEFAULT_NUMBER_FORMAT = NumberFormat.getNumberInstance();
060            DEFAULT_CURRENCY_FORMAT = NumberFormat.getCurrencyInstance();
061            DEFAULT_NUMBER_FORMAT.setMinimumFractionDigits(DEFAULT_CURRENCY_FORMAT.getMinimumFractionDigits());
062            DEFAULT_NUMBER_FORMAT.setMaximumFractionDigits(DEFAULT_CURRENCY_FORMAT.getMaximumFractionDigits());
063            LOCAL_CURRENCY_CODE = getDefaultCurrencyCode();
064        }
065    
066        static final boolean isAPropertyDefaultFacet() {
067            return PropertyDefaultFacet.class.isAssignableFrom(MoneyValueSemanticsProvider.class);
068        }
069    
070        private static String getDefaultCurrencyCode() {
071            try {
072                return DEFAULT_CURRENCY_FORMAT.getCurrency().getCurrencyCode();
073            } catch (final UnsupportedOperationException e) {
074                return "";
075            }
076        }
077    
078        /**
079         * Required because implementation of {@link Parser} and {@link EncoderDecoder}.
080         */
081        public MoneyValueSemanticsProvider() {
082            this(null, null, null);
083        }
084    
085        public MoneyValueSemanticsProvider(final FacetHolder holder, final IsisConfiguration configuration,
086            final ValueSemanticsProviderContext context) {
087            super(type(), holder, Money.class, TYPICAL_LENGTH, IMMUTABLE, EQUAL_BY_CONTENT, DEFAULT_VALUE, configuration,
088                context);
089    
090            final String property = ConfigurationConstants.ROOT + "value.money.currency";
091            defaultCurrencyCode = configuration.getString(property, LOCAL_CURRENCY_CODE);
092        }
093    
094        // //////////////////////////////////////////////////////////////////
095        // Parser
096        // //////////////////////////////////////////////////////////////////
097    
098        @Override
099        protected Money doParse(final Object context, final String text) {
100            final String entry = text.trim();
101            final int pos = entry.lastIndexOf(' ');
102            if (endsWithCurrencyCode(entry, pos)) {
103                final String value = entry.substring(0, pos);
104                final String code = entry.substring(pos + 1);
105                return parseNumberAndCurrencyCode(value, code);
106            } else {
107                return parseDerivedValue(context, entry);
108            }
109        }
110    
111        private boolean endsWithCurrencyCode(final String entry, final int pos) {
112            final String suffix = entry.substring(pos + 1);
113            final boolean isCurrencyCode =
114                suffix.length() == 3 && Character.isLetter(suffix.charAt(0)) && Character.isLetter(suffix.charAt(1))
115                    && Character.isLetter(suffix.charAt(2));
116            return isCurrencyCode;
117        }
118    
119        private Money parseDerivedValue(final Object original, final String entry) {
120            Money money = (Money) original;
121            if (money == null || money.getCurrency().equals(LOCAL_CURRENCY_CODE)) {
122                try {
123                    final double value = DEFAULT_CURRENCY_FORMAT.parse(entry).doubleValue();
124                    money = new Money(value, LOCAL_CURRENCY_CODE);
125                    return money;
126                } catch (final ParseException ignore) {
127                }
128            }
129    
130            try {
131                final double value = DEFAULT_NUMBER_FORMAT.parse(entry).doubleValue();
132                final String currencyCode = money == null ? defaultCurrencyCode : money.getCurrency();
133                money = new Money(value, currencyCode);
134                return money;
135            } catch (final ParseException ex) {
136                throw new TextEntryParseException("Not a distinguishable money value " + entry, ex);
137            }
138        }
139    
140        private Money parseNumberAndCurrencyCode(final String amount, final String code) {
141            final String currencyCode = code.toUpperCase();
142            try {
143                Currency.getInstance(currencyCode.toUpperCase());
144            } catch (final IllegalArgumentException e) {
145                throw new TextEntryParseException("Invalid currency code " + currencyCode, e);
146            }
147            try {
148                final Money money = new Money(DEFAULT_NUMBER_FORMAT.parse(amount).doubleValue(), currencyCode);
149                return money;
150            } catch (final ParseException e) {
151                throw new TextEntryParseException("Invalid money entry", e);
152            }
153        }
154    
155        @Override
156        public String titleString(final Object object, final Localization localization) {
157            if (object == null) {
158                return "";
159            }
160            final Money money = (Money) object;
161            final boolean localCurrency = LOCAL_CURRENCY_CODE.equals(money.getCurrency());
162            if (localCurrency) {
163                return DEFAULT_CURRENCY_FORMAT.format(money.doubleValue());
164            } else {
165                return DEFAULT_NUMBER_FORMAT.format(money.doubleValue()) + " " + money.getCurrency();
166            }
167        }
168    
169        @Override
170        public String titleStringWithMask(final Object value, final String usingMask) {
171            if (value == null) {
172                return "";
173            }
174            final Money money = (Money) value;
175            return new DecimalFormat(usingMask).format(money.doubleValue());
176        }
177    
178        // //////////////////////////////////////////////////////////////////
179        // EncoderDecoder
180        // //////////////////////////////////////////////////////////////////
181    
182        @Override
183        protected String doEncode(final Object object) {
184            final Money money = (Money) object;
185            final String value = String.valueOf(money.doubleValue()) + " " + money.getCurrency();
186            return value;
187        }
188    
189        @Override
190        protected Money doRestore(final String data) {
191            final String dataString = data;
192            final int pos = dataString.indexOf(' ');
193            final String amount = dataString.substring(0, pos);
194            final String currency = dataString.substring(pos + 1);
195            return new Money(Double.valueOf(amount).doubleValue(), currency);
196        }
197    
198        // //////////////////////////////////////////////////////////////////
199        // MoneyValueFacet
200        // //////////////////////////////////////////////////////////////////
201    
202        @Override
203        public float getAmount(final ObjectAdapter object) {
204            final Money money = (Money) object.getObject();
205            if (money == null) {
206                return 0.0f;
207            } else {
208                return money.floatValue();
209            }
210        }
211    
212        @Override
213        public String getCurrencyCode(final ObjectAdapter object) {
214            final Money money = (Money) object.getObject();
215            if (money == null) {
216                return "";
217            } else {
218                return money.getCurrency();
219            }
220        }
221    
222        @Override
223        public ObjectAdapter createValue(final float amount, final String currencyCode) {
224            return getAdapterMap().adapterFor(new Money(amount, currencyCode));
225        }
226    
227        // /////// toString ///////
228    
229        @Override
230        public String toString() {
231            return "MoneyValueSemanticsProvider: " + getDefaultCurrencyCode();
232        }
233    
234    }