/*
* 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 org.apache.commons.lang3.text;
import java.text.Format;
import java.text.MessageFormat;
import java.text.ParsePosition;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Iterator;
import java.util.Locale;
import java.util.Map;
import java.util.Objects;
import org.apache.commons.lang3.ObjectUtils;
import org.apache.commons.lang3.Validate;
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.
NOTICE: The various subformat mutator methods are considered unnecessary; they exist on the parent
class to allow the type of customization which it is the job of this class to provide in
a configurable fashion. These methods have thus been disabled and will throw
UnsupportedOperationException
if called.
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.
Since: 2.4 Deprecated: as of 3.6, use commons-text
ExtendedMessageFormat instead
/**
* Extends <code>java.text.MessageFormat</code> to allow pluggable/additional formatting
* options for embedded format elements. Client code should specify a registry
* of <code>FormatFactory</code> instances associated with <code>String</code>
* 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 <code>java.text.MessageFormat</code> can be overridden
* at the format and/or format style level (see MessageFormat). A "format element"
* embedded in the message pattern is specified (<b>()?</b> signifies optionality):<br>
* <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>
*
* <p>
* <i>format-name</i> and <i>format-style</i> values are trimmed of surrounding whitespace
* in the manner of <code>java.text.MessageFormat</code>. If <i>format-name</i> denotes
* <code>FormatFactory formatFactoryInstance</code> in <code>registry</code>, a <code>Format</code>
* matching <i>format-name</i> and <i>format-style</i> is requested from
* <code>formatFactoryInstance</code>. If this is successful, the <code>Format</code>
* found is used for this format element.
* </p>
*
* <p><b>NOTICE:</b> The various subformat mutator methods are considered unnecessary; they exist on the parent
* class to allow the type of customization which it is the job of this class to provide in
* a configurable fashion. These methods have thus been disabled and will throw
* <code>UnsupportedOperationException</code> if called.
* </p>
*
* <p>Limitations inherited from <code>java.text.MessageFormat</code>:</p>
* <ul>
* <li>When using "choice" subformats, support for nested formatting instructions is limited
* to that provided by the base class.</li>
* <li>Thread-safety of <code>Format</code>s, including <code>MessageFormat</code> and thus
* <code>ExtendedMessageFormat</code>, is not guaranteed.</li>
* </ul>
*
* @since 2.4
* @deprecated as of 3.6, use commons-text
* <a href="https://commons.apache.org/proper/commons-text/javadocs/api-release/org/apache/commons/text/ExtendedMessageFormat.html">
* ExtendedMessageFormat</a> instead
*/
@Deprecated
public class ExtendedMessageFormat extends MessageFormat {
private static final long serialVersionUID = -2362048321261811743L;
private static final int HASH_SEED = 31;
private static final String DUMMY_PATTERN = "";
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<String, ? extends FormatFactory> registry;
Create a new ExtendedMessageFormat for the default locale.
Params: - pattern – the pattern to use, not null
Throws: - IllegalArgumentException – in case of a bad pattern.
/**
* 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(final String pattern) {
this(pattern, Locale.getDefault());
}
Create a new ExtendedMessageFormat.
Params: - pattern – the pattern to use, not null
- locale – the locale to use, not null
Throws: - IllegalArgumentException – in case of a bad pattern.
/**
* 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(final String pattern, final Locale locale) {
this(pattern, locale, null);
}
Create a new ExtendedMessageFormat for the default locale.
Params: - pattern – the pattern to use, not null
- registry – the registry of format factories, may be null
Throws: - IllegalArgumentException – in case of a bad 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(final String pattern, final Map<String, ? extends FormatFactory> registry) {
this(pattern, Locale.getDefault(), registry);
}
Create a new ExtendedMessageFormat.
Params: - pattern – the pattern to use, not null
- locale – the locale to use, not null
- registry – the registry of format factories, may be null
Throws: - IllegalArgumentException – in case of a bad pattern.
/**
* 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(final String pattern, final Locale locale, final Map<String, ? extends FormatFactory> registry) {
super(DUMMY_PATTERN);
setLocale(locale);
this.registry = registry;
applyPattern(pattern);
}
{@inheritDoc}
/**
* {@inheritDoc}
*/
@Override
public String toPattern() {
return toPattern;
}
Apply the specified pattern.
Params: - pattern – String
/**
* Apply the specified pattern.
*
* @param pattern String
*/
@Override
public final void applyPattern(final String pattern) {
if (registry == null) {
super.applyPattern(pattern);
toPattern = super.toPattern();
return;
}
final ArrayList<Format> foundFormats = new ArrayList<>();
final ArrayList<String> foundDescriptions = new ArrayList<>();
final StringBuilder stripCustom = new StringBuilder(pattern.length());
final ParsePosition pos = new ParsePosition(0);
final char[] c = pattern.toCharArray();
int fmtCount = 0;
while (pos.getIndex() < pattern.length()) {
switch (c[pos.getIndex()]) {
case QUOTE:
appendQuotedString(pattern, pos, stripCustom);
break;
case START_FE:
fmtCount++;
seekNonWs(pattern, pos);
final int start = pos.getIndex();
final 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);
Validate.isTrue(foundFormats.size() == fmtCount);
Validate.isTrue(foundDescriptions.size() == fmtCount);
if (c[pos.getIndex()] != END_FE) {
throw new IllegalArgumentException(
"Unreadable format element at position " + start);
}
//$FALL-THROUGH$
default:
stripCustom.append(c[pos.getIndex()]);
next(pos);
}
}
super.applyPattern(stripCustom.toString());
toPattern = insertFormats(super.toPattern(), foundDescriptions);
if (containsElements(foundFormats)) {
final Format[] origFormats = getFormats();
// only loop over what we know we have, as MessageFormat on Java 1.3
// seems to provide an extra format element:
int i = 0;
for (final Iterator<Format> it = foundFormats.iterator(); it.hasNext(); i++) {
final Format f = it.next();
if (f != null) {
origFormats[i] = f;
}
}
super.setFormats(origFormats);
}
}
Throws UnsupportedOperationException - see class Javadoc for details.
Params: - formatElementIndex – format element index
- newFormat – the new format
Throws: - UnsupportedOperationException – always thrown since this isn't supported by ExtendMessageFormat
/**
* Throws UnsupportedOperationException - see class Javadoc for details.
*
* @param formatElementIndex format element index
* @param newFormat the new format
* @throws UnsupportedOperationException always thrown since this isn't supported by ExtendMessageFormat
*/
@Override
public void setFormat(final int formatElementIndex, final Format newFormat) {
throw new UnsupportedOperationException();
}
Throws UnsupportedOperationException - see class Javadoc for details.
Params: - argumentIndex – argument index
- newFormat – the new format
Throws: - UnsupportedOperationException – always thrown since this isn't supported by ExtendMessageFormat
/**
* Throws UnsupportedOperationException - see class Javadoc for details.
*
* @param argumentIndex argument index
* @param newFormat the new format
* @throws UnsupportedOperationException always thrown since this isn't supported by ExtendMessageFormat
*/
@Override
public void setFormatByArgumentIndex(final int argumentIndex, final Format newFormat) {
throw new UnsupportedOperationException();
}
Throws UnsupportedOperationException - see class Javadoc for details.
Params: - newFormats – new formats
Throws: - UnsupportedOperationException – always thrown since this isn't supported by ExtendMessageFormat
/**
* Throws UnsupportedOperationException - see class Javadoc for details.
*
* @param newFormats new formats
* @throws UnsupportedOperationException always thrown since this isn't supported by ExtendMessageFormat
*/
@Override
public void setFormats(final Format[] newFormats) {
throw new UnsupportedOperationException();
}
Throws UnsupportedOperationException - see class Javadoc for details.
Params: - newFormats – new formats
Throws: - UnsupportedOperationException – always thrown since this isn't supported by ExtendMessageFormat
/**
* Throws UnsupportedOperationException - see class Javadoc for details.
*
* @param newFormats new formats
* @throws UnsupportedOperationException always thrown since this isn't supported by ExtendMessageFormat
*/
@Override
public void setFormatsByArgumentIndex(final Format[] newFormats) {
throw new UnsupportedOperationException();
}
Check if this extended message format is equal to another object.
Params: - obj – the object to compare to
Returns: true if this object equals the other, otherwise false
/**
* Check if this extended message format is equal to another object.
*
* @param obj the object to compare to
* @return true if this object equals the other, otherwise false
*/
@Override
public boolean equals(final Object obj) {
if (obj == this) {
return true;
}
if (obj == null) {
return false;
}
if (!super.equals(obj)) {
return false;
}
if (ObjectUtils.notEqual(getClass(), obj.getClass())) {
return false;
}
final ExtendedMessageFormat rhs = (ExtendedMessageFormat)obj;
if (ObjectUtils.notEqual(toPattern, rhs.toPattern)) {
return false;
}
return !ObjectUtils.notEqual(registry, rhs.registry);
}
{@inheritDoc}
/**
* {@inheritDoc}
*/
@Override
public int hashCode() {
int result = super.hashCode();
result = HASH_SEED * result + Objects.hashCode(registry);
result = HASH_SEED * result + Objects.hashCode(toPattern);
return result;
}
Get a custom format from a format description.
Params: - desc – String
Returns: Format
/**
* Get a custom format from a format description.
*
* @param desc String
* @return Format
*/
private Format getFormat(final String desc) {
if (registry != null) {
String name = desc;
String args = null;
final int i = desc.indexOf(START_FMT);
if (i > 0) {
name = desc.substring(0, i).trim();
args = desc.substring(i + 1).trim();
}
final FormatFactory factory = registry.get(name);
if (factory != null) {
return factory.getFormat(name, args, getLocale());
}
}
return null;
}
Read the argument index from the current format element
Params: - pattern – pattern to parse
- pos – current parse position
Returns: argument index
/**
* 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(final String pattern, final ParsePosition pos) {
final int start = pos.getIndex();
seekNonWs(pattern, pos);
final StringBuilder result = new StringBuilder();
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 (final 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);
}
Parse the format component of a format element.
Params: - pattern – string to parse
- pos – current parse position
Returns: Format description String
/**
* 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(final String pattern, final ParsePosition pos) {
final int start = pos.getIndex();
seekNonWs(pattern, pos);
final int text = pos.getIndex();
int depth = 1;
for (; pos.getIndex() < pattern.length(); next(pos)) {
switch (pattern.charAt(pos.getIndex())) {
case START_FE:
depth++;
break;
case END_FE:
depth--;
if (depth == 0) {
return pattern.substring(text, pos.getIndex());
}
break;
case QUOTE:
getQuotedString(pattern, pos);
break;
default:
break;
}
}
throw new IllegalArgumentException(
"Unterminated format element at position " + start);
}
Insert formats back into the pattern for toPattern() support.
Params: - pattern – source
- customPatterns – The custom patterns to re-insert, if any
Returns: full pattern
/**
* 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(final String pattern, final ArrayList<String> customPatterns) {
if (!containsElements(customPatterns)) {
return pattern;
}
final StringBuilder sb = new StringBuilder(pattern.length() * 2);
final ParsePosition pos = new ParsePosition(0);
int fe = -1;
int depth = 0;
while (pos.getIndex() < pattern.length()) {
final char c = pattern.charAt(pos.getIndex());
switch (c) {
case QUOTE:
appendQuotedString(pattern, pos, sb);
break;
case START_FE:
depth++;
sb.append(START_FE).append(readArgumentIndex(pattern, next(pos)));
// do not look for custom patterns when they are embedded, e.g. in a choice
if (depth == 1) {
fe++;
final String customPattern = customPatterns.get(fe);
if (customPattern != null) {
sb.append(START_FMT).append(customPattern);
}
}
break;
case END_FE:
depth--;
//$FALL-THROUGH$
default:
sb.append(c);
next(pos);
}
}
return sb.toString();
}
Consume whitespace from the current parse position.
Params: - pattern – String to read
- pos – current position
/**
* Consume whitespace from the current parse position.
*
* @param pattern String to read
* @param pos current position
*/
private void seekNonWs(final String pattern, final ParsePosition pos) {
int len = 0;
final char[] buffer = pattern.toCharArray();
do {
len = StrMatcher.splitMatcher().isMatch(buffer, pos.getIndex());
pos.setIndex(pos.getIndex() + len);
} while (len > 0 && pos.getIndex() < pattern.length());
}
Convenience method to advance parse position by 1
Params: - pos – ParsePosition
Returns: pos
/**
* Convenience method to advance parse position by 1
*
* @param pos ParsePosition
* @return <code>pos</code>
*/
private ParsePosition next(final ParsePosition pos) {
pos.setIndex(pos.getIndex() + 1);
return pos;
}
Consume a quoted string, adding it to appendTo
if
specified.
Params: - pattern – pattern to parse
- pos – current parse position
- appendTo – optional StringBuilder to append
Returns: appendTo
/**
* Consume a quoted string, adding it to <code>appendTo</code> if
* specified.
*
* @param pattern pattern to parse
* @param pos current parse position
* @param appendTo optional StringBuilder to append
* @return <code>appendTo</code>
*/
private StringBuilder appendQuotedString(final String pattern, final ParsePosition pos,
final StringBuilder appendTo) {
assert pattern.toCharArray()[pos.getIndex()] == QUOTE :
"Quoted string must start with quote character";
// handle quote character at the beginning of the string
if(appendTo != null) {
appendTo.append(QUOTE);
}
next(pos);
final int start = pos.getIndex();
final char[] c = pattern.toCharArray();
final int lastHold = start;
for (int i = pos.getIndex(); i < pattern.length(); i++) {
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);
}
Consume quoted string only
Params: - pattern – pattern to parse
- pos – current parse position
/**
* Consume quoted string only
*
* @param pattern pattern to parse
* @param pos current parse position
*/
private void getQuotedString(final String pattern, final ParsePosition pos) {
appendQuotedString(pattern, pos, null);
}
Learn whether the specified Collection contains non-null elements.
Params: - coll – to check
Returns: true
if some Object was found, false
otherwise.
/**
* Learn whether the specified Collection contains non-null elements.
* @param coll to check
* @return <code>true</code> if some Object was found, <code>false</code> otherwise.
*/
private boolean containsElements(final Collection<?> coll) {
if (coll == null || coll.isEmpty()) {
return false;
}
for (final Object name : coll) {
if (name != null) {
return true;
}
}
return false;
}
}