001/*
002 * Copyright (c) 2012, 2013, Credit Suisse (Anatole Tresch), Werner Keil,
003 * and individual contributors by the @author tags.
004 * 
005 * Licensed under the Apache License, Version 2.0 (the "License"); you may not
006 * use this file except in compliance with the License. You may obtain a copy of
007 * the License at
008 * 
009 * http://www.apache.org/licenses/LICENSE-2.0
010 * 
011 * Unless required by applicable law or agreed to in writing, software
012 * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
013 * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
014 * License for the specific language governing permissions and limitations under
015 * the License.
016 */
017package org.javamoney.moneta.format;
018
019import java.io.IOException;
020import java.text.ParseException;
021import java.util.ArrayList;
022import java.util.List;
023import java.util.Locale;
024
025import javax.money.CurrencyUnit;
026import javax.money.MonetaryAmount;
027
028import org.javamoney.moneta.Money;
029import org.javamoney.moneta.function.MonetaryRoundings;
030
031/**
032 * Formats instances of {@code MonetaryAmount} to a {@link String} or an
033 * {@link Appendable}.
034 * <p>
035 * Instances of this class are not thread-safe. Basically when using
036 * {@link MonetaryAmountFormat} instances a new instance should be created on
037 * each access.
038 */
039public final class MonetaryAmountFormat {
040
041        public static enum CurrencyStyle {
042                CODE, NAME, NUMERIC_CODE, SYMBOL
043        }
044
045        /** The tokens to be used for formatting/parsing. */
046        private List<FormatToken> tokens = new ArrayList<FormatToken>();
047
048        private CurrencyUnit defaultCurrency;
049
050        public AmountStyle getAmountStyle() {
051                AmountNumberToken numberToken = getNumberToken();
052                if (numberToken == null) {
053                        throw new IllegalStateException(
054                                        "This format has no numer value attached.");
055                }
056                return numberToken.getAmountStyle();
057        }
058
059        private CurrencyToken getCurrencyToken() {
060                for (FormatToken t : this.tokens) {
061                        if (t instanceof CurrencyToken) {
062                                return (CurrencyToken) t;
063                        }
064                }
065                return null;
066        }
067
068        private AmountNumberToken getNumberToken() {
069                for (FormatToken t : this.tokens) {
070                        if (t instanceof CurrencyToken) {
071                                return (AmountNumberToken) t;
072                        }
073                }
074                return null;
075        }
076
077        /**
078         * Creates a new instance.
079         * 
080         * @param buildItemFormat
081         *            the base buildItemFormat, not null.
082         * @param itemFactory
083         *            the itemFactory to be used, not null.
084         */
085        private MonetaryAmountFormat(
086                        List<FormatToken> tokens, CurrencyUnit defaultCurrency) {
087                if (tokens == null || tokens.isEmpty()) {
088                        throw new IllegalArgumentException(
089                                        "tokens must not be null or empty.");
090                }
091                this.tokens.addAll(tokens);
092                this.defaultCurrency = defaultCurrency;
093        }
094
095        public CurrencyUnit getDefaultCurrency() {
096                return defaultCurrency;
097        }
098
099        /**
100         * Formats a value of {@code T} to a {@code String}. The {@link Locale}
101         * passed defines the overall target {@link Locale}, whereas the
102         * {@link LocalizationStyle} attached with the instances configures, how the
103         * {@link MonetaryFormat} should generally behave. The
104         * {@link LocalizationStyle} allows to configure the formatting and parsing
105         * in arbitrary details. The attributes that are supported are determined by
106         * the according {@link MonetaryFormat} implementation:
107         * <ul>
108         * <li>When the {@link MonetaryFormat} was created using the {@link Builder}
109         * , all the {@link FormatToken}, that model the overall format, and the
110         * {@link ItemFactory}, that is responsible for extracting the final parsing
111         * result, returned from a parsing call, are all possible recipients for
112         * attributes of the configuring {@link LocalizationStyle}.
113         * <li>When the {@link MonetaryFormat} was provided by an instance of
114         * {@link ItemFormatFactorySpi} the {@link MonetaryFormat} returned
115         * determines the capabilities that can be configured.
116         * </ul>
117         * 
118         * So, regardless if an {@link MonetaryFormat} is created using the fluent
119         * style {@link Builder} pattern, or provided as preconfigured
120         * implementation, {@link LocalizationStyle}s allow to configure them both
121         * effectively.
122         * 
123         * @param amount
124         *            the amount to print, not {@code null}
125         * @return the string printed using the settings of this formatter
126         * @throws UnsupportedOperationException
127         *             if the formatter is unable to print
128         */
129        public String format(MonetaryAmount amount) {
130                StringBuilder builder = new StringBuilder();
131                try {
132                        print(builder, amount);
133                } catch (IOException e) {
134                        throw new IllegalStateException("Error foratting of " + amount, e);
135                }
136                return builder.toString();
137        }
138
139        /**
140         * Prints a item value to an {@code Appendable}.
141         * <p>
142         * Example implementations of {@code Appendable} are {@code StringBuilder},
143         * {@code StringBuffer} or {@code Writer}. Note that {@code StringBuilder}
144         * and {@code StringBuffer} never throw an {@code IOException}.
145         * 
146         * @param appendable
147         *            the appendable to add to, not null
148         * @param item
149         *            the item to print, not null
150         * @param locale
151         *            the main target {@link Locale} to be used, not {@code null}
152         * @throws UnsupportedOperationException
153         *             if the formatter is unable to print
154         * @throws ItemFormatException
155         *             if there is a problem while printing
156         * @throws IOException
157         *             if an IO error occurs
158         */
159        public void print(Appendable appendable, MonetaryAmount amount)
160                        throws IOException {
161                for (FormatToken token : tokens) {
162                        token.print(appendable, amount);
163                }
164        }
165
166        /**
167         * Fully parses the text into an instance of {@code T}.
168         * <p>
169         * The parse must complete normally and parse the entire text. If the parse
170         * completes without reading the entire length of the text, an exception is
171         * thrown. If any other problem occurs during parsing, an exception is
172         * thrown.
173         * <p>
174         * This method uses a {@link Locale} as an input parameter. Additionally the
175         * {@link ItemFormatException} instance is configured by a
176         * {@link LocalizationStyle}. {@link LocalizationStyle}s allows to configure
177         * formatting input in detail. This allows to implement complex formatting
178         * requirements using this interface.
179         * 
180         * @param text
181         *            the text to parse, not null
182         * @param locale
183         *            the main target {@link Locale} to be used, not {@code null}
184         * @return the parsed value, never {@code null}
185         * @throws UnsupportedOperationException
186         *             if the formatter is unable to parse
187         * @throws ItemParseException
188         *             if there is a problem while parsing
189         */
190        public MonetaryAmount parse(CharSequence text)
191                        throws ParseException {
192                ParseContext ctx = new ParseContext(text);
193                for (FormatToken token : tokens) {
194                        token.parse(ctx);
195                }
196                CurrencyUnit unit = ctx.getParsedCurrency();
197                Number num = ctx.getParsedNumber();
198                if (unit == null) {
199                        unit = defaultCurrency;
200                }
201                if (num == null) {
202                        throw new ParseException(text.toString(), -1);
203                }
204                return Money.of(unit, num);
205        }
206
207        /**
208         * This class implements a builder that allows creating of
209         * {@link MonetaryFormat} instances programmatically using a fluent API. The
210         * formatting hereby is modeled by a concatenation of {@link FormatToken}
211         * instances. The same {@link FormatToken} instances also are responsible
212         * for implementing the opposite, parsing, of an item from an input
213         * character sequence. Each {@link FormatToken} gets access to the current
214         * parsing location, and the original and current character input sequence,
215         * modeled by the {@link ParseContext}. Finall if parsing of a part failed,
216         * a {@link FormatToken} throws an {@link ItemParseException} describing the
217         * problem.
218         * <p>
219         * This class is not thread-safe and therefore should not be shared among
220         * different threads.
221         * 
222         * @author Anatole Tresch
223         * 
224         * @param <T>
225         *            the target type.
226         */
227        public static final class Builder {
228                /** The tokens to be used for formatting/parsing. */
229                private List<FormatToken> tokens = new ArrayList<FormatToken>();
230
231                private Locale locale;
232
233                /**
234                 * The default currency, used, when parsing amounts, where no currency
235                 * is available.
236                 */
237                private CurrencyUnit defaultCurrency;
238
239                private char[] groupChars;
240
241                private int[] groupSizes;
242
243                /**
244                 * Creates a new Builder.
245                 * 
246                 * @param targetType
247                 *            the target class.
248                 */
249                public Builder(Locale locale) {
250                        if (locale == null) {
251                                throw new IllegalArgumentException("Locale required.");
252                        }
253                        this.locale = locale;
254                }
255
256                public Builder withDefaultCurrency(CurrencyUnit currency) {
257                        this.defaultCurrency = currency;
258                        return this;
259                }
260
261                /**
262                 * Add a {@link FormatToken} to the token list.
263                 * 
264                 * @param token
265                 *            the token to add.
266                 * @return the builder, for chaining.
267                 */
268                public Builder appendAmount(AmountStyle style) {
269                        this.tokens.add(new AmountNumberToken(style));
270                        return this;
271                }
272
273                /**
274                 * Add the amount to the given format. Hereby the number default style
275                 * for the {@link #locale} is used, and the number is rounded with the
276                 * currencies, default rounding as returned by
277                 * {@link MonetaryRoundings#getRounding()}.
278                 * 
279                 * @param token
280                 *            the token to add.
281                 * @return the builder, for chaining.
282                 */
283                public Builder appendAmount() {
284                        AmountStyle style = new AmountStyle.Builder(locale).withRounding(
285                                        MonetaryRoundings.getRounding()).build();
286                        this.tokens.add(new AmountNumberToken(style));
287                        return this;
288                }
289
290                /**
291                 * Adds a currency unit to the format using the given
292                 * {@link CurrencyStyle}.
293                 * 
294                 * @param style
295                 *            the style to be used, not {@code null}.
296                 * @return the builder, for chaining.
297                 */
298                public Builder appendCurrency(CurrencyStyle style) {
299                        this.tokens.add(new CurrencyToken(style, locale));
300                        return this;
301                }
302
303                /**
304                 * Adds a currency to the format printing using the currency code.
305                 * 
306                 * @return the builder, for chaining.
307                 */
308                public Builder appendCurrency() {
309                        return appendCurrency(CurrencyStyle.CODE);
310                }
311
312                /**
313                 * Add a {@link FormatToken} to the token list.
314                 * 
315                 * @param literal
316                 *            the literal to add, not {@code null}.
317                 * @return the builder, for chaining.
318                 */
319                public Builder appendLiteral(String literal) {
320                        this.tokens.add(new LiteralToken(literal));
321                        return this;
322                }
323
324                /**
325                 * This method creates an {@link MonetaryFormat} based on this instance,
326                 * hereby using the given a {@link ItemFactory} to extract the item to
327                 * be returned from the {@link ParseContext}'s results.
328                 * 
329                 * @return the {@link MonetaryFormat} instance, never null.
330                 */
331                public MonetaryAmountFormat build() {
332                        if (tokens.isEmpty()) {
333                                // create default JDK currency format
334                                this.tokens.add(new AmountNumberToken(new AmountStyle.Builder(
335                                                locale).withCurrencyFormat(locale)
336                                                .withNumberGroupChars(groupChars)
337                                                .withNumberGroupSizes(groupSizes).build()));
338                        }
339                        return new MonetaryAmountFormat(tokens, defaultCurrency);
340                }
341
342                /*
343                 * (non-Javadoc)
344                 * 
345                 * @see java.lang.Object#toString()
346                 */
347                @Override
348                public String toString() {
349                        return "MonetaryAmountFormat.Builder [tokens=" + tokens + "]";
350                }
351
352                public Builder withNumberGroupSizes(int... groupSizes) {
353                        this.groupSizes = groupSizes.clone();
354                        return this;
355                }
356
357                public Builder withNumberGroupChars(char... groupChars) {
358                        this.groupChars = groupChars.clone();
359                        return this;
360                }
361
362        }
363}