humanize.text.ExtendedMessageFormat Maven / Gradle / Ivy
The newest version!
/*
* Licensed to the Apache Software Foundation (ASF) under one or more
* contributor license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright ownership.
* The ASF licenses this file to You under the Apache License, Version 2.0
* (the "License"); you may not use this file except in compliance with
* the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package humanize.text;
import java.text.Format;
import java.text.MessageFormat;
import java.text.ParsePosition;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.Iterator;
import java.util.Locale;
import java.util.Map;
import com.google.common.base.Preconditions;
/**
* Extends java.text.MessageFormat
to allow pluggable/additional
* formatting options for embedded format elements. Client code should specify a
* registry of FormatFactory
instances associated with
* String
format names. This registry will be consulted when the
* format elements are parsed from the message pattern. In this way custom
* patterns can be specified, and the formats supported by
* java.text.MessageFormat
can be overridden at the format and/or
* format style level (see MessageFormat). A "format element" embedded in the
* message pattern is specified (()? signifies optionality):
* {
argument-number(,
* format-name (,
format-style)?)?
* }
*
*
* format-name and format-style values are trimmed of surrounding
* whitespace in the manner of java.text.MessageFormat
. If
* format-name denotes FormatFactory formatFactoryInstance
* in registry
, a Format
matching format-name
* and format-style is requested from formatFactoryInstance
.
* If this is successful, the Format
found is used for this format
* element.
*
*
*
* Limitations inherited from java.text.MessageFormat
:
*
* - When using "choice" subformats, support for nested formatting
* instructions is limited to that provided by the base class.
* - Thread-safety of
Format
s, including
* MessageFormat
and thus ExtendedMessageFormat
, is
* not guaranteed.
*
*
*
* @version $Id: ExtendedMessageFormat.java 1144929 2011-07-10 18:26:16Z
* ggregory $
*/
public class ExtendedMessageFormat extends MessageFormat
{
private static final long serialVersionUID = -2362048321261811743L;
private static final String DUMMY_PATTERN = "";
private static final String ESCAPED_QUOTE = "''";
private static final char START_FMT = ',';
private static final char END_FE = '}';
private static final char START_FE = '{';
private static final char QUOTE = '\'';
private String toPattern;
private final Map registry;
private static final char[] SPLIT_CHARS = " \t\n\r\f".toCharArray();
static
{
Arrays.sort(SPLIT_CHARS);
}
/**
* Create a new ExtendedMessageFormat for the default locale.
*
* @param pattern
* the pattern to use, not null
* @throws IllegalArgumentException
* in case of a bad pattern.
*/
public ExtendedMessageFormat(String pattern)
{
this(pattern, Locale.getDefault());
}
/**
* Create a new ExtendedMessageFormat.
*
* @param pattern
* the pattern to use, not null
* @param locale
* the locale to use, not null
* @throws IllegalArgumentException
* in case of a bad pattern.
*/
public ExtendedMessageFormat(String pattern, Locale locale)
{
this(pattern, locale, null);
}
/**
* Create a new ExtendedMessageFormat.
*
* @param pattern
* the pattern to use, not null
* @param locale
* the locale to use, not null
* @param registry
* the registry of format factories, may be null
* @throws IllegalArgumentException
* in case of a bad pattern.
*/
public ExtendedMessageFormat(String pattern, Locale locale, Map registry)
{
super(DUMMY_PATTERN);
setLocale(locale);
this.registry = registry;
applyPattern(pattern);
}
/**
* Create a new ExtendedMessageFormat for the default locale.
*
* @param pattern
* the pattern to use, not null
* @param registry
* the registry of format factories, may be null
* @throws IllegalArgumentException
* in case of a bad pattern.
*/
public ExtendedMessageFormat(String pattern, Map registry)
{
this(pattern, Locale.getDefault(), registry);
}
/**
* Apply the specified pattern.
*
* @param pattern
* String
*/
@Override
public final void applyPattern(String pattern)
{
if (hasRegistry())
{
super.applyPattern(pattern);
toPattern = super.toPattern();
return;
}
ArrayList foundFormats = new ArrayList();
ArrayList foundDescriptions = new ArrayList();
StringBuilder stripCustom = new StringBuilder(pattern.length());
ParsePosition pos = new ParsePosition(0);
char[] c = pattern.toCharArray();
int fmtCount = 0;
while (pos.getIndex() < pattern.length())
{
char charType = c[pos.getIndex()];
if (QUOTE == charType)
{
appendQuotedString(pattern, pos, stripCustom, true);
continue;
}
if (START_FE == charType)
{
fmtCount++;
seekNonWs(pattern, pos);
int start = pos.getIndex();
int index = readArgumentIndex(pattern, next(pos));
stripCustom.append(START_FE).append(index);
seekNonWs(pattern, pos);
Format format = null;
String formatDescription = null;
if (c[pos.getIndex()] == START_FMT)
{
formatDescription = parseFormatDescription(pattern, next(pos));
format = getFormat(formatDescription);
if (format == null)
{
stripCustom.append(START_FMT).append(formatDescription);
}
}
foundFormats.add(format);
foundDescriptions.add(format == null ? null : formatDescription);
Preconditions.checkState(foundFormats.size() == fmtCount);
Preconditions.checkState(foundDescriptions.size() == fmtCount);
if (c[pos.getIndex()] != END_FE)
{
throw new IllegalArgumentException("Unreadable format element at position " + start);
}
}
//$FALL-THROUGH$
stripCustom.append(c[pos.getIndex()]);
next(pos);
}
super.applyPattern(stripCustom.toString());
toPattern = insertFormats(super.toPattern(), foundDescriptions);
if (containsElements(foundFormats))
{
Format[] origFormats = getFormats();
// only loop over what we know we have, as MessageFormat on Java 1.3
// seems to provide an extra format element:
Iterator it = foundFormats.iterator();
for (int i = 0; it.hasNext(); i++)
{
Format f = it.next();
if (f != null)
{
origFormats[i] = f;
}
}
super.setFormats(origFormats);
}
}
@Override
public boolean equals(Object obj)
{
if (this == obj)
return true;
if (!super.equals(obj))
return false;
if (getClass() != obj.getClass())
return false;
ExtendedMessageFormat other = (ExtendedMessageFormat) obj;
if (registry == null)
{
if (other.registry != null)
return false;
} else if (!registry.equals(other.registry))
return false;
if (toPattern == null)
{
if (other.toPattern != null)
return false;
} else if (!toPattern.equals(other.toPattern))
return false;
return true;
}
/**
* Return the hashcode.
*
* @return the hashcode
*/
@Override
public int hashCode()
{
final int prime = 31;
int result = super.hashCode();
result = prime * result + ((registry == null) ? 0 : registry.hashCode());
result = prime * result + ((toPattern == null) ? 0 : toPattern.hashCode());
return result;
}
/**
* {@inheritDoc}
*/
@Override
public void setFormat(int formatElementIndex, Format newFormat)
{
super.setFormat(formatElementIndex, newFormat);
}
/**
* {@inheritDoc}
*/
@Override
public void setFormatByArgumentIndex(int argumentIndex, Format newFormat)
{
super.setFormatByArgumentIndex(argumentIndex, newFormat);
}
/**
* {@inheritDoc}
*/
@Override
public void setFormats(Format[] newFormats)
{
super.setFormats(newFormats);
}
/**
* {@inheritDoc}
*/
@Override
public void setFormatsByArgumentIndex(Format[] newFormats)
{
super.setFormatsByArgumentIndex(newFormats);
}
/**
* {@inheritDoc}
*/
@Override
public String toPattern()
{
return toPattern;
}
/**
* Consume a quoted string, adding it to appendTo
if specified.
*
* @param pattern
* pattern to parse
* @param pos
* current parse position
* @param appendTo
* optional StringBuffer to append
* @param escapingOn
* whether to process escaped quotes
* @return appendTo
*/
private StringBuilder appendQuotedString(String pattern, ParsePosition pos, StringBuilder appendTo,
boolean escapingOn)
{
int start = pos.getIndex();
char[] c = pattern.toCharArray();
if (escapingOn && c[start] == QUOTE)
{
next(pos);
return appendTo == null ? null : appendTo.append(QUOTE);
}
int lastHold = start;
for (int i = pos.getIndex(); i < pattern.length(); i++)
{
if (escapingOn && pattern.substring(i).startsWith(ESCAPED_QUOTE))
{
appendTo.append(c, lastHold, pos.getIndex() - lastHold).append(QUOTE);
pos.setIndex(i + ESCAPED_QUOTE.length());
lastHold = pos.getIndex();
continue;
}
switch (c[pos.getIndex()])
{
case QUOTE:
next(pos);
return appendTo == null ? null : appendTo.append(c, lastHold, pos.getIndex() - lastHold);
default:
next(pos);
}
}
throw new IllegalArgumentException("Unterminated quoted string at position " + start);
}
/**
* Learn whether the specified Collection contains non-null elements.
*
* @param coll
* to check
* @return true
if some Object was found, false
* otherwise.
*/
private boolean containsElements(Collection> coll)
{
return coll != null && !coll.isEmpty();
// if (coll == null || coll.size() == 0) {
// return false;
// }
// for (Object name : coll) {
// if (name != null) {
// return true;
// }
// }
// return false;
}
/**
* Get a custom format from a format description.
*
* @param desc
* String
* @return Format
*/
private Format getFormat(String desc)
{
if (registry != null)
{
String name = desc;
String args = null;
int i = desc.indexOf(START_FMT);
if (i > 0)
{
name = desc.substring(0, i).trim();
args = desc.substring(i + 1).trim();
}
FormatFactory factory = registry.get(name);
if (factory != null)
{
return factory.getFormat(name, args, getLocale());
}
}
return null;
}
/**
* Consume quoted string only
*
* @param pattern
* pattern to parse
* @param pos
* current parse position
* @param escapingOn
* whether to process escaped quotes
*/
private void getQuotedString(String pattern, ParsePosition pos, boolean escapingOn)
{
appendQuotedString(pattern, pos, null, escapingOn);
}
private boolean hasRegistry()
{
return registry == null || registry.isEmpty();
}
/**
* Insert formats back into the pattern for toPattern() support.
*
* @param pattern
* source
* @param customPatterns
* The custom patterns to re-insert, if any
* @return full pattern
*/
private String insertFormats(String pattern, ArrayList customPatterns)
{
if (!containsElements(customPatterns))
{
return pattern;
}
StringBuilder sb = new StringBuilder(pattern.length() * 2);
ParsePosition pos = new ParsePosition(0);
int fe = -1;
int depth = 0;
do
{
char c = pattern.charAt(pos.getIndex());
if (QUOTE == c)
{
appendQuotedString(pattern, pos, sb, false);
continue;
}
if (START_FE == c)
{
depth++;
if (depth == 1)
{
fe++;
sb.append(START_FE).append(readArgumentIndex(pattern, next(pos)));
String customPattern = customPatterns.get(fe);
if (customPattern != null)
{
sb.append(START_FMT).append(customPattern);
}
}
continue;
}
if (END_FE == c)
{
depth--;
}
//$FALL-THROUGH$
sb.append(c);
next(pos);
} while (pos.getIndex() < pattern.length());
return sb.toString();
}
/**
* Convenience method to advance parse position by 1
*
* @param pos
* ParsePosition
* @return pos
*/
private ParsePosition next(ParsePosition pos)
{
pos.setIndex(pos.getIndex() + 1);
return pos;
}
/**
* Parse the format component of a format element.
*
* @param pattern
* string to parse
* @param pos
* current parse position
* @return Format description String
*/
private String parseFormatDescription(String pattern, ParsePosition pos)
{
int start = pos.getIndex();
seekNonWs(pattern, pos);
int text = pos.getIndex();
int depth = 1;
for (; pos.getIndex() < pattern.length(); next(pos))
{
char charAt = pattern.charAt(pos.getIndex());
if (START_FE == charAt)
{
depth++;
continue;
}
if (END_FE == charAt)
{
depth--;
if (depth == 0)
{
return pattern.substring(text, pos.getIndex());
}
continue;
}
if (QUOTE == charAt)
{
getQuotedString(pattern, pos, false);
}
}
throw new IllegalArgumentException("Unterminated format element at position " + start);
}
/**
* Read the argument index from the current format element
*
* @param pattern
* pattern to parse
* @param pos
* current parse position
* @return argument index
*/
private int readArgumentIndex(String pattern, ParsePosition pos)
{
int start = pos.getIndex();
seekNonWs(pattern, pos);
StringBuffer result = new StringBuffer();
boolean error = false;
for (; !error && pos.getIndex() < pattern.length(); next(pos))
{
char c = pattern.charAt(pos.getIndex());
if (Character.isWhitespace(c))
{
seekNonWs(pattern, pos);
c = pattern.charAt(pos.getIndex());
if (c != START_FMT && c != END_FE)
{
error = true;
continue;
}
}
if ((c == START_FMT || c == END_FE) && result.length() > 0)
{
try
{
return Integer.parseInt(result.toString());
} catch (NumberFormatException e)
{ // NOPMD
// we've already ensured only digits, so unless something
// outlandishly large was specified we should be okay.
}
}
error = !Character.isDigit(c);
result.append(c);
}
if (error)
{
throw new IllegalArgumentException("Invalid format argument index at position " + start + ": "
+ pattern.substring(start, pos.getIndex()));
}
throw new IllegalArgumentException("Unterminated format element at position " + start);
}
/**
* Consume whitespace from the current parse position.
*
* @param pattern
* String to read
* @param pos
* current position
*/
private void seekNonWs(String pattern, ParsePosition pos)
{
int len = 0;
char[] buffer = pattern.toCharArray();
do
{
len = Arrays.binarySearch(SPLIT_CHARS, buffer[pos.getIndex()]) >= 0 ? 1 : 0;
pos.setIndex(pos.getIndex() + len);
} while (len > 0 && pos.getIndex() < pattern.length());
}
}