at.spardat.enterprise.fmt.FmtFactory Maven / Gradle / Ivy
/*******************************************************************************
* Copyright (c) 2003, 2007 s IT Solutions AT Spardat GmbH .
* All rights reserved. This program and the accompanying materials
* are made available under the terms of the Eclipse Public License v1.0
* which accompanies this distribution, and is available at
* http://www.eclipse.org/legal/epl-v10.html
*
* Contributors:
* s IT Solutions AT Spardat GmbH - initial API and implementation
*******************************************************************************/
//@(#) $Id: FmtFactory.java 3120 2009-02-11 10:44:52Z gub $
package at.spardat.enterprise.fmt;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.Locale;
import at.spardat.enterprise.exc.SysException;
import at.spardat.enterprise.util.StringSplitter;
import at.spardat.enterprise.util.StringUtil;
/**
* This class is a factory that allows to construct all the formatters/validators provided
* in this package by using a String-specification.
*
* The specification starts with a component indicating the type, then follows a list
* of additional parameters needed for the particular type and usually ends with a
* combination of style-constants. All components are separated by comma. Example:
*
* n,13,2,m&t
*
* denotes a numeric formatter (n) that accepts at most 13 digits in front of the comma,
* at most 2 digits after the comma and has two styles m and t, expressing
* that mandatory (m) input is required and (t) thousands-separation characters are used. Here
* are some more examples
*
*
* format meaning
* s,35 String which must not be longer than 35 digits.
* s,35,m As above, but mandatory, i.e., must not be empty.
* sm,5,5,m Exactly 5 characters.
* sr,0,35,a-c String of length not more than 35 consisting only of characters { 'a', 'b', 'c' }.
* n,13,2,m&t Numeric with at most 13 digits before and 2 after the dec-point, example:
* -1.000.000,99 in the austrian locale
* n,-1,2,nn&m 10000000000000,99 in the austrian locale. Negative inputs are not allowed.
* May consist of arbitrary long sequence of digits in front of dec-point.
* nr,-1,0,5,596 Integer number between 5 and 596.
* nr,13,0,inf,596 Integer number between -9999999999999 and 596.
* d Date using the format 24.04.2000 (this is Locale-dependent).
* d,m As above, but input is required.
* d,m&ful Mandatory date using the format "Montag, 24. April 2000", although this is locale-dependent.
* dr,0,7,m Mandatory date that must lie in the range [today ... today plus 7 days].
* dr,20000101,20101231 Date that must lie in the range [1.1.2000 ... 31.12.2010].
* dp,dd-MM-yyyy,m Mandatory, non-localized date format, e.g., "24-04-2000".
* b Boolean format, e.g. "yes".
*
*
* Here is the more formal definitions of the format specifications:
*
*
* s,maxLen(,style)
* String with arbitrary characters of max length maxLen. -1 for maxLen
* means unrestricted maximum length. With an optional third component, a style { m,uc,lc}
* may be specified.
* create may be used without a Locale.
*
* sm,minLen,maxLen(,style)
* Like the last, with the exception that an accepted string must have at least
* a length of minLen. The empty string is accepted unless style m
* is specified.
*
* sr,minLen,maxLen,range(,style)
* String of maximum length maxLen and minimum len minLen. Both minLen
* and maxLen may be empty of inf to denote "unrestrictedness".
* The character set is restricted to
* a range specification range, e.g., a-z denotes for example all unicode characters
* greater equal to a and less equal to z. For a detailed description see
* class {@link at.spardat.enterprise.fmt.AStringFmtRange}.
* With the last optional parameter style, the styles {m,uc,lc} may be specified.
* create may be used without a Locale.
*
* sre,(minLen),(maxLen),(range),(m&ul&lc),bundleKey,resBundle,regex
* The values in brackets can be empty strings, all commas are mandatory.
* String of maximum length maxLen and minimum len minLen. Both minLen
* and maxLen may be empty of inf to denote "unrestrictedness".
* The character set is restricted to
* a range specification by the optional string range, e.g., a-z denotes for example all unicode characters
* greater equal to a and less equal to z. For a detailed description see
* class {@link at.spardat.enterprise.fmt.AStringFmtRange}.
* The optional parameter style, the styles {m,uc,lc} may be specified.
* create may be used without a Locale.
* For the error message shown in the case of a negative evaluation of the regular expression
* a bundleKey identifying the error message and a resBundle (without locale)
* from which the error message is taken have to be specified.
* regex states the regular expression which validates the input.
* The regular expression interpretation/validation is done by the java.util.regex classes,
* see there for the proper regular expression usage.
*
* n,beforeC,afterC(,style)
* Numeric value with at most beforeC digits before and afterC digits
* after the decimal point. Both may be -1 or empty, indicating unlimited length.
* An optional last parameter style that may adopt values out of { m, t, nn }
* may be specified.
* create requires a Locale.
* Example: "n,13,2,nn&t" specifies a numeric value with 13 digits before and 2
* after the comma. It is displayed with thousands separation chars
* and does not accept negative values when entered.
*
* nr,beforeC,afterC,min,max(,style)
* Behaves like the last one, additionally lower and upper bounds concerning the legal
* input values, that is the parameters min and max are specified. Both,
* min and max expect a string encoding of a double as is accepted by
* the method Double.parseDouble. If you want to let one of either min
* or max unrestricted, specify inf which denotes infinity.
* FmtFactory.create requires a Locale.
*
* np,pattern,beforeC,afterC(,style)
* Specifies a numeric format by using a java.text.DecimalFormat-pattern.
* The style parameter may adopt values out of { m, nn }. Please note that
* the number of decimal places in the pattern must match afterC. Also note
* that java.text.DecimalFormat internally uses doubles and therefore
* the maximum number of digits that may be represented without loss of precision
* is 15.
* FmtFactory.create requires a Locale.
*
* d(,style)
* Specifies a date format. If a style is specified, it must be one of { sho, med, lon, ful }
* optionally combined with { m }. If no style is specified, med is default.
* FmtFactory.create requires a Locale.
*
* dr,min,max(,style)
* Same as the last one, with the extension that lower- and/or upper-bounds for
* permissible dates are specified. Both min and max can be an absolute
* dates in the format yyyyMMdd
or day-offsets relative to today,
* i.e., -1 stands for yesterday, 0 for today, 1 for tomorrow.
* inf stands for infinity and may be used if one of the two
* bounds is to be unspecified. The same is achieved if min or max
* is empty. Example:
* "dr,0,inf,m" or "dr,0,,m" specifies that a date must be entered (mandatory) and that is must
* not lie in the past (lower bound is today).
* "dr,,7" specifies that permissible dates are the ones in the past up to
* today plus one week.
* "dr,19000101,," specifies that the entered date must be 1.1.1900 or later.
* FmtFactory.create requires a Locale.
*
* dp,pattern(,style)
* Specifies a date format by using a java.text.SimpleDateFormat-pattern.
* For the last optional style-parameter, m (mandatory) may be used.
* FmtFactory.create may be used without Locale.
* Please note that this pattern does not work with internationalization since
* you are specifying the pattern directly. In international applications, use
* d(,style) instead.
*
* ts(,dateStyle(,timeStyle))
* Specifies a timestamp format. Styles may be specified for both the date-component
* and the time-component (or only the date-component) that are values out of { sho, med, lon, ful }.
* If input should be mandatory, m-style may be combined with either style.
* FmtFactory.create requires a Locale.
*
* tsp,pattern(,style)
* Specifies a timestamp format by using a java.util.SimpleDateFormat-pattern.
* style is optional and may be m.
* FmtFactory.create requires a Locale.
* Please note that this pattern does not work with internationalization since
* you are specifying the pattern yourself. In international applications, use
* ts(,...) instead.
*
* b(,style)
* Specifies a boolean format. If a style is specified, it must be one of { sho, lon }
* optionally combined with { m }. If no style is specified, lon is default.
* FmtFactory.create requires a Locale.
*
*
* The following list defines the style-strings supported:
*
*
* styles description
* m mandatory; non-empty input must be provided
* t used with numeric types and indicates that thousands separation characters
* should be used.
* nn used with numeric types and indicates that just positive numbers including
* zero may be entered (i.e., no negative numbers).
* sz suppressZero; used with numeric types to display zero as empty string and,
* vice versa, an empty UI-string becomes zero.
* sho denotes a short date/time format, e.g., 24.04.00
* med denotes medium data/time format, e.g., 24.04.2000
* lon denotes long date/time format, e.g., 24. April 2000
* ful denotes full date/time format, e.g., Montag, 24. April 2000
* uc uppercase; input is converted to uppercase; used with strings
* lc lowercase; input is converted to lowercase; used with strings
*
*
* Wherever style-components are requested, you may enter a combination of styles
* appending them with the &sign. For example, m&nn describes a number where
* input must be entered and the provided input must not be negative.
*
* Comma is used as delimiter to separate the components. What if one component itself
* needs to have a comma? Then you must use a percent-character as escape-character. For example,
* suppose you need a dp-spec where the SimpleDateFormat-pattern is
* EEE, dd.MM. Then the dp-spec must look like
*
* dp,EEE%, dd.MM
*
* If the component should contain a percent itself, you must use a double-percent instead.
*
* @author YSD
*/
public class FmtFactory {
private static HashMap styles_ = new HashMap();
private static FmtFactory theInstance_ = new FmtFactory();
static {
styles_.put ("m", new Integer (IFmt.MANDATORY));
styles_.put ("t", new Integer (ABcdFmt.THOUS_SEPS));
styles_.put ("nn", new Integer (ABcdFmt.NO_NEG));
styles_.put ("sz", new Integer (ABcdFmt.SUPPRESS_ZERO));
styles_.put ("sho", new Integer (ADateFmt.SHORT));
styles_.put ("med", new Integer (ADateFmt.MEDIUM));
styles_.put ("lon", new Integer (ADateFmt.LONG));
styles_.put ("ful", new Integer (ADateFmt.FULL));
styles_.put ("uc", new Integer (AStringFmt.UPPER_CASE));
styles_.put ("lc", new Integer (AStringFmt.LOWER_CASE));
}
/**
* Creates a formatter according to a provided specification that must follow the rules
* defined above. This method does not require a Locale, therefore should
* only be used with formats that are not localizable (see above). But if some particular
* format needs a Locale and this method is called, Locale.getDefault is used instead.
*
* @param spec the specification
* @return newly created formatter
*/
public static IFmt create (String spec) {
return create (spec, Locale.getDefault());
}
/**
* Creates a formatter according to a provided specification that must follow the rules
* defined above.
*
* @param spec the specification
* @param l the locale required for formatter-classes that are locale-dependent.
* @return newly created formatter
*/
public static IFmt create (String spec, Locale l) {
return theInstance_.createImpl (spec, l);
}
/**
* Enables an application to change the used factory-implementation. The provided
* object must be a subclass of this class.
*/
public static void setFactory (FmtFactory fac) {
if (fac != null) theInstance_ = fac;
}
/**
* This method actually does the work and is responsible to create an IFmt.
*
* @param spec specification
* @param l Locale
* @return formatter
* @exception RuntimeException on illegal inputs
*/
protected IFmt createImpl (String spec, Locale l) {
StringSplitter ss = new StringSplitter (spec, ',', '%');
String type = getToken (ss, "type");
int typeLen = type.length();
int style = 0;
if (typeLen == 0) throw new SysException ("empty type '" + type + "' in format spec '" + spec + "'");
char firstCh = type.charAt(0);
switch (firstCh) {
case 's':
if (typeLen == 1) {
// s,maxLen(,style)
int maxLen = getIntToken2 (ss, "maxLen");
if (ss.hasMoreTokens()) style = getStyle (ss);
return AStringFmt.getInstance (maxLen, style);
} else if (type.equals ("sm")) {
// s,minLen,maxLen(,style)
int minLen = getIntToken2 (ss, "minLen");
int maxLen = getIntToken2 (ss, "maxLen");
if (ss.hasMoreTokens()) style = getStyle (ss);
AStringFmt fmt = AStringFmt.getInstance (maxLen, style);
fmt.setMinLen(minLen);
return fmt;
} else if (type.equals("sr")) {
// sr,minLen,maxLen,range(,style)
int minLen = getIntToken2 (ss, "minLen");
int maxLen = getIntToken2 (ss, "maxLen");
String range = getToken (ss, "range");
if (ss.hasMoreTokens()) style = getStyle (ss);
AStringFmt fmt = AStringFmt.getInstance(maxLen, range, style);
fmt.setMinLen(minLen);
return fmt;
} else if (type.equals("sre")) {
// sre,(minLen),(maxLen),(range),(m&ul&lc),bundleKey,resBundle,regex
int minLen = getIntToken2 (ss, "minLen");
int maxLen = getIntToken2 (ss, "maxLen");
String range = getToken (ss, "range");
style = getStyle (ss);
String bundleKey = getToken (ss, "bundleKey");
String resBundle = getToken (ss, "resBundle");
String regex = getToken (ss, "regex");
AStringFmt fmt = AStringFmt.getInstance(maxLen, range, style,regex,bundleKey,resBundle);
fmt.setMinLen(minLen);
return fmt;
}
break;
case 'n':
if (typeLen == 1) {
// n,beforeC,afterC(,style)
int beforeC = getIntToken2 (ss, "beforeC");
int afterC = getIntToken2 (ss, "afterC");
if (ss.hasMoreTokens()) style = getStyle (ss);
return ABcdFmt.getInstance(beforeC, afterC, style, l);
} else if (type.equals("np")) {
// np,pattern(,style)
String pattern = getToken(ss, "pattern");
int beforeC = getIntToken2 (ss, "beforeC");
int afterC = getIntToken2 (ss, "afterC");
if (ss.hasMoreTokens()) style = getStyle (ss);
return ABcdFmt.getInstance (pattern, beforeC, afterC, style, l);
} else if (type.equals("nr")) {
// nr,beforeC,afterC,min,max(,style)
int beforeC = getIntToken2 (ss, "beforeC");
int afterC = getIntToken2 (ss, "afterC");
double minVal, maxVal;
minVal = getDoubleToken (ss, "min");
if (minVal == Double.POSITIVE_INFINITY) minVal = -Double.MAX_VALUE;
maxVal = getDoubleToken (ss, "max");
if (maxVal == Double.POSITIVE_INFINITY) maxVal = Double.MAX_VALUE;
if (ss.hasMoreTokens()) style = getStyle (ss);
return ABcdFmt.getInstance (beforeC, afterC, style, minVal, maxVal, l);
}
break;
case 'd':
if (typeLen == 1) {
// d(,style)
if (ss.hasMoreTokens()) style = getStyle (ss);
return ADateFmt.getInstance(style, l);
} else if (type.equals("dp")) {
// dp,pattern(,style)
String pattern = getToken(ss, "pattern");
if (ss.hasMoreTokens()) style = getStyle (ss);
return ADateFmt.getInstance(pattern, style,l);
// } else if (type.equals("dr")) {
// // dr,min,max(,style)
// int min = getIntToken (ss, "min");
// if (min == Integer.MIN_VALUE) min = Integer.MAX_VALUE;
// int max = getIntToken (ss, "max");
// if (max == Integer.MIN_VALUE) max = Integer.MAX_VALUE;
// if (ss.hasMoreTokens()) style = getStyle (ss);
// return ADateFmt.getInstance (style, l, min, max);
//
} else if (type.equals("dr")) {
// dr,min,max(,style)
int min = Integer.MAX_VALUE;
int max = Integer.MAX_VALUE;
String minDate=null;
String maxDate=null;
if(ss.peekNextToken()!=null&&ss.peekNextToken().length()==8) {
minDate = getToken(ss,"min");
} else {
min = getIntToken (ss, "min");
if (min == Integer.MIN_VALUE) min = Integer.MAX_VALUE;
}
if(ss.peekNextToken()!=null&&ss.peekNextToken().length()==8) {
maxDate = getToken (ss, "max");
} else {
max = getIntToken (ss, "max");
if (max == Integer.MIN_VALUE) max = Integer.MAX_VALUE;
}
if (ss.hasMoreTokens()) style = getStyle (ss);
return ADateFmt.getInstance (style, l, min, max, minDate, maxDate);
}
break;
case 't':
if (type.equals("ts")) {
// ts(,dateStyle(,timeStyle))
int dStyle = 0;
int tStyle = 0;
if (ss.hasMoreTokens()) dStyle = getStyle (ss);
if (ss.hasMoreTokens()) tStyle = getStyle (ss);
return ATimeStampFmt.getInstance(dStyle, tStyle, l);
} else if (type.equals("tsp")) {
// tsp,pattern(,style)
String pattern = getToken (ss, "pattern");
if (ss.hasMoreTokens()) style = getStyle (ss);
return ATimeStampFmt.getInstance(pattern, style, l);
}
break;
case 'b':
if (ss.hasMoreTokens()) style = getStyle (ss);
return ABooleanFmt.getInstance(style,l);
}
// fallthrough
throw new RuntimeException ("unknown type '" + type + "' in format spec '" + spec + "'");
}
/**
* Helper method to extract the next token or to throw an exception if there is no more token.
*/
protected String getToken (StringSplitter ss, String expected) {
if (!ss.hasMoreTokens()) throw new RuntimeException (expected + " expected in " + ss.getString());
return ss.nextToken();
}
/**
* Helper method that extracts the next token that must be an integer and throws an exception
* if there is no more token.
*
* @param ss the input stream where the next token has to be extracted
* @param expected text to be used in error messages
* @return an parsed integer; the integer is Integer.MAX_VALUE if the token is inf (infinity).
* the integer is Integer.MIN_VALUE if the token is empty.
*/
protected int getIntToken (StringSplitter ss, String expected) {
String tok = getToken (ss, expected);
if (tok.length() == 0) return Integer.MIN_VALUE;
if (tok.equals("inf")) return Integer.MAX_VALUE;
int toReturn = 0;
try {
toReturn = Integer.parseInt(tok);
} catch (Exception ex) {
throw new SysException (ex, expected + " expected in " + ss.getString() + " (must be a number)");
}
return toReturn;
}
/**
* Same as getIntToken, except that if the token is inf or empty,
* then -1 is returned.
*/
protected int getIntToken2 (StringSplitter ss, String expected) {
int rv = getIntToken (ss, expected);
if (rv == Integer.MAX_VALUE || rv == Integer.MAX_VALUE) return -1;
return rv;
}
/**
* Helper method that extracts the next token that must be a double as required by Double.parseDouble
* and throws an Exception if there are no more tokens.
*
* @param ss the input stream
* @param expected text to be used in error messages
* @return a parsed double; the returned value is Double.POSITIVE_INFINITY if the token is inf.
*/
protected double getDoubleToken (StringSplitter ss, String expected) {
String tok = getToken (ss, expected);
if (tok.equals("inf")) return Double.POSITIVE_INFINITY;
double toReturn = 0.0;
try {
toReturn = Double.parseDouble(tok);
} catch (Exception ex) {
throw new SysException (ex, expected + " expected in " + ss.getString() + " (must be a double as required by Double.parseDouble)");
}
return toReturn;
}
/**
* Extracts the next token from ss, expects a list
* of style-strings that are separated by ampersands. If
* there is no more token in ss, zero is returned.
*
* @param ss the input StringSplitter whose next token is optionally extracted.
* @return bit-or combination of styles.
*/
protected int getStyle (StringSplitter ss) {
if (!ss.hasMoreTokens()) return 0;
String styleString = ss.nextToken();
if (styleString.length() == 0) return 0;
if (styleString.indexOf('&') >= 0) {
// the hard part; consists of many atomic styles
ArrayList lst = StringUtil.split (styleString, "&");
int toReturn = 0;
for (int i=0,size=lst.size(); i