001    /*
002     *  Copyright 2001-2005 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.time.format;
017    
018    import java.io.IOException;
019    import java.io.Writer;
020    import java.util.Locale;
021    
022    import org.joda.time.MutablePeriod;
023    import org.joda.time.Period;
024    import org.joda.time.PeriodType;
025    import org.joda.time.ReadWritablePeriod;
026    import org.joda.time.ReadablePeriod;
027    
028    /**
029     * Controls the printing and parsing of a time period to and from a string.
030     * <p>
031     * This class is the main API for printing and parsing used by most applications.
032     * Instances of this class are created via one of three factory classes:
033     * <ul>
034     * <li>{@link PeriodFormat} - formats by pattern and style</li>
035     * <li>{@link ISOPeriodFormat} - ISO8601 formats</li>
036     * <li>{@link PeriodFormatterBuilder} - complex formats created via method calls</li>
037     * </ul>
038     * <p>
039     * An instance of this class holds a reference internally to one printer and
040     * one parser. It is possible that one of these may be null, in which case the
041     * formatter cannot print/parse. This can be checked via the {@link #isPrinter()}
042     * and {@link #isParser()} methods.
043     * <p>
044     * The underlying printer/parser can be altered to behave exactly as required
045     * by using a decorator modifier:
046     * <ul>
047     * <li>{@link #withLocale(Locale)} - returns a new formatter that uses the specified locale</li>
048     * </ul>
049     * This returns a new formatter (instances of this class are immutable).
050     * <p>
051     * The main methods of the class are the <code>printXxx</code> and
052     * <code>parseXxx</code> methods. These are used as follows:
053     * <pre>
054     * // print using the default locale
055     * String periodStr = formatter.print(period);
056     * // print using the French locale
057     * String periodStr = formatter.withLocale(Locale.FRENCH).print(period);
058     * 
059     * // parse using the French locale
060     * Period date = formatter.withLocale(Locale.FRENCH).parsePeriod(str);
061     * </pre>
062     *
063     * @author Brian S O'Neill
064     * @author Stephen Colebourne
065     * @since 1.0
066     */
067    public class PeriodFormatter {
068    
069        /** The internal printer used to output the datetime. */
070        private final PeriodPrinter iPrinter;
071        /** The internal parser used to output the datetime. */
072        private final PeriodParser iParser;
073        /** The locale to use for printing and parsing. */
074        private final Locale iLocale;
075        /** The period type used in parsing. */
076        private final PeriodType iParseType;
077    
078        /**
079         * Creates a new formatter, however you will normally use the factory
080         * or the builder.
081         * 
082         * @param printer  the internal printer, null if cannot print
083         * @param parser  the internal parser, null if cannot parse
084         */
085        public PeriodFormatter(
086                PeriodPrinter printer, PeriodParser parser) {
087            super();
088            iPrinter = printer;
089            iParser = parser;
090            iLocale = null;
091            iParseType = null;
092        }
093    
094        /**
095         * Constructor.
096         * 
097         * @param printer  the internal printer, null if cannot print
098         * @param parser  the internal parser, null if cannot parse
099         * @param locale  the locale to use
100         * @param type  the parse period type
101         */
102        private PeriodFormatter(
103                PeriodPrinter printer, PeriodParser parser,
104                Locale locale, PeriodType type) {
105            super();
106            iPrinter = printer;
107            iParser = parser;
108            iLocale = locale;
109            iParseType = type;
110        }
111    
112        //-----------------------------------------------------------------------
113        /**
114         * Is this formatter capable of printing.
115         * 
116         * @return true if this is a printer
117         */
118        public boolean isPrinter() {
119            return (iPrinter != null);
120        }
121    
122        /**
123         * Gets the internal printer object that performs the real printing work.
124         * 
125         * @return the internal printer
126         */
127        public PeriodPrinter getPrinter() {
128            return iPrinter;
129        }
130    
131        /**
132         * Is this formatter capable of parsing.
133         * 
134         * @return true if this is a parser
135         */
136        public boolean isParser() {
137            return (iParser != null);
138        }
139    
140        /**
141         * Gets the internal parser object that performs the real parsing work.
142         * 
143         * @return the internal parser
144         */
145        public PeriodParser getParser() {
146            return iParser;
147        }
148    
149        //-----------------------------------------------------------------------
150        /**
151         * Returns a new formatter with a different locale that will be used
152         * for printing and parsing.
153         * <p>
154         * A PeriodFormatter is immutable, so a new instance is returned,
155         * and the original is unaltered and still usable.
156         * 
157         * @param locale  the locale to use
158         * @return the new formatter
159         */
160        public PeriodFormatter withLocale(Locale locale) {
161            if (locale == getLocale() || (locale != null && locale.equals(getLocale()))) {
162                return this;
163            }
164            return new PeriodFormatter(iPrinter, iParser, locale, iParseType);
165        }
166    
167        /**
168         * Gets the locale that will be used for printing and parsing.
169         * 
170         * @return the locale to use
171         */
172        public Locale getLocale() {
173            return iLocale;
174        }
175    
176        //-----------------------------------------------------------------------
177        /**
178         * Returns a new formatter with a different PeriodType for parsing.
179         * <p>
180         * A PeriodFormatter is immutable, so a new instance is returned,
181         * and the original is unaltered and still usable.
182         * 
183         * @param type  the type to use in parsing
184         * @return the new formatter
185         */
186        public PeriodFormatter withParseType(PeriodType type) {
187            if (type == iParseType) {
188                return this;
189            }
190            return new PeriodFormatter(iPrinter, iParser, iLocale, type);
191        }
192    
193        /**
194         * Gets the PeriodType that will be used for parsing.
195         * 
196         * @return the parse type to use
197         */
198        public PeriodType getParseType() {
199            return iParseType;
200        }
201    
202        //-----------------------------------------------------------------------
203        /**
204         * Prints a ReadablePeriod to a StringBuffer.
205         *
206         * @param buf  the formatted period is appended to this buffer
207         * @param period  the period to format, not null
208         */
209        public void printTo(StringBuffer buf, ReadablePeriod period) {
210            checkPrinter();
211            checkPeriod(period);
212            
213            getPrinter().printTo(buf, period, iLocale);
214        }
215    
216        /**
217         * Prints a ReadablePeriod to a Writer.
218         *
219         * @param out  the formatted period is written out
220         * @param period  the period to format, not null
221         */
222        public void printTo(Writer out, ReadablePeriod period) throws IOException {
223            checkPrinter();
224            checkPeriod(period);
225            
226            getPrinter().printTo(out, period, iLocale);
227        }
228    
229        /**
230         * Prints a ReadablePeriod to a new String.
231         *
232         * @param period  the period to format, not null
233         * @return the printed result
234         */
235        public String print(ReadablePeriod period) {
236            checkPrinter();
237            checkPeriod(period);
238            
239            PeriodPrinter printer = getPrinter();
240            StringBuffer buf = new StringBuffer(printer.calculatePrintedLength(period, iLocale));
241            printer.printTo(buf, period, iLocale);
242            return buf.toString();
243        }
244    
245        /**
246         * Checks whether printing is supported.
247         * 
248         * @throws UnsupportedOperationException if printing is not supported
249         */
250        private void checkPrinter() {
251            if (iPrinter == null) {
252                throw new UnsupportedOperationException("Printing not supported");
253            }
254        }
255    
256        /**
257         * Checks whether the period is non-null.
258         * 
259         * @throws IllegalArgumentException if the period is null
260         */
261        private void checkPeriod(ReadablePeriod period) {
262            if (period == null) {
263                throw new IllegalArgumentException("Period must not be null");
264            }
265        }
266    
267        //-----------------------------------------------------------------------
268        /**
269         * Parses a period from the given text, at the given position, saving the
270         * result into the fields of the given ReadWritablePeriod. If the parse
271         * succeeds, the return value is the new text position. Note that the parse
272         * may succeed without fully reading the text.
273         * <p>
274         * The parse type of the formatter is not used by this method.
275         * <p>
276         * If it fails, the return value is negative, but the period may still be
277         * modified. To determine the position where the parse failed, apply the
278         * one's complement operator (~) on the return value.
279         *
280         * @param period  a period that will be modified
281         * @param text  text to parse
282         * @param position position to start parsing from
283         * @return new position, if negative, parse failed. Apply complement
284         * operator (~) to get position of failure
285         * @throws IllegalArgumentException if any field is out of range
286         */
287        public int parseInto(ReadWritablePeriod period, String text, int position) {
288            checkParser();
289            checkPeriod(period);
290            
291            return getParser().parseInto(period, text, position, iLocale);
292        }
293    
294        /**
295         * Parses a period from the given text, returning a new Period.
296         *
297         * @param text  text to parse
298         * @return parsed value in a Period object
299         * @throws IllegalArgumentException if any field is out of range
300         */
301        public Period parsePeriod(String text) {
302            checkParser();
303            
304            return parseMutablePeriod(text).toPeriod();
305        }
306    
307        /**
308         * Parses a period from the given text, returning a new MutablePeriod.
309         *
310         * @param text  text to parse
311         * @return parsed value in a MutablePeriod object
312         * @throws IllegalArgumentException if any field is out of range
313         */
314        public MutablePeriod parseMutablePeriod(String text) {
315            checkParser();
316            
317            MutablePeriod period = new MutablePeriod(0, iParseType);
318            int newPos = getParser().parseInto(period, text, 0, iLocale);
319            if (newPos >= 0) {
320                if (newPos >= text.length()) {
321                    return period;
322                }
323            } else {
324                newPos = ~newPos;
325            }
326            throw new IllegalArgumentException(FormatUtils.createErrorMessage(text, newPos));
327        }
328    
329        /**
330         * Checks whether parsing is supported.
331         * 
332         * @throws UnsupportedOperationException if parsing is not supported
333         */
334        private void checkParser() {
335            if (iParser == null) {
336                throw new UnsupportedOperationException("Parsing not supported");
337            }
338        }
339    
340    }