de.tsl2.nano.format.RegExpFormat Maven / Gradle / Ivy
Go to download
Show more of this group Show more artifacts with this name
Show all versions of tsl2.nano.descriptor Show documentation
Show all versions of tsl2.nano.descriptor Show documentation
TSL2 Framework Descriptor (currency-handling, generic formatter, descriptors for beans, collections, actions and values)
/*
*
*
* Copyright © 2002-2008 Thomas Schneider
* Alle Rechte vorbehalten.
* Weiterverbreitung, Benutzung, Vervielfältigung oder Offenlegung,
* auch auszugsweise, nur mit Genehmigung.
*
*/
package de.tsl2.nano.format;
import static de.tsl2.nano.core.util.FormatUtil.getCurrencyFormatNoFraction;
import static de.tsl2.nano.core.util.FormatUtil.getCurrencyFormatNoSymbol;
import java.io.IOException;
import java.io.Serializable;
import java.math.BigDecimal;
import java.sql.Time;
import java.sql.Timestamp;
import java.text.DateFormat;
import java.text.DecimalFormat;
import java.text.DecimalFormatSymbols;
import java.text.FieldPosition;
import java.text.Format;
import java.text.NumberFormat;
import java.text.ParseException;
import java.text.ParsePosition;
import java.util.Collection;
import java.util.Currency;
import java.util.Date;
import java.util.HashMap;
import java.util.Hashtable;
import java.util.Locale;
import java.util.Map;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import org.apache.commons.logging.Log;
import org.simpleframework.xml.Attribute;
import org.simpleframework.xml.Element;
import org.simpleframework.xml.ElementMap;
import org.simpleframework.xml.core.Commit;
import org.simpleframework.xml.core.Complete;
import org.simpleframework.xml.core.Persist;
import de.tsl2.nano.bean.BeanContainer;
import de.tsl2.nano.collection.ReferenceMap;
import de.tsl2.nano.core.ENV;
import de.tsl2.nano.core.ManagedException;
import de.tsl2.nano.core.cls.BeanAttribute;
import de.tsl2.nano.core.cls.BeanClass;
import de.tsl2.nano.core.log.LogFactory;
import de.tsl2.nano.core.util.DateUtil;
import de.tsl2.nano.core.util.FormatUtil;
import de.tsl2.nano.core.util.MapUtil;
import de.tsl2.nano.core.util.StringUtil;
import de.tsl2.nano.core.util.Util;
import de.tsl2.nano.currency.CurrencyUtil;
import de.tsl2.nano.util.operation.IConverter;
/**
* provides standard formatting through regular expressions. usable to verify input in a text component. the
* parseObject() isn't able to return the desired instance!
*
* if you only want to format or parse standard numbers or dates, then use the standard formatters like NumberFormat and
* DateFormat.
*
* This formatter is intended to be used to constrain or verify user input trough an input mask.
*
* four standard expressions are available:
*
* - standard text
* - numbers
* - dates
* - length
*
*
* @author ts 18.09.2008
* @version $Revision: 1.0 $
*/
@SuppressWarnings({ "unchecked", "rawtypes" })
public class RegExpFormat extends Format implements INumberFormatCheck {
private static final long serialVersionUID = 1L;
private static final Log LOG = LogFactory.getLog(RegExpFormat.class);
@Element(data = true)
private String pattern;
@Attribute
private int regExpFlags;
@Attribute
private int maxCharacterCount = -1;
private transient Pattern compiledPattern;
/** needed to format objects */
@Element(name = "parser", required = false)
protected GenericParser> parser = null;
/** default: the full regexp must be matched on format or parse! */
@Attribute
boolean fullMatch = true;
/** on default, this class is not able to parse a string to an object */
@Attribute
boolean isAbleToParse = false;
public static final char DECIMAL_SEPARATOR = new DecimalFormatSymbols(Locale.getDefault()).getDecimalSeparator();
public static final char GROUPING_SEPARATOR = new DecimalFormatSymbols(Locale.getDefault()).getGroupingSeparator();
/** characters only to format an expression (like german date '01.01.2001' the dots) */
public static final String FORMAT_CHARACTERS = " .,;_/*#|-:";
/** alphanumeric character with lowest ascii code (=32) */
public static final char MIN_CHAR = ' ';
/** alphanumeric character with highest ascii code (=126) */
public static final char MAX_CHAR = '~';
/**
* this pattern is not only a number format, but a pattern from nothing up to the desired number. E.g.: -999,999 -->
* you start with '-', than you type the numbers!
*/
public static final String FORMAT_DECIMAL = "[-][0-9]{0,2}[" + DECIMAL_SEPARATOR + "][0-9]{0,2}";
/**
* All single byte characters, for applications / RDBMS that do not support multibyte/unicode characters.
* Note that the Posix pattern matcher "\\p{ASCII}" only covers characters up to \x7F, so for example no German
* "Umlaute".
*/
public static final String PATTERN_SINGLE_BYTE = "[\\x00-\\xFF]";
public static final String PATTERN_SINGLE_BYTE_SPACE = "[\\x00-\\xFF €]";
// TODO how to get the date format from locale?
public static final String FORMAT_DATE_SQL = "[1-2]\\d\\d\\d\\-[0-1]\\d\\-[0-3]\\d";
public static final String FORMAT_DATE_DE = "[0-3]\\d\\.[0-1]\\d(\\.[1-2]\\d\\d\\d)?";
public static final String FORMAT_TIME = "[0-2]\\d\\:[0-5]\\d(\\:[0-5]\\d)?";
public static final String FORMAT_DATETIME = FORMAT_DATE_SQL + "( " + FORMAT_TIME + ")?";
public static final String FORMAT_DATETIME_DE = FORMAT_DATE_DE + "( " + FORMAT_TIME + ")?";
public static final String FORMAT_NAME_ALPHA_DE = "[a-zA-ZäöüÄÖÜß]+";
public static final String FORMAT_NAME_ALPHA_EXT_DE = FORMAT_NAME_ALPHA_DE + PATTERN_SINGLE_BYTE_SPACE;
public static final String FORMAT_NAME_ALPHA = "[a-zA-Z]*";
public static final String FORMAT_NAME_ALPHA_NUM = "[a-zA-Z0-9]*";
public static final String FORMAT_NUMBER = "[0-9]*";
private static final Map systemInitMap = new Hashtable();
@ElementMap(attribute = true, inline = true, required = false, name = "initmask", key = "pattern")
Map initMap = null;
static {
try {
/*
* standard german date format mask
*/
systemInitMap.put(FORMAT_DATE_DE, DateFormat.getDateInstance(DateFormat.MEDIUM).format(getInitialDate()));
/*
* on german dates with month-day 31, the standard-initmask '01.01.CURRENTYEAR' does not work.
* example: input: 31.1 and mask: 01.01.2011 ==> 31.11.2011 ==> error
*/
systemInitMap.put("31.1", DateFormat.getDateInstance(DateFormat.MEDIUM).format(getInitialDate_Nov()));
/*
* standard decimal number
*/
systemInitMap.put(FORMAT_DECIMAL, "00" + DECIMAL_SEPARATOR + "00");
/*
* standard time
*/
systemInitMap.put(FORMAT_TIME, "00:00:00");
} catch (final Exception e) {
throw new RuntimeException(e);
}
}
static Date getInitialDate() {
// return new Date();
/*
* current date may be a problem on months, having less than 31 days.
* we give 01.01.CURRENT_YEAR
*/
return DateUtil.getDate(DateUtil.getYear(new Date()), 1, 1);
}
static Date getInitialDate_Nov() {
// return new Date();
/*
* current date may be a problem on months, having less than 31 days.
* we give 01.10.CURRENT_YEAR
*/
return DateUtil.getDate(DateUtil.getYear(new Date()), 10, 1);
}
/**
* constructor to be de-serializable
*/
public RegExpFormat() {
super();
}
/**
* simple constructor without length argument. only usable on simple patterns without length definitions (like
* {0,5}).
*
* @param pattern regexp without length def.
*/
protected RegExpFormat(String pattern) {
this(pattern, null, calcLength(pattern, null), 0);
}
/**
* simple constructor without length argument. only usable on simple patterns without length definitions (like
* {0,5}).
*
* @param pattern regexp without length def.
* @param init init string
*/
public RegExpFormat(String pattern, String init) {
this(pattern, init, calcLength(pattern, init), 0);
}
/**
* Constructor
*
* @param pattern regular expression to check
* @param maxCharacterCount maximum allowed count of characters
* @param regExpFlags additional Flags for the regular expression
* @see Pattern Pattern for Flags description
*/
public RegExpFormat(String pattern, int maxCharacterCount, int regExpFlags) {
this(pattern, null, maxCharacterCount, regExpFlags);
}
/**
* Constructor
*
* @param pattern regular expression to check
* @param maxCharacterCount maximum allowed count of characters
* @see Pattern Pattern for Flags description
*/
public RegExpFormat(String pattern, int maxCharacterCount) {
this(pattern, null, maxCharacterCount, 0);
}
/**
* Constructor
*
* @param pattern regular expression to check
* @param maxCharacterCount maximum allowed count of characters
* @param regExpFlags additional Flags for the regular expression
* @see Pattern Pattern for Flags description
*/
public RegExpFormat(String pattern, String init, int maxCharacterCount, int regExpFlags) {
this(pattern, init, maxCharacterCount, regExpFlags, (Format) null);
}
/**
* Constructor
*
* @param pattern regular expression to check
* @param init default init string
* @param maxCharacterCount maximum allowed count of characters
* @param regExpFlags additional Flags for the regular expression
* @param parser (optional) format instance to be set as default format and parser
* @see Pattern Pattern for Flags description
*/
public RegExpFormat(String pattern, String init, int maxCharacterCount, int regExpFlags, Format parser) {
super();
this.pattern = pattern;
if (init != null) {
initMap = new Hashtable();
initMap.putAll(systemInitMap);
initMap.put(pattern, init);
} else {//to avoid using more resources, we use the static map
initMap = systemInitMap;
}
this.maxCharacterCount = maxCharacterCount;
this.regExpFlags = regExpFlags;
if (parser != null && !(parser instanceof RegExpFormat)) {
isAbleToParse = true;
this.parser = (GenericParser>) (parser instanceof GenericParser ? parser : new GenericParser(parser));
}
}
/**
* init
*
* @param pattern regular expression to check
* @param maxCharacterCount maximum allowed count of characters
* @param regExpFlags additional Flags for the regular expression
* @see Pattern Pattern for Flags description
*/
protected void init(String pattern, String init, int maxCharacterCount, int regExpFlags) {
this.pattern = pattern;
if (init != null) {
if (initMap == systemInitMap) {
initMap = new HashMap();
}
initMap.put(pattern, init);
}
this.maxCharacterCount = maxCharacterCount;
this.regExpFlags = regExpFlags;
compiledPattern = null;
}
/**
* tries to calculate the length through the given pattern. if the pattern contains brackets, no length will be
* calculated
*
* @param pattern pattern to evaluate
* @return pattern length, or throw exception, if containing brackets
*/
private static int calcLength(String pattern, String init) {
if (init != null && init.length() > 0) {
return init.length();
}
if (pattern.indexOf('{') == -1 && pattern.indexOf('[') == -1 && pattern.indexOf('(') == -1) {
return pattern.length();
}
// throw new ManagedException("tsl2nano.implementationerror", new Object[] { pattern,
// "only simple patterns without length definitions (like {0,5}) are allowed!" });
int length = ENV.get("field.pattern.regexp.default.length", 64);
LOG.warn("can't calculate pattern lenght for " + pattern + " --> using default length of " + length);
return length;
}
/**
* directly call to format with fullMatch parameter
*
* @param source object to format
* @param fullMatch if true, the source must exactly match the format pattern
* @return string representation of the given source
*/
public String format(Object source, boolean fullMatch) {
final boolean lastFullMatch = setFullMatch(fullMatch);
final String result = format(source);
setFullMatch(lastFullMatch);
return result;
}
/**
* converts the given object to a string, using the default formatter. Then the string will be formatted through the
* pattern. set {@link #fullMatch} to false, if you only need a find - not an exact match.
*
* @see java.text.Format#format(java.lang.Object, java.lang.StringBuffer, java.text.FieldPosition)
*/
@Override
public StringBuffer format(Object obj, StringBuffer toAppendTo, FieldPosition pos) {
String o;
//first: do a standard formatting, before checking against the regular expression
if (obj instanceof String) {
o = (String) obj;
} else {
if (parser == null) {
setDefaultFormatter(obj);
}
o = parser.format(obj);
}
if (compiledPattern == null) {
compiledPattern = Pattern.compile(pattern, regExpFlags);
}
//second: fill separation characters
o = getTextFormatted(o);
//third: check against the regular expression
final Matcher matcher = compiledPattern.matcher(o);
final boolean matches = fullMatch ? matcher.matches() : matcher.find();
if (matches) {
return toAppendTo.append(matcher.group());
} else {
throw new ManagedException("tsl2nano.regexpfailure", new Object[] { o, pattern });
}
}
/**
* The RegExpFormat doesn't know the object type - it will never do a true parsing! The method will return a
* formatted string, like the format() do. If not the whole text matches the pattern, null will be returned. But you
* have the possibility to set the {@link #fullMatch} to false, to only find (not match) the pattern.
*
* @see java.text.Format#parseObject(java.lang.String, java.text.ParsePosition)
*/
@Override
public Object parseObject(String source, ParsePosition pos) {
if (Util.isEmpty(source)) {
// set the pos > 0, otherwise the calling method will throw a parseexception.
pos.setIndex(1);
return null;
}
final String init = getInitMask(source);
// if pattern requires more characters, fill the source with a defined initialization.
if (init != null && source.length() <= init.length()) {
source = source + init.substring(source.length());
}
if (compiledPattern == null) {
compiledPattern = Pattern.compile(pattern, regExpFlags);
}
final Matcher matcher = compiledPattern.matcher(source);
final boolean matches = fullMatch ? matcher.matches() : matcher.find();
// set the pos > 0, otherwise the calling method will throw a parseexception.
pos.setIndex(source.length());
//perhaps real parsing
Object obj = null;
if (matches) {
try {
obj = parser != null && isAbleToParse(parser) ? parser.parseObject(source + getParsingSuffix(parser,
source)) : matcher.group();
} catch (final ParseException e) {
// ManagedException.forward(e);
LOG.error(e);
}
} else {
LOG.warn("text '" + source
+ "' doesn't match regular expression '"
+ pattern
+ "' (flags: "
+ regExpFlags
+ ", fullmatch: "
+ fullMatch
+ ", defaultFormatter: "
+ parser
+ ")");
}
return matches ? obj : null;
}
/**
* @return Returns the maxCharacterCount of this formatter.
*/
public int getMaxCharacterCount() {
return maxCharacterCount;
}
/**
* @return Returns the fullMatch.
*/
public boolean isFullMatch() {
return fullMatch;
}
/**
* @param fullMatch The fullMatch to set.
* @return lastFullMatch
*/
public boolean setFullMatch(boolean fullMatch) {
final boolean lastFullMatch = this.fullMatch;
this.fullMatch = fullMatch;
return lastFullMatch;
}
/**
* @deprecated: use {@link #numberWithGrouping(int, int, boolean)}
* @see #number(int, int, boolean)
*/
protected static final String number(int dec, int fract) {
return number(dec, fract, false);
}
/**
* @deprecated: use {@link #numberWithGrouping(int, int, boolean)}
* @param dec count of decimals
* @param fract count of fracts
* @param fixed if true, fixed characters will be used.
* @return regular expression describing the given number.
*/
protected static final String number(int dec, int fract, boolean fixed) {
final String p = "([-])|([-]{0,1}[0-9]{1," + dec
+ "}"
+ (fract > 0 ? "([" + DECIMAL_SEPARATOR + "]" + "[0-9]{0," + fract + "}){0,1})" : ")");
if (fixed) {
systemInitMap.put(p, fixed('0', dec) + (fract > 0 ? DECIMAL_SEPARATOR + fixed('0', fract) : ""));
}
return p;
}
/**
* numbers with grouping characters
*
* @param dec count of decimals
* @param fract count of fracts
* @param fixed if true, fixed characters will be used.
* @return regular expression describing the given number.
*/
protected static final String numberWithGrouping(int dec, int fract, boolean fixed) {
final StringBuilder p = new StringBuilder("([-])|([-]{0,1}");
/*
* involve grouping separators (3-digits-blocks).
* if text input don't contain fractions, fractions will be added automated,
* so, the decimal part should not exceed length of dec.
*/
dec -= fract;
// a len of 0 will be changed to 1!
if (dec <= 0)
dec = 1;
final int r = dec % 3;
if (r > 0)
p.append("\\d{0," + r + "}" + "[" + GROUPING_SEPARATOR + "]?");
final String a = "\\d{0,3}" + "[" + GROUPING_SEPARATOR + "]?";
final int c = (dec - r) / 3;
for (int i = 0; i < c; i++) {
p.append(a);
}
p.append((fract > 0 ? "([" + DECIMAL_SEPARATOR + "]" + "[0-9]{0," + fract + "}){0,1})" : ")"));
if (fixed) {
systemInitMap.put(p.toString(), fixed('0', dec) + (fract > 0 ? DECIMAL_SEPARATOR + fixed('0', fract) : ""));
}
return p.toString();
}
/**
* @param count count of characters
* @param alphaonly if true, only alpha numerics are allowed
* @return regular expression describing the given text specifications
*/
public static final String alphanum(int count, boolean alphaonly) {
return (alphaonly ? "[a-zA-Z]" : PATTERN_SINGLE_BYTE_SPACE) + "{0," + count + "}";
}
/**
* @param c characters to use
* @param count count to duplicate the character c.
* @return string with length count containing the characters c.
*/
private static final String fixed(char c, int count) {
final StringBuffer buf = new StringBuffer(count);
for (int i = 0; i < count; i++) {
buf.append(c);
}
return buf.toString();
}
/**
* provides a pattern defining numeric blocks, separated by spaces. e.g.: '0123 4567 8901'. this is an example with
* blockwidth=4 and mincount=maxcount=3.
*
* @param blockwidth numeric block width
* @param mincount minimum count of blocks
* @param maxcount maximum count of blocks
* @param allowRest if true, the last block may have less than blockwith numbers (e.g.: '0123 4567 89')
* @return regular expression defining numeric blocks
*/
public static final String patternNumericBlock(int blockwidth, int mincount, int maxcount, boolean allowRest) {
String pattern = "([0-9]{blockwidth,blockwidth} ){minCount,maxCount}" + (allowRest ? "[0-9]{0,blockwidth}" : "");
pattern = StringUtil.insertProperties(pattern,
MapUtil.asMap("blockwidth", blockwidth, "mincount", mincount, "maxcount", maxcount));
return pattern;
}
/**
* creates a standard regular expression for a currency. the biggest currency will be 999,999,999.99
*
* @return new formatter for a currency
*/
public static RegExpFormat createCurrencyRegExp() {
final int dec = 11, fract = 2;
return new RegExpFormat(numberWithGrouping(dec, fract, false) + getCurrencyPostfix(), null, dec + fract + 3,//28082012ts: 1 -->3 to enable a number like 123456789 --> 123.456.789,00 €
0,
getCurrencyFormat());
}
/**
* creates a standard regular expression for a currency without showing the currency symbol
*
* @return new formatter for a currency
*/
public static RegExpFormat createCurrencyNoSymbol() {
final int dec = 11, fract = 2;
return new RegExpFormat(numberWithGrouping(dec, fract, false) + "", null, dec + fract + 3,//28082012ts: 1 -->3 to enable a number like 123456789 --> 123.456.789,00 €
0,
getCurrencyFormatNoSymbol());
}
/**
* creates a standard regular expression for a number, without position after decimal point.
*
* @return new formatter for the given number
*/
public static RegExpFormat createCurrencyNoFraction() {
final int dec = 11, fract = 0;
return new RegExpFormat(numberWithGrouping(dec - 2, fract, false) + getCurrencyPostfix(),
null,
dec + fract + 3,//28082012ts: 1 -->3 to enable a number like 123456789 --> 123.456.789,00 €
0,
getCurrencyFormatNoFraction());
}
/**
* creates a regular expression for big currencies without decimal place. the biggest currency will be
* 999,999,999,999,999
*
* @return new formatter for a currency
*/
public static RegExpFormat createBigCurrencyRegExp() {
final int dec = 17, fract = 0;
return new RegExpFormat(numberWithGrouping(dec - 2, fract, false) + getCurrencyPostfix(),
null,
dec + fract + 3,//28082012ts: 1 -->3 to enable a number like 123456789 --> 123.456.789,00 €
0,
getCurrencyFormat());
}
/**
* creates a regular expresssion for the given type of number
*
* @param numberType type of number (having scale and precision)
* @return new formatter
*/
public static RegExpFormat createNumberRegExp(BigDecimal decimal) {
assert decimal != null : "decimal must not be null!";
//the scale is the scaled fraction digit count
final int dec = decimal.scale();
//the precision is the unscaled fraction digit count
final int fract = decimal.precision();
return createNumberRegExp(10, dec + fract);
}
/**
* delegates to {@link #createNumberRegExp(int, int, Class)}.
*/
public static RegExpFormat createNumberRegExp(int dec, int fract) {
return createNumberRegExp(dec, fract, null);
}
/**
* creates a standard regular expression for a number.
*
* @param dec count of decimals
* @param fract count of fracts
* @param type type of number (Integer, Double, BigDecimal, primitives..)
* @return new formatter for the given number
*/
public static RegExpFormat createNumberRegExp(int dec, int fract, Class> type) {
return new RegExpFormat(numberWithGrouping(dec, fract, false),
null,
dec + fract + 1,
0,
new GenericParser(type, null, null, fract));
}
/**
* Create a new alpha-numerical RegExpFormat.
*
* @param count count of characters
* @param alphaonly if true, only alpha numerics are allowed (but e.g. no spaces)
* @return new formatter for the given text specifications
*/
public static RegExpFormat createAlphaNumRegExp(int count, boolean alphaonly) {
return new RegExpFormat(alphanum(count, alphaonly), count, count > 100 ? Pattern.MULTILINE : 0);
}
/**
* createDateRegExp
*
* @return regexp for a german date
*/
public static RegExpFormat createDateRegExp() {
RegExpFormat regExp = new RegExpFormat(FORMAT_DATE_DE,
DateFormat.getDateInstance().format(getInitialDate()),
10,
0,
new GenericParser(Date.class));
return regExp;
}
/**
* createDateRegExp
*
* @return regexp for a german time
*/
public static RegExpFormat createTimeRegExp() {
RegExpFormat regExp = new RegExpFormat(FORMAT_TIME,
DateFormat.getTimeInstance().format(getInitialDate()),
8,
0,
new GenericParser(Time.class));
return regExp;
}
/**
* createDateRegExp
*
* @return regexp for a german date-time
*/
public static RegExpFormat createDateTimeRegExp() {
RegExpFormat regExp = new RegExpFormat(FORMAT_DATETIME_DE, DateFormat.getDateTimeInstance()
.format(getInitialDate()), 19, 0, new GenericParser(Timestamp.class));
return regExp;
}
/**
* Build a RegExpFormat for the given pattern. Note that the pattern MUST NOT contain length checks, this is added
* during construction (a suffix "{minLength, maxLength}" is added to the given pattern).
*
* @param pattern the pattern
* @param init optional init pattern
* @param minLength the minimum length
* @param maxLength maximum field length
* @param regExpFlags additional Flags for the regular expression
* @return the formatter
* @see Pattern Pattern for Flags description
*/
public static RegExpFormat createPatternRegExp(String pattern,
String init,
int minLength,
int maxLength,
int regExpFlags) {
assert minLength >= 0 : "minLength must be >= 0";
assert maxLength >= minLength : "maxLength must be >= minLength";
return new RegExpFormat(pattern + "{" + minLength + "," + maxLength + "}", init, maxLength, regExpFlags);
}
/**
* Create a new RegExpFormat that only controls the length of the input.
*
* @param minLength the minimum length
* @param maxLength maximum field length
* @param regExpFlags additional Flags for the regular expression
* @return the formatter
* @see Pattern Pattern for Flags description
*/
public static RegExpFormat createLengthRegExp(int minLength, int maxLength, int regExpFlags) {
assert minLength >= 0 : "minLength must be >= 0";
assert maxLength >= minLength : "maxLength must be >= minLength";
return new RegExpFormat(".{" + minLength + "," + maxLength + "}", maxLength, regExpFlags);
}
/**
* {@inheritDoc}
*/
@Override
public String toString() {
return this.getClass().getSimpleName() + "[pattern: " + pattern + "]";
}
/**
* getDefaultFormatter
*
* @return
*/
public Format getDefaultFormatter() {
return parser;
}
/**
* @param obj object to format with a new default formatter
*/
protected void setDefaultFormatter(Object obj) {
this.parser = new GenericParser(FormatUtil.getDefaultFormat(obj, false), obj.getClass());
}
/**
* append all following format characters (like ./:) of inputmask to the end of source.
*
* @param source source text
* @param initmask initmask for source text
* @return source and following format characters
*/
public static final String getTextWithCaretJump(String source, String initmask) {
final StringBuffer textWithCaretJump = new StringBuffer(source);
final int i = getNextInputPosition(source.length(), initmask);
if (source.length() < initmask.length() && i > source.length()) {
textWithCaretJump.append(initmask.substring(source.length(), i));
} else {
LOG.warn("initmask '" + initmask + "' is to short for text '" + source + "'");
}
return textWithCaretJump.toString();
}
/**
* tries to jump over all format characters (see initmask)
*
* @param currentPosition current caret position
* @return the next input position
*/
public int getNextInputPosition(int currentPosition) {
return getNextInputPosition(currentPosition, getInitMask());
}
/**
* tries to jump over all format characters (see initmask)
*
* @param currentPosition current caret position
* @param initmask mask, holding the formatting characters
* @return the next input position
*/
private static int getNextInputPosition(int currentPosition, String initmask) {
if (initmask != null) {
while (currentPosition < initmask.length()) {
if (!FORMAT_CHARACTERS.contains(initmask.substring(currentPosition, currentPosition + 1))) {
break;
}
currentPosition++;
}
}
return currentPosition;
}
/**
* getTextFormatted
*
* @param newText new text
* @return new text formatted with caret jumps
*/
public String getTextFormatted(String newText) {
final StringBuffer textWithCaretJump = new StringBuffer(getMaxCharacterCount());
final String initmask = getInitMask();
if (initmask != null) {
int ti = 0;
for (int i = 0; i < initmask.length(); i++) {
final String mc = initmask.substring(i, i + 1);
if (FORMAT_CHARACTERS.contains(mc) && (newText.length() <= ti || newText.charAt(ti) != mc.charAt(0))) {
textWithCaretJump.append(mc);
continue;
} else if (newText.length() <= ti) {
//no formatting char and source end reached
break;
}
textWithCaretJump.append(newText.charAt(ti));
ti++;
}
} else {
textWithCaretJump.append(newText);
}
return textWithCaretJump.toString();
}
/**
* formatWithCaretJump
*
* @param newText new text
* @return new text formatted with caret jumps
*/
public String formatWithCaretJump(String newText) {
return format(getTextFormatted(newText), false);
}
/**
* tries to evaluate, if the formatter is a simple one, or one, containing separation characters like in a date.
*
* @return true, if initmask contains at least one format character defined in {@link #FORMAT_CHARACTERS}.
*/
public boolean hasSeparationCharacter() {
if (getInitMask() == null) {
return false;
}
final char[] initMask = getInitMask().toCharArray();
for (int i = 0; i < initMask.length; i++) {
if (FORMAT_CHARACTERS.contains(String.valueOf(initMask[i]))) {
return true;
}
}
return false;
}
/**
* @return Returns the pattern.
*/
public String getPattern() {
return pattern;
}
/**
* setPattern
*
* @param pattern The pattern to set.
* @param init initializing (default) string
* @param maxLength max char count
* @param regExpFlags reg exp flags
*/
public void setPattern(String pattern, String init, int maxLength, int regExpFlags) {
init(pattern, init, maxLength, regExpFlags);
}
/**
* @return Returns the regExpFlags.
*/
public int getRegExpFlags() {
return regExpFlags;
}
/**
* getInitMask
*
* @return init mask
*/
public String getInitMask() {
return getInitMask(null);
}
/**
* is able to return a source specific init-mask. needed e.g. for dates. is called by parsing the source to an
* object to test the correctness.
*
* @param source (optional) source. if null, the init-mask stored for the pattern will be returned
* @return init mask
*/
protected String getInitMask(String source) {
String mask = null;
if (source != null) {
mask = initMap.get(source);
}
return mask != null ? mask : initMap.get(pattern);
}
/**
* @return Returns the isAbleToParse.
*/
public boolean isAbleToParse() {
return isAbleToParse;
}
/**
* @param isAbleToParse The isAbleToParse to set.
*/
public void setAbleToParse(boolean isAbleToParse) {
this.isAbleToParse = isAbleToParse;
}
/**
* if a format, like the currency (see {@link DecimalFormat#getNegativeSuffix()}, needs a number suffix, we have to
* give it to the parsing process.
*
* @param format format to ask
* @return empty or filled suffix
*/
protected static String getParsingSuffix(Format format, String txt) {
if (format instanceof GenericParser) {
format = ((GenericParser) format).getParsingFormat();
}
//TODO: its a workaround to handle the currency format. use the positive/negative prefix and suffix
if (format instanceof DecimalFormat) {
final StringBuffer s = new StringBuffer();
final DecimalFormat decFormat = (DecimalFormat) format;
final String prefix = decFormat.getNegativePrefix();
if (txt.equals(prefix)) {
s.append("0");
}
final String suffix = decFormat.getNegativeSuffix();
if (!txt.contains(suffix)) {
return s.append(suffix).toString();
} else {
return s.toString();
}
} else {
return "";
}
}
/**
* isAbleToParse
*
* @param format format
* @return true, if format can create an object from string
*/
public static boolean isAbleToParse(Format format) {
return !(format instanceof RegExpFormat) || ((RegExpFormat) format).isAbleToParse();
}
/**
* on default this method returns true, if the underlying {@link #parser} is a {@link NumberFormat}.
*
* override this method if you have a {@link #parser} that is not of type {@link NumberFormat} but you want a field
* to displayed like a number (e.g. in a table-column as right-alignment).
*
* @return true, if this format instance asserts to be a number format
*/
@Override
public boolean isNumber() {
return parser.isNumber();
}
/**
* convenience to check, if the given format is an instance of {@link RegExpFormat} and
* {@link RegExpFormat#isNumber} returns true.
*
* @param format format to evaluate
* @return true, if format is - or contains - a number format
*/
public static boolean isNumber(Format format) {
return (format instanceof NumberFormat) || (format instanceof INumberFormatCheck && ((INumberFormatCheck) format).isNumber());
}
/**
* getCurrencyPostfix
*
* @return regexp for currency postfix
*/
protected static final String getCurrencyPostfix() {
return getCurrencyPostfix(NumberFormat.getCurrencyInstance().getCurrency().getSymbol());
}
/**
* getCurrencyPostfix
*
* @param locale currency locale
* @return regexp for currency postfix
*/
protected static final String getCurrencyPostfix(Locale locale) {
return getCurrencyPostfix(NumberFormat.getCurrencyInstance(locale).getCurrency().getSymbol());
}
/**
* getCurrencyPostfix
*
* @param symbol currency symbol
* @return regexp for currency postfix
*/
protected static final String getCurrencyPostfix(String symbol) {
return "( " + symbol + "){0,1}";
}
/**
* currency with currency default precision (normally:2). object types must be {@link BigDecimal}!
*
* @return standard currency regular expression for current locale
*/
public static final Format getCurrencyFormat(Currency c) {
return CurrencyUtil.getFormat(c.getCurrencyCode(), c.getDefaultFractionDigits());
}
/**
* currency with currency default precision (normally:2). object types must be {@link BigDecimal}!
*
* @return standard currency regular expression for current locale
*/
public static final Format getCurrencyFormat() {
Currency c = NumberFormat.getCurrencyInstance().getCurrency();
return getCurrencyFormat(c);
}
/**
* creates a {@link Format} that is able to format and parse the given type. useful on text-fields representing an
* object.
*
* if you give a converter, the formatting/parsing will be done in two steps:
* - convert a string to the attributes type - search the right bean through this filled attribute
*
* Example:
* A person has an id of type long. the id will be shown with a mask, containing spaces. the formatted string '100
* 000 00000' must be converted to a long 10000000000. through an example person with given id, a real database
* object will be found.
*
* Example-Code:
*
*
* //string to long converter
* IConverter converter = new IConverter() {
* Override
* public Object from(Object toValue) {
* return SteuerakteUtil.getFormattedStnr(toValue);
* }
*
* Override
* public Object to(Object fromValue) {
* return SteuerakteUtil.getFormattedStnrAsLong((String) fromValue);
* }
* };
* return RegularExpressionFormat.getParser(Person.class,
* PersonConst.ATTR_PERSID,
* "[0-9]{3,3} [0-9]{3,3} [0-9]{5,5}",
* "000 000 00000",
* converter);
*
*
* @param field object type
* @param type field type
* @param uniqueIdAttribute attribute name to resolve field object
* @param pattern regular expression to constrain the object-to-string representation
* @param initMask default representing string for empty objects. the string length will be used to check for
* completition.
* @param converter (optional) converts a string to the attribute type (e.g.: the string '100 000 00000' will be
* converted to long 10000000000).
* @param useCache if true, all loaded objects will be cached.
* @return new {@link RegExpFormat}
*/
public static RegExpFormat getParser(final Class type,
final String uniqueIdAttribute,
String pattern,
final String initMask,
final IConverter converter,
final boolean useCache) {
final RegExpFormat format = new RegExpFormat(pattern, initMask) {
/** serialVersionUID */
private static final long serialVersionUID = 1L;
final TYPE instance = BeanClass.getBeanClass(type).createInstance();
final BeanAttribute attribute = BeanAttribute.getBeanAttribute(type, uniqueIdAttribute);
final ReferenceMap cache = useCache ? new ReferenceMap() : null;
@Override
public StringBuffer format(Object obj, StringBuffer toAppendTo, FieldPosition pos) {
if (type.isAssignableFrom(obj.getClass())) {
Object strObj = converter != null ? converter.from(attribute.getValue(obj))
: attribute.getValue(obj);
return toAppendTo.append(strObj);
} else {
// on verifyText, it will be a string
return super.format(obj, toAppendTo, pos);
}
}
@Override
public Object parseObject(String source) throws ParseException {
final String formattedSource = (String) super.parseObject(source);
if (formattedSource == null) {
return null;
}
Object convSource = converter != null ? converter.to(formattedSource) : formattedSource;
attribute.setValue(instance, convSource);
if (source.length() < initMask.length() || formattedSource.equals(getInitMask())) {
return instance;
}
if (useCache && cache.containsKey(convSource)) {
return cache.get(convSource);
}
Collection result = BeanContainer.instance().getBeansByExample(instance);
if (result.size() > 0) {
Object resObject = result.iterator().next();
if (useCache) {
cache.put(convSource, resObject);
}
return resObject;
}
return null;
}
@Override
public boolean isAbleToParse() {
return true;
}
};
format.setAbleToParse(true);
return format;
}
/**
* Extension for {@link Serializable}
*/
private void writeObject(java.io.ObjectOutputStream out) throws IOException {
initSerialization();
out.defaultWriteObject();
}
@Persist
private void initSerialization() {
//remove copied entries of systeminitmap - if it is not the systemInitMap itself!
if (initMap != null && initMap != systemInitMap) {
MapUtil.removeAll(initMap, systemInitMap.keySet());
} else {
initMap = null;
}
}
private void readObject(java.io.ObjectInputStream in) throws IOException, ClassNotFoundException {
in.defaultReadObject();
initDeserialization();
}
@Commit
@Complete
private void initDeserialization() {
if (initMap == null) {
initMap = systemInitMap;
}
}
}