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.lang3.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.lang3.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 905268 2010-02-01 12:01:04Z 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<String, ? extends FormatFactory> 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<String, ? extends FormatFactory> 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<String, ? extends FormatFactory> registry) {
124            super(DUMMY_PATTERN);
125            setLocale(locale);
126            this.registry = registry;
127            applyPattern(pattern);
128        }
129    
130        /**
131         * {@inheritDoc}
132         */
133        @Override
134        public String toPattern() {
135            return toPattern;
136        }
137    
138        /**
139         * Apply the specified pattern.
140         * 
141         * @param pattern String
142         */
143        @Override
144        public final void applyPattern(String pattern) {
145            if (registry == null) {
146                super.applyPattern(pattern);
147                toPattern = super.toPattern();
148                return;
149            }
150            ArrayList<Format> foundFormats = new ArrayList<Format>();
151            ArrayList<String> foundDescriptions = new ArrayList<String>();
152            StringBuilder stripCustom = new StringBuilder(pattern.length());
153    
154            ParsePosition pos = new ParsePosition(0);
155            char[] c = pattern.toCharArray();
156            int fmtCount = 0;
157            while (pos.getIndex() < pattern.length()) {
158                switch (c[pos.getIndex()]) {
159                case QUOTE:
160                    appendQuotedString(pattern, pos, stripCustom, true);
161                    break;
162                case START_FE:
163                    fmtCount++;
164                    seekNonWs(pattern, pos);
165                    int start = pos.getIndex();
166                    int index = readArgumentIndex(pattern, next(pos));
167                    stripCustom.append(START_FE).append(index);
168                    seekNonWs(pattern, pos);
169                    Format format = null;
170                    String formatDescription = null;
171                    if (c[pos.getIndex()] == START_FMT) {
172                        formatDescription = parseFormatDescription(pattern,
173                                next(pos));
174                        format = getFormat(formatDescription);
175                        if (format == null) {
176                            stripCustom.append(START_FMT).append(formatDescription);
177                        }
178                    }
179                    foundFormats.add(format);
180                    foundDescriptions.add(format == null ? null : formatDescription);
181                    Validate.isTrue(foundFormats.size() == fmtCount);
182                    Validate.isTrue(foundDescriptions.size() == fmtCount);
183                    if (c[pos.getIndex()] != END_FE) {
184                        throw new IllegalArgumentException(
185                                "Unreadable format element at position " + start);
186                    }
187                    //$FALL-THROUGH$
188                default:
189                    stripCustom.append(c[pos.getIndex()]);
190                    next(pos);
191                }
192            }
193            super.applyPattern(stripCustom.toString());
194            toPattern = insertFormats(super.toPattern(), foundDescriptions);
195            if (containsElements(foundFormats)) {
196                Format[] origFormats = getFormats();
197                // only loop over what we know we have, as MessageFormat on Java 1.3 
198                // seems to provide an extra format element:
199                int i = 0;
200                for (Iterator<Format> it = foundFormats.iterator(); it.hasNext(); i++) {
201                    Format f = it.next();
202                    if (f != null) {
203                        origFormats[i] = f;
204                    }
205                }
206                super.setFormats(origFormats);
207            }
208        }
209    
210        /**
211         * {@inheritDoc}
212         * @throws UnsupportedOperationException
213         */
214        @Override
215        public void setFormat(int formatElementIndex, Format newFormat) {
216            throw new UnsupportedOperationException();
217        }
218    
219        /**
220         * {@inheritDoc}
221         * @throws UnsupportedOperationException
222         */
223        @Override
224        public void setFormatByArgumentIndex(int argumentIndex, Format newFormat) {
225            throw new UnsupportedOperationException();
226        }
227    
228        /**
229         * {@inheritDoc}
230         * @throws UnsupportedOperationException
231         */
232        @Override
233        public void setFormats(Format[] newFormats) {
234            throw new UnsupportedOperationException();
235        }
236    
237        /**
238         * {@inheritDoc}
239         * @throws UnsupportedOperationException
240         */
241        @Override
242        public void setFormatsByArgumentIndex(Format[] newFormats) {
243            throw new UnsupportedOperationException();
244        }
245    
246        /**
247         * Get a custom format from a format description.
248         * 
249         * @param desc String
250         * @return Format
251         */
252        private Format getFormat(String desc) {
253            if (registry != null) {
254                String name = desc;
255                String args = null;
256                int i = desc.indexOf(START_FMT);
257                if (i > 0) {
258                    name = desc.substring(0, i).trim();
259                    args = desc.substring(i + 1).trim();
260                }
261                FormatFactory factory = registry.get(name);
262                if (factory != null) {
263                    return factory.getFormat(name, args, getLocale());
264                }
265            }
266            return null;
267        }
268    
269        /**
270         * Read the argument index from the current format element
271         * 
272         * @param pattern pattern to parse
273         * @param pos current parse position
274         * @return argument index
275         */
276        private int readArgumentIndex(String pattern, ParsePosition pos) {
277            int start = pos.getIndex();
278            seekNonWs(pattern, pos);
279            StringBuffer result = new StringBuffer();
280            boolean error = false;
281            for (; !error && pos.getIndex() < pattern.length(); next(pos)) {
282                char c = pattern.charAt(pos.getIndex());
283                if (Character.isWhitespace(c)) {
284                    seekNonWs(pattern, pos);
285                    c = pattern.charAt(pos.getIndex());
286                    if (c != START_FMT && c != END_FE) {
287                        error = true;
288                        continue;
289                    }
290                }
291                if ((c == START_FMT || c == END_FE) && result.length() > 0) {
292                    try {
293                        return Integer.parseInt(result.toString());
294                    } catch (NumberFormatException e) {
295                        // we've already ensured only digits, so unless something
296                        // outlandishly large was specified we should be okay.
297                    }
298                }
299                error = !Character.isDigit(c);
300                result.append(c);
301            }
302            if (error) {
303                throw new IllegalArgumentException(
304                        "Invalid format argument index at position " + start + ": "
305                                + pattern.substring(start, pos.getIndex()));
306            }
307            throw new IllegalArgumentException(
308                    "Unterminated format element at position " + start);
309        }
310    
311        /**
312         * Parse the format component of a format element.
313         * 
314         * @param pattern string to parse
315         * @param pos current parse position
316         * @return Format description String
317         */
318        private String parseFormatDescription(String pattern, ParsePosition pos) {
319            int start = pos.getIndex();
320            seekNonWs(pattern, pos);
321            int text = pos.getIndex();
322            int depth = 1;
323            for (; pos.getIndex() < pattern.length(); next(pos)) {
324                switch (pattern.charAt(pos.getIndex())) {
325                case START_FE:
326                    depth++;
327                    break;
328                case END_FE:
329                    depth--;
330                    if (depth == 0) {
331                        return pattern.substring(text, pos.getIndex());
332                    }
333                    break;
334                case QUOTE:
335                    getQuotedString(pattern, pos, false);
336                    break;
337                }
338            }
339            throw new IllegalArgumentException(
340                    "Unterminated format element at position " + start);
341        }
342    
343        /**
344         * Insert formats back into the pattern for toPattern() support.
345         *
346         * @param pattern source
347         * @param customPatterns The custom patterns to re-insert, if any
348         * @return full pattern
349         */
350        private String insertFormats(String pattern, ArrayList<String> customPatterns) {
351            if (!containsElements(customPatterns)) {
352                return pattern;
353            }
354            StringBuilder sb = new StringBuilder(pattern.length() * 2);
355            ParsePosition pos = new ParsePosition(0);
356            int fe = -1;
357            int depth = 0;
358            while (pos.getIndex() < pattern.length()) {
359                char c = pattern.charAt(pos.getIndex());
360                switch (c) {
361                case QUOTE:
362                    appendQuotedString(pattern, pos, sb, false);
363                    break;
364                case START_FE:
365                    depth++;
366                    if (depth == 1) {
367                        fe++;
368                        sb.append(START_FE).append(
369                                readArgumentIndex(pattern, next(pos)));
370                        String customPattern = customPatterns.get(fe);
371                        if (customPattern != null) {
372                            sb.append(START_FMT).append(customPattern);
373                        }
374                    }
375                    break;
376                case END_FE:
377                    depth--;
378                    //$FALL-THROUGH$
379                default:
380                    sb.append(c);
381                    next(pos);
382                }
383            }
384            return sb.toString();
385        }
386    
387        /**
388         * Consume whitespace from the current parse position.
389         * 
390         * @param pattern String to read
391         * @param pos current position
392         */
393        private void seekNonWs(String pattern, ParsePosition pos) {
394            int len = 0;
395            char[] buffer = pattern.toCharArray();
396            do {
397                len = StrMatcher.splitMatcher().isMatch(buffer, pos.getIndex());
398                pos.setIndex(pos.getIndex() + len);
399            } while (len > 0 && pos.getIndex() < pattern.length());
400        }
401    
402        /**
403         * Convenience method to advance parse position by 1
404         * 
405         * @param pos ParsePosition
406         * @return <code>pos</code>
407         */
408        private ParsePosition next(ParsePosition pos) {
409            pos.setIndex(pos.getIndex() + 1);
410            return pos;
411        }
412    
413        /**
414         * Consume a quoted string, adding it to <code>appendTo</code> if
415         * specified.
416         * 
417         * @param pattern pattern to parse
418         * @param pos current parse position
419         * @param appendTo optional StringBuffer to append
420         * @param escapingOn whether to process escaped quotes
421         * @return <code>appendTo</code>
422         */
423        private StringBuilder appendQuotedString(String pattern, ParsePosition pos,
424                StringBuilder appendTo, boolean escapingOn) {
425            int start = pos.getIndex();
426            char[] c = pattern.toCharArray();
427            if (escapingOn && c[start] == QUOTE) {
428                next(pos);
429                return appendTo == null ? null : appendTo.append(QUOTE);
430            }
431            int lastHold = start;
432            for (int i = pos.getIndex(); i < pattern.length(); i++) {
433                if (escapingOn && pattern.substring(i).startsWith(ESCAPED_QUOTE)) {
434                    appendTo.append(c, lastHold, pos.getIndex() - lastHold).append(
435                            QUOTE);
436                    pos.setIndex(i + ESCAPED_QUOTE.length());
437                    lastHold = pos.getIndex();
438                    continue;
439                }
440                switch (c[pos.getIndex()]) {
441                case QUOTE:
442                    next(pos);
443                    return appendTo == null ? null : appendTo.append(c, lastHold,
444                            pos.getIndex() - lastHold);
445                default:
446                    next(pos);
447                }
448            }
449            throw new IllegalArgumentException(
450                    "Unterminated quoted string at position " + start);
451        }
452    
453        /**
454         * Consume quoted string only
455         * 
456         * @param pattern pattern to parse
457         * @param pos current parse position
458         * @param escapingOn whether to process escaped quotes
459         */
460        private void getQuotedString(String pattern, ParsePosition pos,
461                boolean escapingOn) {
462            appendQuotedString(pattern, pos, null, escapingOn);
463        }
464    
465        /**
466         * Learn whether the specified Collection contains non-null elements.
467         * @param coll to check
468         * @return <code>true</code> if some Object was found, <code>false</code> otherwise.
469         */
470        private boolean containsElements(Collection<?> coll) {
471            if (coll == null || coll.size() == 0) {
472                return false;
473            }
474            for (Iterator<?> iter = coll.iterator(); iter.hasNext();) {
475                if (iter.next() != null) {
476                    return true;
477                }
478            }
479            return false;
480        }
481    }