/*
* This program is free software; you can redistribute it and/or modify it under the
* terms of the GNU Lesser General Public License, version 2.1 as published by the Free Software
* Foundation.
*
* You should have received a copy of the GNU Lesser General Public License along with this
* program; if not, you can obtain a copy at http://www.gnu.org/licenses/old-licenses/lgpl-2.1.html
* or from the Free Software Foundation, Inc.,
* 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
*
* This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY;
* without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
* See the GNU Lesser General Public License for more details.
*
* Copyright (c) 2008 - 2009 Pentaho Corporation and Contributors. All rights reserved.
*/
package org.pentaho.reporting.libraries.formatting;
import org.pentaho.reporting.libraries.base.util.ArgumentNullException;
import java.text.DateFormat;
import java.util.ArrayList;
import java.util.Locale;
import java.util.TimeZone;
/**
* A wrapper around the java.text.MessageFormat class. This wrapper limits the possible interactions with the wrapped
* format class and therefore eliminates the need to clone the choice format whenever the wrapper is cloned.
* <p/>
* The pattern accepted by the this class is the same as the message-format pattern, with the exception that this class
* allows to escape the special characters using the backslash-character. Unlike the original MessageFormat class, this
* class allows to set a null-string for parameters that are null.
*
* @author Thomas Morgner
*/
public class FastMessageFormat implements FastFormat {
private String pattern;
private Locale locale;
private FastFormat[] subFormats;
private String[] constantTexts;
private int[] argumentMapping;
private int sizeHint;
private String nullString;
private transient StringBuffer buffer;
private DummyFieldPosition fieldPosition;
private TimeZone timeZone;
/**
* Creates a new default message format object for the given pattern using the default locale as locale.
*
* @param pattern the pattern.
*/
public FastMessageFormat( final String pattern ) {
this( pattern, Locale.getDefault() );
}
/**
* Creates a new default message format object for the given pattern and locale.
*
* @param pattern the pattern.
* @param locale the locale.
*/
public FastMessageFormat( final String pattern, final Locale locale ) {
this( pattern, locale, TimeZone.getDefault() );
}
public FastMessageFormat( final String pattern, final Locale locale, final TimeZone timeZone ) {
ArgumentNullException.validate( "timeZone", timeZone );
ArgumentNullException.validate( "pattern", pattern );
ArgumentNullException.validate( "locale", locale );
this.pattern = pattern;
this.locale = locale;
this.nullString = "<null>";
this.timeZone = timeZone;
final String[] arguments = new String[ 3 ];
int argumentIndex = 0;
int stackDepth = 0;
boolean escape = false;
final ArrayList<String> constants = new ArrayList<String>();
final ArrayList<FastFormat> patterns = new ArrayList<FastFormat>();
final ArrayList<Integer> indexMappings = new ArrayList<Integer>();
final StringBuilder b = new StringBuilder( pattern.length() );
final char[] chars = this.pattern.toCharArray();
for ( int i = 0; i < chars.length; i++ ) {
final char c = chars[ i ];
if ( escape == true ) {
b.append( c );
escape = false;
continue;
}
switch( c ) {
case '{': {
if ( stackDepth == 0 ) {
argumentIndex = 0;
arguments[ 0 ] = null;
arguments[ 1 ] = null;
arguments[ 2 ] = null;
constants.add( b.toString() );
this.sizeHint += b.length();
b.delete( 0, b.length() );
} else {
b.append( '{' );
}
stackDepth += 1;
break;
}
case '}': {
stackDepth -= 1;
if ( stackDepth < 0 ) {
throw new IllegalArgumentException( "Invalid pattern; curly braces do not match at position: " + i );
}
if ( stackDepth == 0 ) {
arguments[ argumentIndex ] = b.toString();
b.delete( 0, b.length() );
final String argIndexString = arguments[ 0 ];
if ( argIndexString == null ) {
throw new IllegalArgumentException( "Invalid pattern; no argument index for pattern ending at: " + i );
}
try {
indexMappings.add( new Integer( argIndexString ) );
final String argTypeRaw = arguments[ 1 ];
final String argPattern = arguments[ 2 ];
patterns.add( createFormatter( locale, argTypeRaw, argPattern ) );
} catch ( NumberFormatException nfe ) {
throw new IllegalArgumentException( "Invalid pattern; argument index is no number: " + i );
}
continue;
} else {
b.append( '}' );
}
break;
}
case ',': {
if ( stackDepth == 1 && argumentIndex < 2 ) {
// separator ..
arguments[ argumentIndex ] = b.toString();
b.delete( 0, b.length() );
argumentIndex += 1;
} else {
b.append( c );
}
break;
}
case '\\': {
escape = true;
break;
}
default: {
b.append( c );
}
}
}
this.sizeHint += b.length();
constants.add( b.toString() );
if ( stackDepth != 0 ) {
throw new IllegalArgumentException( "Invalid pattern; curly braces do not match" );
}
this.constantTexts = constants.toArray( new String[ constants.size() ] );
this.argumentMapping = new int[ indexMappings.size() ];
for ( int i = 0; i < indexMappings.size(); i++ ) {
final Integer integer = indexMappings.get( i );
argumentMapping[ i ] = integer.intValue();
}
this.subFormats = patterns.toArray( new FastFormat[ patterns.size() ] );
this.sizeHint += argumentMapping.length * 5;
}
/**
* Creates a sub-formatter for the given raw-type and raw-pattern. The formatter will be initialized with the locale
* given.
*
* @param locale the locale for the new sub-formatter.
* @param argTypeRaw the type, one of "time", "date", "datetime", "number" or "choice".
* @param argPattern the type-specific raw pattern.
* @return the creates format or null, if the raw format did not match anything valid.
*/
private FastFormat createFormatter( final Locale locale, final String argTypeRaw, final String argPattern ) {
if ( argTypeRaw == null ) {
return null;
}
final String trimmedType = argTypeRaw.trim();
if ( "time".equals( trimmedType ) ) {
if ( "short".equals( argPattern ) ) {
return new FastDateFormat( 0, DateFormat.SHORT, locale, timeZone );
} else if ( "medium".equals( argPattern ) ) {
return new FastDateFormat( 0, DateFormat.MEDIUM, locale, timeZone );
} else if ( "long".equals( argPattern ) ) {
return new FastDateFormat( 0, DateFormat.LONG, locale, timeZone );
} else if ( "full".equals( argPattern ) ) {
return new FastDateFormat( 0, DateFormat.FULL, locale, timeZone );
} else {
if ( argPattern == null ) {
return new FastDateFormat( 0, DateFormat.MEDIUM, locale, timeZone );
} else {
return new FastDateFormat( argPattern, locale, timeZone );
}
}
} else if ( "date".equals( trimmedType ) ) {
if ( "short".equals( argPattern ) ) {
return new FastDateFormat( DateFormat.SHORT, 0, locale, timeZone );
} else if ( "medium".equals( argPattern ) ) {
return new FastDateFormat( DateFormat.MEDIUM, 0, locale, timeZone );
} else if ( "long".equals( argPattern ) ) {
return new FastDateFormat( DateFormat.LONG, 0, locale, timeZone );
} else if ( "full".equals( argPattern ) ) {
return new FastDateFormat( DateFormat.FULL, 0, locale, timeZone );
} else {
if ( argPattern == null ) {
return new FastDateFormat( DateFormat.MEDIUM, 0, locale, timeZone );
} else {
return new FastDateFormat( argPattern, locale, timeZone );
}
}
} else if ( "datetime".equals( trimmedType ) ) {
if ( "short".equals( argPattern ) ) {
return new FastDateFormat( DateFormat.SHORT, DateFormat.SHORT, locale, timeZone );
} else if ( "medium".equals( argPattern ) ) {
return new FastDateFormat( DateFormat.MEDIUM, DateFormat.MEDIUM, locale, timeZone );
} else if ( "long".equals( argPattern ) ) {
return new FastDateFormat( DateFormat.LONG, DateFormat.LONG, locale, timeZone );
} else if ( "full".equals( argPattern ) ) {
return new FastDateFormat( DateFormat.FULL, DateFormat.FULL, locale, timeZone );
} else {
if ( argPattern == null ) {
return new FastDateFormat( 0, DateFormat.MEDIUM, locale, timeZone );
} else {
return new FastDateFormat( argPattern, locale, timeZone );
}
}
} else if ( "number".equals( trimmedType ) ) {
if ( "currency".equals( argPattern ) ) {
return new FastDecimalFormat( FastDecimalFormat.TYPE_CURRENCY, locale );
} else if ( "percent".equals( argPattern ) ) {
return new FastDecimalFormat( FastDecimalFormat.TYPE_PERCENT, locale );
} else if ( "integer".equals( argPattern ) ) {
return new FastDecimalFormat( FastDecimalFormat.TYPE_INTEGER, locale );
} else {
if ( argPattern == null ) {
return new FastDecimalFormat( FastDecimalFormat.TYPE_DEFAULT, locale );
} else {
return new FastDecimalFormat( argPattern, locale );
}
}
} else if ( "choice".equals( trimmedType ) ) {
return new FastChoiceFormat( argPattern, locale );
} else {
return null;
}
}
/**
* Returns the number of subformats in the message-formatter.
*
* @return the number of subformats.
*/
public int getSubFormatCount() {
return subFormats.length;
}
/**
* Returns the subformat at the given index.
*
* @param index the index.
* @return a clone of the fast-format or null, if there is no formatter at that position.
*/
protected FastFormat getSubFormat( final int index ) {
final FastFormat fastFormat = subFormats[ index ];
if ( fastFormat != null ) {
try {
return (FastFormat) fastFormat.clone();
} catch ( CloneNotSupportedException e ) {
throw new IllegalStateException();
}
}
return null;
}
/**
* Returns the current locale of the formatter.
*
* @return the current locale, never null.
*/
public Locale getLocale() {
return locale;
}
/**
* Returns the current time-zone for date formats.
*
* @return the current time zone, never null.
*/
public TimeZone getTimeZone() {
return timeZone;
}
/**
* Returns the currently active pattern.
*
* @return the locale.
*/
public String getPattern() {
return pattern;
}
/**
* Returns the subformats of this message-format.
*
* @return the subformats as deeply cloned array.
*/
protected FastFormat[] getSubFormats() {
try {
final FastFormat[] retval = new FastFormat[ subFormats.length ];
for ( int i = 0; i < subFormats.length; i++ ) {
final FastFormat fastFormat = subFormats[ i ];
if ( fastFormat != null ) {
retval[ i ] = (FastFormat) fastFormat.clone();
}
}
return retval;
} catch ( CloneNotSupportedException e ) {
throw new IllegalStateException( "Should not happen" );
}
}
/**
* Redefines the subformats of this message-format.
*
* @param subFormats the subformats.
*/
protected void setSubFormats( final FastFormat[] subFormats ) {
if ( subFormats == null ) {
throw new NullPointerException();
}
if ( subFormats.length != this.subFormats.length ) {
throw new IllegalArgumentException();
}
try {
for ( int i = 0; i < subFormats.length; i++ ) {
final FastFormat fastFormat = subFormats[ i ];
if ( fastFormat != null ) {
this.subFormats[ i ] = (FastFormat) fastFormat.clone();
}
}
} catch ( CloneNotSupportedException e ) {
throw new IllegalStateException( "Should not happen" );
}
}
/**
* Returns the null-string that is used whenever a parameter object is null.
*
* @return the nullstring.
*/
public String getNullString() {
return nullString;
}
/**
* Defines the null-string that is used whenever a parameter object is null.
*
* @param nullString the nullstring, never null in itself.
*/
public void setNullString( final String nullString ) {
if ( nullString == null ) {
throw new NullPointerException();
}
this.nullString = nullString;
}
/**
* Formats the given object in a formatter-specific way.
*
* @param parameters the parameters for the formatting.
* @return the formatted string.
*/
public String format( final Object parameters ) {
if ( parameters instanceof Object[] == false ) {
throw new IllegalArgumentException();
}
final Object[] parameterArray = (Object[]) parameters;
if ( subFormats.length == 0 ) {
return constantTexts[ 0 ];
}
if ( buffer == null ) {
buffer = new StringBuffer( sizeHint );
} else {
buffer.delete( 0, buffer.length() );
}
for ( int i = 0; i < subFormats.length; i++ ) {
final FastFormat format = subFormats[ i ];
buffer.append( constantTexts[ i ] );
final Object value = parameterArray[ argumentMapping[ i ] ];
if ( value == null ) {
buffer.append( nullString );
} else if ( format == null ) {
buffer.append( String.valueOf( value ) );
} else if ( format instanceof FastChoiceFormat ) {
final String formatStr = format.format( value );
final FastMessageFormat fastMessageFormat = new FastMessageFormat( formatStr, locale );
buffer.append( fastMessageFormat.format( parameters ) );
} else {
buffer.append( format.format( value ) );
}
}
buffer.append( constantTexts[ subFormats.length ] );
if ( buffer.length() > sizeHint ) {
this.sizeHint = buffer.length();
}
return buffer.toString();
}
/**
* Clones the formatter.
*
* @return the clone.
* @throws CloneNotSupportedException if cloning failed.
*/
public Object clone() throws CloneNotSupportedException {
return super.clone();
}
}