001    /*
002     *  Copyright 2009-2013 Stephen Colebourne
003     *
004     *  Licensed under the Apache License, Version 2.0 (the "License");
005     *  you may not use this file except in compliance with the License.
006     *  You may obtain a copy of the License at
007     *
008     *      http://www.apache.org/licenses/LICENSE-2.0
009     *
010     *  Unless required by applicable law or agreed to in writing, software
011     *  distributed under the License is distributed on an "AS IS" BASIS,
012     *  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
013     *  See the License for the specific language governing permissions and
014     *  limitations under the License.
015     */
016    package org.joda.money.format;
017    
018    import java.io.IOException;
019    import java.io.Serializable;
020    import java.util.Arrays;
021    import java.util.Locale;
022    
023    import org.joda.money.BigMoney;
024    import org.joda.money.BigMoneyProvider;
025    import org.joda.money.Money;
026    
027    /**
028     * Formats instances of money to and from a String.
029     * <p>
030     * Instances of {@code MoneyFormatter} can be created by
031     * {@code MoneyFormatterBuilder}.
032     * <p>
033     * This class is immutable and thread-safe.
034     */
035    public final class MoneyFormatter implements Serializable {
036    
037        /**
038         * Serialization version.
039         */
040        private static final long serialVersionUID = 2385346258L;
041    
042        /**
043         * The locale to use.
044         */
045        private final Locale locale;
046        /**
047         * The printers.
048         */
049        private final MoneyPrinter[] printers;
050        /**
051         * The parsers.
052         */
053        private final MoneyParser[] parsers;
054    
055        //-----------------------------------------------------------------------
056        /**
057         * Validates that the object specified is not null
058         *
059         * @param object  the object to check, null throws exception
060         * @param message  the message to use in the exception, not null
061         * @throws NullPointerException if the input value is null
062         */
063        static void checkNotNull(Object object, String message) {
064            if (object == null) {
065                throw new NullPointerException(message);
066            }
067        }
068    
069        //-----------------------------------------------------------------------
070        /**
071         * Constructor, creating a new formatter.
072         * 
073         * @param locale  the locale to use, not null
074         * @param printers  the printers, not null
075         * @param parsers  the parsers, not null
076         */
077        MoneyFormatter(
078                Locale locale,
079                MoneyPrinter[] printers,
080                MoneyParser[] parsers) {
081            assert locale != null;
082            assert printers != null;
083            assert parsers != null;
084            assert printers.length == parsers.length;
085            this.locale = locale;
086            this.printers = printers;
087            this.parsers = parsers;
088        }
089    
090        //-----------------------------------------------------------------------
091        /**
092         * Appends the printers and parsers from this formatter to the builder.
093         * 
094         * @param builder  the builder to append to not null
095         */
096        void appendTo(MoneyFormatterBuilder builder) {
097            for (int i = 0; i < printers.length; i++) {
098                builder.append(printers[i], parsers[i]);
099            }
100        }
101    
102        //-----------------------------------------------------------------------
103        /**
104         * Gets the locale to use.
105         * 
106         * @return the locale, never null
107         */
108        public Locale getLocale() {
109            return locale;
110        }
111    
112        /**
113         * Returns a copy of this instance with the specified locale.
114         * <p>
115         * Changing the locale may change the style of output depending on how the
116         * formatter has been configured.
117         * 
118         * @param locale  the locale, not null
119         * @return the new instance, never null
120         */
121        public MoneyFormatter withLocale(Locale locale) {
122            checkNotNull(locale, "Locale must not be null");
123            return new MoneyFormatter(locale, printers, parsers);
124        }
125    
126        //-----------------------------------------------------------------------
127        /**
128         * Checks whether this formatter can print.
129         * <p>
130         * If the formatter cannot print, an UnsupportedOperationException will
131         * be thrown from the print methods.
132         * 
133         * @return true if the formatter can print
134         */
135        public boolean isPrinter() {
136            return Arrays.asList(printers).contains(null) == false;
137        }
138    
139        /**
140         * Checks whether this formatter can parse.
141         * <p>
142         * If the formatter cannot parse, an UnsupportedOperationException will
143         * be thrown from the print methods.
144         * 
145         * @return true if the formatter can parse
146         */
147        public boolean isParser() {
148            return Arrays.asList(parsers).contains(null) == false;
149        }
150    
151        //-----------------------------------------------------------------------
152        /**
153         * Prints a monetary value to a {@code String}.
154         * 
155         * @param moneyProvider  the money to print, not null
156         * @return the string printed using the settings of this formatter
157         * @throws UnsupportedOperationException if the formatter is unable to print
158         * @throws MoneyFormatException if there is a problem while printing
159         */
160        public String print(BigMoneyProvider moneyProvider) {
161            StringBuilder buf = new StringBuilder();
162            print(buf, moneyProvider);
163            return buf.toString();
164        }
165    
166        /**
167         * Prints a monetary value to an {@code Appendable} converting
168         * any {@code IOException} to a {@code MoneyFormatException}.
169         * <p>
170         * Example implementations of {@code Appendable} are {@code StringBuilder},
171         * {@code StringBuffer} or {@code Writer}. Note that {@code StringBuilder}
172         * and {@code StringBuffer} never throw an {@code IOException}.
173         * 
174         * @param appendable  the appendable to add to, not null
175         * @param moneyProvider  the money to print, not null
176         * @throws UnsupportedOperationException if the formatter is unable to print
177         * @throws MoneyFormatException if there is a problem while printing
178         */
179        public void print(Appendable appendable, BigMoneyProvider moneyProvider) {
180            try {
181                printIO(appendable, moneyProvider);
182            } catch (IOException ex) {
183                throw new MoneyFormatException(ex.getMessage(), ex);
184            }
185        }
186    
187        /**
188         * Prints a monetary value to an {@code Appendable} potentially
189         * throwing an {@code IOException}.
190         * <p>
191         * Example implementations of {@code Appendable} are {@code StringBuilder},
192         * {@code StringBuffer} or {@code Writer}. Note that {@code StringBuilder}
193         * and {@code StringBuffer} never throw an {@code IOException}.
194         * 
195         * @param appendable  the appendable to add to, not null
196         * @param moneyProvider  the money to print, not null
197         * @throws UnsupportedOperationException if the formatter is unable to print
198         * @throws MoneyFormatException if there is a problem while printing
199         * @throws IOException if an IO error occurs
200         */
201        public void printIO(Appendable appendable, BigMoneyProvider moneyProvider) throws IOException {
202            checkNotNull(moneyProvider, "BigMoneyProvider must not be null");
203            if (isPrinter() == false) {
204                throw new UnsupportedOperationException("MoneyFomatter has not been configured to be able to print");
205            }
206            
207            BigMoney money = BigMoney.of(moneyProvider);
208            MoneyPrintContext context = new MoneyPrintContext(locale);
209            for (MoneyPrinter printer : printers) {
210                printer.print(context, appendable, money);
211            }
212        }
213    
214        //-----------------------------------------------------------------------
215        /**
216         * Fully parses the text into a {@code BigMoney}.
217         * <p>
218         * The parse must complete normally and parse the entire text (currency and amount).
219         * If the parse completes without reading the entire length of the text, an exception is thrown.
220         * If any other problem occurs during parsing, an exception is thrown.
221         * 
222         * @param text  the text to parse, not null
223         * @return the parsed monetary value, never null
224         * @throws UnsupportedOperationException if the formatter is unable to parse
225         * @throws MoneyFormatException if there is a problem while parsing
226         */
227        public BigMoney parseBigMoney(CharSequence text) {
228            checkNotNull(text, "Text must not be null");
229            MoneyParseContext result = parse(text, 0);
230            if (result.isError() || result.isFullyParsed() == false || result.isComplete() == false) {
231                String str = (text.length() > 64 ? text.subSequence(0, 64).toString() + "..." : text.toString());
232                if (result.isError()) {
233                    throw new MoneyFormatException("Text could not be parsed at index " + result.getErrorIndex() + ": " + str);
234                } else if (result.isFullyParsed() == false) {
235                    throw new MoneyFormatException("Unparsed text found at index " + result.getIndex() + ": " + str);
236                } else {
237                    throw new MoneyFormatException("Parsing did not find both currency and amount: " + str);
238                }
239            }
240            return result.toBigMoney();
241        }
242    
243        /**
244         * Fully parses the text into a {@code Money} requiring that the parsed
245         * amount has the correct number of decimal places.
246         * <p>
247         * The parse must complete normally and parse the entire text (currency and amount).
248         * If the parse completes without reading the entire length of the text, an exception is thrown.
249         * If any other problem occurs during parsing, an exception is thrown.
250         * 
251         * @param text  the text to parse, not null
252         * @return the parsed monetary value, never null
253         * @throws UnsupportedOperationException if the formatter is unable to parse
254         * @throws MoneyFormatException if there is a problem while parsing
255         * @throws ArithmeticException if the scale of the parsed money exceeds the scale of the currency
256         */
257        public Money parseMoney(CharSequence text) {
258            return parseBigMoney(text).toMoney();
259        }
260    
261        /**
262         * Parses the text extracting monetary information.
263         * <p>
264         * This method parses the input providing low-level access to the parsing state.
265         * The resulting context contains the parsed text, indicator of error, position
266         * following the parse and the parsed currency and amount.
267         * Together, these provide enough information for higher level APIs to use.
268         *
269         * @param text  the text to parse, not null
270         * @param startIndex  the start index to parse from
271         * @return the parsed monetary value, null only if the parse results in an error
272         * @throws IndexOutOfBoundsException if the start index is invalid
273         * @throws UnsupportedOperationException if this formatter cannot parse
274         */
275        public MoneyParseContext parse(CharSequence text, int startIndex) {
276            checkNotNull(text, "Text must not be null");
277            if (startIndex < 0 || startIndex > text.length()) {
278                throw new StringIndexOutOfBoundsException("Invalid start index: " + startIndex);
279            }
280            if (isParser() == false) {
281                throw new UnsupportedOperationException("MoneyFomatter has not been configured to be able to parse");
282            }
283            MoneyParseContext context = new MoneyParseContext(locale, text, startIndex);
284            for (MoneyParser parser : parsers) {
285                parser.parse(context);
286                if (context.isError()) {
287                    break;
288                }
289            }
290            return context;
291        }
292    
293        //-----------------------------------------------------------------------
294        /**
295         * Gets a string summary of the formatter.
296         * 
297         * @return a string summarising the formatter, never null
298         */
299        @Override
300        public String toString() {
301            StringBuilder buf1 = new StringBuilder();
302            if (isPrinter()) {
303                for (MoneyPrinter printer : printers) {
304                    buf1.append(printer.toString());
305                }
306            }
307            StringBuilder buf2 = new StringBuilder();
308            if (isParser()) {
309                for (MoneyParser parser : parsers) {
310                    buf2.append(parser.toString());
311                }
312            }
313            String str1 = buf1.toString();
314            String str2 = buf2.toString();
315            if (isPrinter() && isParser() == false) {
316                return str1;
317            } else if (isParser() && isPrinter() == false) {
318                return str2;
319            } else if (str1.equals(str2)) {
320                return str1;
321            } else {
322                return str1 + ":" + str2;
323            }
324        }
325    
326    }