001    /*
002     * Licensed to the Apache Software Foundation (ASF) under one or more
003     * contributor license agreements.  See the NOTICE file distributed with
004     * this work for additional information regarding copyright ownership.
005     * The ASF licenses this file to You under the Apache License, Version 2.0
006     * (the "License"); you may not use this file except in compliance with
007     * the License.  You may obtain a copy of 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,
013     * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
014     * See the License for the specific language governing permissions and
015     * limitations under the License.
016     */
017    package org.apache.commons.lang.text;
018    
019    import java.text.Format;
020    import java.text.MessageFormat;
021    import java.text.ParsePosition;
022    import java.util.ArrayList;
023    import java.util.Collection;
024    import java.util.Iterator;
025    import java.util.Locale;
026    import java.util.Map;
027    
028    import org.apache.commons.lang.Validate;
029    
030    /**
031     * Extends <code>java.text.MessageFormat</code> to allow pluggable/additional formatting
032     * options for embedded format elements.  Client code should specify a registry
033     * of <code>FormatFactory</code> instances associated with <code>String</code>
034     * format names.  This registry will be consulted when the format elements are 
035     * parsed from the message pattern.  In this way custom patterns can be specified,
036     * and the formats supported by <code>java.text.MessageFormat</code> can be overridden
037     * at the format and/or format style level (see MessageFormat).  A "format element"
038     * embedded in the message pattern is specified (<b>()?</b> signifies optionality):<br />
039     * <code>{</code><i>argument-number</i><b>(</b><code>,</code><i>format-name</i><b>(</b><code>,</code><i>format-style</i><b>)?)?</b><code>}</code>
040     *
041     * <p>
042     * <i>format-name</i> and <i>format-style</i> values are trimmed of surrounding whitespace
043     * in the manner of <code>java.text.MessageFormat</code>.  If <i>format-name</i> denotes
044     * <code>FormatFactory formatFactoryInstance</code> in <code>registry</code>, a <code>Format</code>
045     * matching <i>format-name</i> and <i>format-style</i> is requested from
046     * <code>formatFactoryInstance</code>.  If this is successful, the <code>Format</code>
047     * found is used for this format element.
048     * </p>
049     *
050     * <p>NOTICE: The various subformat mutator methods are considered unnecessary; they exist on the parent
051     * class to allow the type of customization which it is the job of this class to provide in
052     * a configurable fashion.  These methods have thus been disabled and will throw
053     * <code>UnsupportedOperationException</code> if called.
054     * </p>
055     * 
056     * <p>Limitations inherited from <code>java.text.MessageFormat</code>:
057     * <ul>
058     * <li>When using "choice" subformats, support for nested formatting instructions is limited
059     *     to that provided by the base class.</li>
060     * <li>Thread-safety of <code>Format</code>s, including <code>MessageFormat</code> and thus
061     *     <code>ExtendedMessageFormat</code>, is not guaranteed.</li>
062     * </ul>
063     * </p>
064     * 
065     * @author Apache Software Foundation
066     * @author Matt Benson
067     * @since 2.4
068     * @version $Id: ExtendedMessageFormat.java 905636 2010-02-02 14:03:32Z niallp $
069     */
070    public class ExtendedMessageFormat extends MessageFormat {
071        private static final long serialVersionUID = -2362048321261811743L;
072    
073        private static final String DUMMY_PATTERN = "";
074        private static final String ESCAPED_QUOTE = "''";
075        private static final char START_FMT = ',';
076        private static final char END_FE = '}';
077        private static final char START_FE = '{';
078        private static final char QUOTE = '\'';
079    
080        private String toPattern;
081        private final Map registry;
082    
083        /**
084         * Create a new ExtendedMessageFormat for the default locale.
085         * 
086         * @param pattern  the pattern to use, not null
087         * @throws IllegalArgumentException in case of a bad pattern.
088         */
089        public ExtendedMessageFormat(String pattern) {
090            this(pattern, Locale.getDefault());
091        }
092    
093        /**
094         * Create a new ExtendedMessageFormat.
095         * 
096         * @param pattern  the pattern to use, not null
097         * @param locale  the locale to use, not null
098         * @throws IllegalArgumentException in case of a bad pattern.
099         */
100        public ExtendedMessageFormat(String pattern, Locale locale) {
101            this(pattern, locale, null);
102        }
103    
104        /**
105         * Create a new ExtendedMessageFormat for the default locale.
106         * 
107         * @param pattern  the pattern to use, not null
108         * @param registry  the registry of format factories, may be null
109         * @throws IllegalArgumentException in case of a bad pattern.
110         */
111        public ExtendedMessageFormat(String pattern, Map registry) {
112            this(pattern, Locale.getDefault(), registry);
113        }
114    
115        /**
116         * Create a new ExtendedMessageFormat.
117         * 
118         * @param pattern  the pattern to use, not null
119         * @param locale  the locale to use, not null
120         * @param registry  the registry of format factories, may be null
121         * @throws IllegalArgumentException in case of a bad pattern.
122         */
123        public ExtendedMessageFormat(String pattern, Locale locale, Map registry) {
124            super(DUMMY_PATTERN);
125            setLocale(locale);
126            this.registry = registry;
127            applyPattern(pattern);
128        }
129    
130        /**
131         * {@inheritDoc}
132         */
133        public String toPattern() {
134            return toPattern;
135        }
136    
137        /**
138         * Apply the specified pattern.
139         * 
140         * @param pattern String
141         */
142        public final void applyPattern(String pattern) {
143            if (registry == null) {
144                super.applyPattern(pattern);
145                toPattern = super.toPattern();
146                return;
147            }
148            ArrayList foundFormats = new ArrayList();
149            ArrayList foundDescriptions = new ArrayList();
150            StringBuffer stripCustom = new StringBuffer(pattern.length());
151    
152            ParsePosition pos = new ParsePosition(0);
153            char[] c = pattern.toCharArray();
154            int fmtCount = 0;
155            while (pos.getIndex() < pattern.length()) {
156                switch (c[pos.getIndex()]) {
157                case QUOTE:
158                    appendQuotedString(pattern, pos, stripCustom, true);
159                    break;
160                case START_FE:
161                    fmtCount++;
162                    seekNonWs(pattern, pos);
163                    int start = pos.getIndex();
164                    int index = readArgumentIndex(pattern, next(pos));
165                    stripCustom.append(START_FE).append(index);
166                    seekNonWs(pattern, pos);
167                    Format format = null;
168                    String formatDescription = null;
169                    if (c[pos.getIndex()] == START_FMT) {
170                        formatDescription = parseFormatDescription(pattern,
171                                next(pos));
172                        format = getFormat(formatDescription);
173                        if (format == null) {
174                            stripCustom.append(START_FMT).append(formatDescription);
175                        }
176                    }
177                    foundFormats.add(format);
178                    foundDescriptions.add(format == null ? null : formatDescription);
179                    Validate.isTrue(foundFormats.size() == fmtCount);
180                    Validate.isTrue(foundDescriptions.size() == fmtCount);
181                    if (c[pos.getIndex()] != END_FE) {
182                        throw new IllegalArgumentException(
183                                "Unreadable format element at position " + start);
184                    }
185                    //$FALL-THROUGH$
186                default:
187                    stripCustom.append(c[pos.getIndex()]);
188                    next(pos);
189                }
190            }
191            super.applyPattern(stripCustom.toString());
192            toPattern = insertFormats(super.toPattern(), foundDescriptions);
193            if (containsElements(foundFormats)) {
194                Format[] origFormats = getFormats();
195                // only loop over what we know we have, as MessageFormat on Java 1.3 
196                // seems to provide an extra format element:
197                int i = 0;
198                for (Iterator it = foundFormats.iterator(); it.hasNext(); i++) {
199                    Format f = (Format) it.next();
200                    if (f != null) {
201                        origFormats[i] = f;
202                    }
203                }
204                super.setFormats(origFormats);
205            }
206        }
207    
208        /**
209         * {@inheritDoc}
210         * @throws UnsupportedOperationException
211         */
212        public void setFormat(int formatElementIndex, Format newFormat) {
213            throw new UnsupportedOperationException();
214        }
215    
216        /**
217         * {@inheritDoc}
218         * @throws UnsupportedOperationException
219         */
220        public void setFormatByArgumentIndex(int argumentIndex, Format newFormat) {
221            throw new UnsupportedOperationException();
222        }
223    
224        /**
225         * {@inheritDoc}
226         * @throws UnsupportedOperationException
227         */
228        public void setFormats(Format[] newFormats) {
229            throw new UnsupportedOperationException();
230        }
231    
232        /**
233         * {@inheritDoc}
234         * @throws UnsupportedOperationException
235         */
236        public void setFormatsByArgumentIndex(Format[] newFormats) {
237            throw new UnsupportedOperationException();
238        }
239    
240        /**
241         * Get a custom format from a format description.
242         * 
243         * @param desc String
244         * @return Format
245         */
246        private Format getFormat(String desc) {
247            if (registry != null) {
248                String name = desc;
249                String args = null;
250                int i = desc.indexOf(START_FMT);
251                if (i > 0) {
252                    name = desc.substring(0, i).trim();
253                    args = desc.substring(i + 1).trim();
254                }
255                FormatFactory factory = (FormatFactory) registry.get(name);
256                if (factory != null) {
257                    return factory.getFormat(name, args, getLocale());
258                }
259            }
260            return null;
261        }
262    
263        /**
264         * Read the argument index from the current format element
265         * 
266         * @param pattern pattern to parse
267         * @param pos current parse position
268         * @return argument index
269         */
270        private int readArgumentIndex(String pattern, ParsePosition pos) {
271            int start = pos.getIndex();
272            seekNonWs(pattern, pos);
273            StringBuffer result = new StringBuffer();
274            boolean error = false;
275            for (; !error && pos.getIndex() < pattern.length(); next(pos)) {
276                char c = pattern.charAt(pos.getIndex());
277                if (Character.isWhitespace(c)) {
278                    seekNonWs(pattern, pos);
279                    c = pattern.charAt(pos.getIndex());
280                    if (c != START_FMT && c != END_FE) {
281                        error = true;
282                        continue;
283                    }
284                }
285                if ((c == START_FMT || c == END_FE) && result.length() > 0) {
286                    try {
287                        return Integer.parseInt(result.toString());
288                    } catch (NumberFormatException e) {
289                        // we've already ensured only digits, so unless something
290                        // outlandishly large was specified we should be okay.
291                    }
292                }
293                error = !Character.isDigit(c);
294                result.append(c);
295            }
296            if (error) {
297                throw new IllegalArgumentException(
298                        "Invalid format argument index at position " + start + ": "
299                                + pattern.substring(start, pos.getIndex()));
300            }
301            throw new IllegalArgumentException(
302                    "Unterminated format element at position " + start);
303        }
304    
305        /**
306         * Parse the format component of a format element.
307         * 
308         * @param pattern string to parse
309         * @param pos current parse position
310         * @return Format description String
311         */
312        private String parseFormatDescription(String pattern, ParsePosition pos) {
313            int start = pos.getIndex();
314            seekNonWs(pattern, pos);
315            int text = pos.getIndex();
316            int depth = 1;
317            for (; pos.getIndex() < pattern.length(); next(pos)) {
318                switch (pattern.charAt(pos.getIndex())) {
319                case START_FE:
320                    depth++;
321                    break;
322                case END_FE:
323                    depth--;
324                    if (depth == 0) {
325                        return pattern.substring(text, pos.getIndex());
326                    }
327                    break;
328                case QUOTE:
329                    getQuotedString(pattern, pos, false);
330                    break;
331                }
332            }
333            throw new IllegalArgumentException(
334                    "Unterminated format element at position " + start);
335        }
336    
337        /**
338         * Insert formats back into the pattern for toPattern() support.
339         *
340         * @param pattern source
341         * @param customPatterns The custom patterns to re-insert, if any
342         * @return full pattern
343         */
344        private String insertFormats(String pattern, ArrayList customPatterns) {
345            if (!containsElements(customPatterns)) {
346                return pattern;
347            }
348            StringBuffer sb = new StringBuffer(pattern.length() * 2);
349            ParsePosition pos = new ParsePosition(0);
350            int fe = -1;
351            int depth = 0;
352            while (pos.getIndex() < pattern.length()) {
353                char c = pattern.charAt(pos.getIndex());
354                switch (c) {
355                case QUOTE:
356                    appendQuotedString(pattern, pos, sb, false);
357                    break;
358                case START_FE:
359                    depth++;
360                    if (depth == 1) {
361                        fe++;
362                        sb.append(START_FE).append(
363                                readArgumentIndex(pattern, next(pos)));
364                        String customPattern = (String) customPatterns.get(fe);
365                        if (customPattern != null) {
366                            sb.append(START_FMT).append(customPattern);
367                        }
368                    }
369                    break;
370                case END_FE:
371                    depth--;
372                    //$FALL-THROUGH$
373                default:
374                    sb.append(c);
375                    next(pos);
376                }
377            }
378            return sb.toString();
379        }
380    
381        /**
382         * Consume whitespace from the current parse position.
383         * 
384         * @param pattern String to read
385         * @param pos current position
386         */
387        private void seekNonWs(String pattern, ParsePosition pos) {
388            int len = 0;
389            char[] buffer = pattern.toCharArray();
390            do {
391                len = StrMatcher.splitMatcher().isMatch(buffer, pos.getIndex());
392                pos.setIndex(pos.getIndex() + len);
393            } while (len > 0 && pos.getIndex() < pattern.length());
394        }
395    
396        /**
397         * Convenience method to advance parse position by 1
398         * 
399         * @param pos ParsePosition
400         * @return <code>pos</code>
401         */
402        private ParsePosition next(ParsePosition pos) {
403            pos.setIndex(pos.getIndex() + 1);
404            return pos;
405        }
406    
407        /**
408         * Consume a quoted string, adding it to <code>appendTo</code> if
409         * specified.
410         * 
411         * @param pattern pattern to parse
412         * @param pos current parse position
413         * @param appendTo optional StringBuffer to append
414         * @param escapingOn whether to process escaped quotes
415         * @return <code>appendTo</code>
416         */
417        private StringBuffer appendQuotedString(String pattern, ParsePosition pos,
418                StringBuffer appendTo, boolean escapingOn) {
419            int start = pos.getIndex();
420            char[] c = pattern.toCharArray();
421            if (escapingOn && c[start] == QUOTE) {
422                next(pos);
423                return appendTo == null ? null : appendTo.append(QUOTE);
424            }
425            int lastHold = start;
426            for (int i = pos.getIndex(); i < pattern.length(); i++) {
427                if (escapingOn && pattern.substring(i).startsWith(ESCAPED_QUOTE)) {
428                    appendTo.append(c, lastHold, pos.getIndex() - lastHold).append(
429                            QUOTE);
430                    pos.setIndex(i + ESCAPED_QUOTE.length());
431                    lastHold = pos.getIndex();
432                    continue;
433                }
434                switch (c[pos.getIndex()]) {
435                case QUOTE:
436                    next(pos);
437                    return appendTo == null ? null : appendTo.append(c, lastHold,
438                            pos.getIndex() - lastHold);
439                default:
440                    next(pos);
441                }
442            }
443            throw new IllegalArgumentException(
444                    "Unterminated quoted string at position " + start);
445        }
446    
447        /**
448         * Consume quoted string only
449         * 
450         * @param pattern pattern to parse
451         * @param pos current parse position
452         * @param escapingOn whether to process escaped quotes
453         */
454        private void getQuotedString(String pattern, ParsePosition pos,
455                boolean escapingOn) {
456            appendQuotedString(pattern, pos, null, escapingOn);
457        }
458    
459        /**
460         * Learn whether the specified Collection contains non-null elements.
461         * @param coll to check
462         * @return <code>true</code> if some Object was found, <code>false</code> otherwise.
463         */
464        private boolean containsElements(Collection coll) {
465            if (coll == null || coll.size() == 0) {
466                return false;
467            }
468            for (Iterator iter = coll.iterator(); iter.hasNext();) {
469                if (iter.next() != null) {
470                    return true;
471                }
472            }
473            return false;
474        }
475    }