package de.jpaw.bonaparte.core;
import java.math.BigDecimal;
import java.math.BigInteger;
import java.math.RoundingMode;
import java.util.UUID;
import org.joda.time.DateTimeZone;
import org.joda.time.Instant;
import org.joda.time.LocalDate;
import org.joda.time.LocalDateTime;
import org.joda.time.LocalTime;
import de.jpaw.bonaparte.pojos.meta.AlphanumericElementaryDataItem;
import de.jpaw.bonaparte.pojos.meta.BasicNumericElementaryDataItem;
import de.jpaw.bonaparte.pojos.meta.BinaryElementaryDataItem;
import de.jpaw.bonaparte.pojos.meta.FieldDefinition;
import de.jpaw.bonaparte.pojos.meta.MiscElementaryDataItem;
import de.jpaw.bonaparte.pojos.meta.NumericElementaryDataItem;
import de.jpaw.bonaparte.pojos.meta.TemporalElementaryDataItem;
import de.jpaw.bonaparte.pojos.meta.XEnumDataItem;
import de.jpaw.enums.AbstractXEnumBase;
import de.jpaw.enums.XEnumFactory;
import de.jpaw.util.Base64;
import de.jpaw.util.ByteArray;
import de.jpaw.util.CharTestsASCII;
import de.jpaw.util.IntegralLimits;
/** Utility subroutines, to be used by various parsers.
* This class performs the logical parsing, while leaving the splitting of fields to the callers.
*
* All parameters are expected to be not-null, null checks must happen before.
*
*/
public class StringParserUtil {
private final ParsePositionProvider parsePositionProvider;
private final boolean instantInMillis;
public StringParserUtil() {
parsePositionProvider = ParsePositionProvider.DEFAULT;
instantInMillis = false;
}
public StringParserUtil(ParsePositionProvider parsePositionProvider) {
this.parsePositionProvider = parsePositionProvider;
instantInMillis = false;
}
public StringParserUtil(ParsePositionProvider parsePositionProvider, boolean instantInMillis) {
this.parsePositionProvider = parsePositionProvider;
this.instantInMillis = instantInMillis;
}
protected MessageParserException err(int errno, FieldDefinition di, String data) {
return new MessageParserException(errno, di.getName(), data, parsePositionProvider);
}
public void ensureNotNull(FieldDefinition di, String data) throws MessageParserException {
if (data == null)
throw new MessageParserException(MessageParserException.ILLEGAL_EXPLICIT_NULL, di.getName(), null, parsePositionProvider);
}
public Character readCharacter(final MiscElementaryDataItem di, String data) throws MessageParserException {
if (data == null)
return null;
if (data.length() == 0) {
throw err(MessageParserException.EMPTY_CHAR, di, data);
} else if (data.length() > 1) {
throw err(MessageParserException.CHAR_TOO_LONG, di, data);
}
return Character.valueOf(data.charAt(0));
}
// just do trim and length checks
public String readStringSub(final AlphanumericElementaryDataItem di, String data) throws MessageParserException {
if (di.getDoTrim())
data = data.trim();
if (data.length() > di.getLength()) {
if (di.getDoTruncate())
data = data.substring(0, di.getLength());
else
throw err(MessageParserException.STRING_TOO_LONG, di, data);
}
return data;
}
public String readAscii(final AlphanumericElementaryDataItem di, String data) throws MessageParserException {
if (data == null)
return null;
data = readStringSub(di, data);
// ASCII checks
String type = di.getDataType().toLowerCase();
if (type.equals("lower") && !CharTestsASCII.isLowerCase(data))
throw err(MessageParserException.ILLEGAL_CHAR_LOWER, di, data);
else if (type.equals("upper") && !CharTestsASCII.isUpperCase(data))
throw err(MessageParserException.ILLEGAL_CHAR_UPPER, di, data);
else {
if (!CharTestsASCII.isPrintable(data))
throw err(MessageParserException.ILLEGAL_CHAR_ASCII, di, data);
}
return data;
}
// readString does the job for Unicode as well as ASCII
public String readString(final AlphanumericElementaryDataItem di, String data) throws MessageParserException {
if (data == null)
return null;
data = readStringSub(di, data);
// check for escape chars (unescaping is done before calling this method, as it is protocol dependent!
for (int i = 0; i < data.length(); ++i) {
char c = data.charAt(i);
if (c < ' ' && c != '\t' && !di.getAllowControlCharacters())
throw err(MessageParserException.ILLEGAL_CHAR_CTRL, di, data);
}
return data;
}
public Boolean readBoolean(final MiscElementaryDataItem di, String data) throws MessageParserException {
if (data == null)
return null;
if (data.equals("false"))
return Boolean.FALSE;
if (data.equals("true"))
return Boolean.TRUE;
if (data.length() != 1)
throw err(MessageParserException.ILLEGAL_BOOLEAN, di, data);
final char c = data.charAt(0);
if (c == '0') {
return Boolean.FALSE;
} else if (c == '1') {
return Boolean.TRUE;
} else {
throw err(MessageParserException.ILLEGAL_BOOLEAN, di, data);
}
}
public ByteArray readByteArray(BinaryElementaryDataItem di, String data) throws MessageParserException {
if (data == null)
return null;
if (data.length() % 4 != 0)
throw err(MessageParserException.BASE64_PARSING_ERROR, di, data);
if ((data.length() / 4) * 3 > di.getLength())
throw err(MessageParserException.BINARY_TOO_LONG, di, data);
return new ByteArray(readRaw(di, data)); // TODO: this call does an unnecessary copy
}
public byte[] readRaw(BinaryElementaryDataItem di, String data) throws MessageParserException {
if (data == null)
return null;
try {
byte [] btmp = data.getBytes();
if (btmp.length % 4 != 0)
throw err(MessageParserException.BASE64_PARSING_ERROR, di, data);
if ((btmp.length / 4) * 3 > di.getLength())
throw err(MessageParserException.BINARY_TOO_LONG, di, data);
return Base64.decode(btmp, 0, btmp.length);
} catch (IllegalArgumentException e) {
throw err(MessageParserException.BASE64_PARSING_ERROR, di, data);
}
// return DatatypeConverter.parseHexBinary(data);
}
public LocalDateTime readDayTime(TemporalElementaryDataItem di, String data) throws MessageParserException {
if (data == null)
return null;
int date;
int fractional = 0;
int dpoint;
if ((dpoint = data.indexOf('.')) < 0) {
// day only despite allowed time
date = Integer.parseInt(data);
} else {
// day and time
date = Integer.parseInt(data.substring(0, dpoint));
fractional = Integer.parseInt(data.substring(dpoint + 1));
switch (data.length() - dpoint - 1) { // i.e. number of fractional digits
case 6:
fractional *= 1000;
break; // precisely seconds resolution (timestamp(0))
case 7:
fractional *= 100;
break;
case 8:
fractional *= 10;
break;
case 9:
break; // maximum resolution (milliseconds)
default: // something weird
throw err(MessageParserException.BAD_TIMESTAMP_FRACTIONALS, di,
String.format("(found %d for %s)", data.length() - dpoint - 1, data));
}
}
// set the date and time
int day, month, year, hour, minute, second;
year = date / 10000;
month = (date %= 10000) / 100;
day = date %= 100;
if (di.getHhmmss()) {
hour = fractional / 10000000;
minute = (fractional %= 10000000) / 100000;
second = (fractional %= 100000) / 1000;
} else {
hour = fractional / 3600000;
minute = (fractional %= 3600000) / 60000;
second = (fractional %= 60000) / 1000;
}
fractional %= 1000;
// first checks
if ((year < 1601) || (year > 2399) || (month == 0) || (month > 12) || (day == 0)
|| (day > 31)) {
throw err(MessageParserException.ILLEGAL_DAY, di,
String.format("(found %d for %s)", year*10000+month*100+day, data));
}
if ((hour > 23) || (minute > 59) || (second > 59)) {
throw err(MessageParserException.ILLEGAL_TIME, di,
String.format("(found %d for %s)", (hour * 10000) + (minute * 100) + second, data));
}
// now set the return value
LocalDateTime result;
try {
// TODO! default is lenient mode, therefore will not check. Solution
// is to read the data again and compare the values of day, month
// and year
result = new LocalDateTime(year, month, day, hour, minute, second, fractional);
} catch (Exception e) {
throw err(MessageParserException.ILLEGAL_CALENDAR_VALUE, di, data);
}
return result;
}
public LocalDate readDay(TemporalElementaryDataItem di, String data) throws MessageParserException {
if (data == null)
return null;
int date = Integer.parseInt(data);
// set the date and time
int day, month, year;
year = date / 10000;
month = (date %= 10000) / 100;
day = date %= 100;
// first checks
if ((year < 1601) || (year > 2399) || (month == 0) || (month > 12) || (day == 0)
|| (day > 31)) {
throw err(MessageParserException.ILLEGAL_DAY, di,
String.format("(found %d for %s)", year*10000+month*100+day, data));
}
// now set the return value
LocalDate result;
try {
// TODO! default is lenient mode, therefore will not check. Solution
// is to read the data again and compare the values of day, month
// and year
result = new LocalDate(year, month, day);
} catch (Exception e) {
throw err(MessageParserException.ILLEGAL_CALENDAR_VALUE, di, data);
}
return result;
}
public LocalTime readTime(TemporalElementaryDataItem di, String data) throws MessageParserException {
if (data == null)
return null;
int millis = 0;
int seconds = 0;
int dpoint;
if ((dpoint = data.indexOf('.')) < 0) {
seconds = Integer.parseInt(data); // only seconds
} else {
// seconds and millis seconds
seconds = Integer.parseInt(data.substring(0, dpoint));
millis = Integer.parseInt(data.substring(dpoint + 1));
switch (data.length() - dpoint - 1) { // i.e. number of fractional digits
case 2:
millis *= 10;
break;
case 1:
millis *= 100;
break;
case 3:
break; // maximum resolution (milliseconds)
default: // something weird
throw err(MessageParserException.BAD_TIMESTAMP_FRACTIONALS, di,
String.format("(found %d for %s)", data.length() - dpoint - 1, data));
}
}
// set the date and time
int hour, minute, second;
if (di.getHhmmss()) {
hour = seconds / 10000;
minute = (seconds % 10000) / 100;
second = seconds % 100;
seconds = 3600 * hour + 60 * minute + second; // convert to seconds of day
} else {
hour = seconds / 3600;
minute = (seconds % 3600) / 60;
second = seconds % 60;
}
// first checks
if ((hour > 23) || (minute > 59) || (second > 59)) {
throw err(MessageParserException.ILLEGAL_TIME, di,
String.format("(found %d for %s)", (hour * 10000) + (minute * 100) + second, data));
}
return new LocalTime(1000 * seconds + millis, DateTimeZone.UTC);
}
// see ExtendedJsonEscaperForAppendables.instantInMillis: this variant assumes "false", i.e. instant in seconds.
public Instant readInstant(TemporalElementaryDataItem di, String data) throws MessageParserException {
if (data == null)
return null;
if (instantInMillis) {
return new Instant(Long.parseLong(data));
}
int millis = 0;
long seconds = 0;
int dpoint;
if ((dpoint = data.indexOf('.')) < 0) {
seconds = Long.parseLong(data); // only seconds
} else {
// seconds and millis seconds
seconds = Long.parseLong(data.substring(0, dpoint));
millis = Integer.parseInt(data.substring(dpoint + 1));
switch (data.length() - dpoint - 1) { // i.e. number of fractional digits
case 2:
millis *= 10;
break;
case 1:
millis *= 100;
break;
case 3:
break; // maximum resolution (milliseconds)
default: // something weird
throw err(MessageParserException.BAD_TIMESTAMP_FRACTIONALS, di,
String.format("(found %d for %s)", data.length() - dpoint - 1, data));
}
}
if (di.getFractionalSeconds() == 0) {
// don't want millis here: trunc! (TODO: add a flag to complain!)
millis = 0;
}
return new Instant(1000L * seconds + millis);
}
public Byte readByte(final BasicNumericElementaryDataItem di, String data) throws MessageParserException {
if (data == null) {
return null;
}
return Byte.valueOf(readPrimitiveByte(di, data));
}
public Short readShort(final BasicNumericElementaryDataItem di, String data) throws MessageParserException {
if (data == null) {
return null;
}
return Short.valueOf(readPrimitiveShort(di, data));
}
public Integer readInteger(final BasicNumericElementaryDataItem di, String data) throws MessageParserException {
if (data == null) {
return null;
}
return Integer.valueOf(readPrimitiveInteger(di, data));
}
public Long readLong(final BasicNumericElementaryDataItem di, String data) throws MessageParserException {
if (data == null) {
return null;
}
return Long.valueOf(readPrimitiveLong(di, data));
}
public Float readFloat(final BasicNumericElementaryDataItem di, String data) throws MessageParserException {
if (data == null)
return null;
return Float.valueOf(readPrimitiveFloat(di, data));
}
public Double readDouble(final BasicNumericElementaryDataItem di, String data) throws MessageParserException {
if (data == null)
return null;
return Double.valueOf(readPrimitiveDouble(di, data));
}
public BigInteger readBigInteger(final BasicNumericElementaryDataItem di, String data) throws MessageParserException {
if (data == null) {
return null;
}
if (data.length() > (di.getTotalDigits() + (((data.charAt(0) == '-') || (data.charAt(0) == '+')) ? 1 : 0)))
throw err(MessageParserException.NUMERIC_TOO_LONG, di, data);
try {
final BigInteger r = new BigInteger(data);
if (!di.getIsSigned() && r.signum() < 0)
throw err(MessageParserException.SUPERFLUOUS_SIGN, di, data);
return r;
} catch (NumberFormatException e) {
throw err(MessageParserException.NUMBER_PARSING_ERROR, di, data);
}
}
// parse the not-null String into a BigDecimal, and perform optional scaling
public BigDecimal readBigDecimal(final NumericElementaryDataItem di, String data) throws MessageParserException {
if (data == null)
return null;
try {
BigDecimal r = new BigDecimal(data);
if (!di.getIsSigned() && r.signum() < 0)
throw err(MessageParserException.SUPERFLUOUS_SIGN, di, data);
int decimals = di.getDecimalDigits();
try {
if (r.scale() > decimals)
r = r.setScale(decimals, di.getRounding() ? RoundingMode.HALF_EVEN : RoundingMode.UNNECESSARY);
if (di.getAutoScale() && r.scale() < decimals) // round for smaller as well!
r = r.setScale(decimals, RoundingMode.UNNECESSARY);
} catch (ArithmeticException a) {
throw err(MessageParserException.TOO_MANY_DECIMALS, di, data);
}
// check for overflow
if (di.getTotalDigits() - decimals < r.precision() - r.scale())
throw err(MessageParserException.TOO_MANY_DIGITS, di, data);
return r;
} catch (NumberFormatException e) {
throw err(MessageParserException.NUMBER_PARSING_ERROR, di, data);
}
}
public UUID readUUID(final MiscElementaryDataItem di, String data) throws MessageParserException {
if (data == null) {
return null;
}
try {
return UUID.fromString(data);
} catch (IllegalArgumentException e) {
throw err(MessageParserException.BAD_UUID_FORMAT, di, data);
}
}
public <T extends AbstractXEnumBase<T>> T readXEnum(final XEnumDataItem di, final XEnumFactory<T> factory, String data) throws MessageParserException {
if (data == null) {
T result = factory.getNullToken();
if (result == null && di.getIsRequired())
throw err(MessageParserException.EMPTY_BUT_REQUIRED_FIELD, di, data);
return result;
}
final T value = factory.getByToken(data);
if (value == null)
throw err(MessageParserException.INVALID_ENUM_TOKEN, di, data);
return value;
}
public boolean readPrimitiveBoolean(MiscElementaryDataItem di, String data) throws MessageParserException {
ensureNotNull(di, data);
if (data.equals("false"))
return false;
if (data.equals("true"))
return true;
if (data.length() != 1)
throw err(MessageParserException.ILLEGAL_BOOLEAN, di, data);
final char c = data.charAt(0);
if (c == '0') {
return false;
} else if (c == '1') {
return true;
} else {
throw err(MessageParserException.ILLEGAL_BOOLEAN, di, data);
}
}
public char readPrimitiveCharacter(MiscElementaryDataItem di, String data) throws MessageParserException {
ensureNotNull(di, data);
if (data.length() == 0) {
throw err(MessageParserException.EMPTY_CHAR, di, data);
} else if (data.length() > 1) {
throw err(MessageParserException.CHAR_TOO_LONG, di, data);
}
return data.charAt(0);
}
public double readPrimitiveDouble(BasicNumericElementaryDataItem di, String data) throws MessageParserException {
ensureNotNull(di, data);
try {
final double r = Double.parseDouble(data);
if (!di.getIsSigned() && r < 0)
throw err(MessageParserException.SUPERFLUOUS_SIGN, di, data);
return r;
} catch (NumberFormatException e) {
throw err(MessageParserException.NUMBER_PARSING_ERROR, di, data);
}
}
public float readPrimitiveFloat(BasicNumericElementaryDataItem di, String data) throws MessageParserException {
ensureNotNull(di, data);
try {
final float r = Float.parseFloat(data);
if (!di.getIsSigned() && r < 0)
throw err(MessageParserException.SUPERFLUOUS_SIGN, di, data);
return r;
} catch (NumberFormatException e) {
throw err(MessageParserException.NUMBER_PARSING_ERROR, di, data);
}
}
public long readPrimitiveLong(BasicNumericElementaryDataItem di, String data) throws MessageParserException {
ensureNotNull(di, data);
try {
final long r = Long.parseLong(data);
if (r < 0 && !di.getIsSigned())
throw err(MessageParserException.SUPERFLUOUS_SIGN, di, data);
final int maxDigits = di.getTotalDigits();
if (maxDigits > 0) {
// make sure that the parsed value does not exceed the configured number of digits
if (r < IntegralLimits.LONG_MIN_VALUES[maxDigits] || r > IntegralLimits.LONG_MAX_VALUES[maxDigits])
throw err(MessageParserException.NUMERIC_TOO_MANY_DIGITS, di, data);
}
return r;
} catch (NumberFormatException e) {
throw err(MessageParserException.NUMBER_PARSING_ERROR, di, data);
}
}
public int readPrimitiveInteger(BasicNumericElementaryDataItem di, String data) throws MessageParserException {
ensureNotNull(di, data);
try {
final int r = Integer.parseInt(data);
if (r < 0 && !di.getIsSigned())
throw err(MessageParserException.SUPERFLUOUS_SIGN, di, data);
final int maxDigits = di.getTotalDigits();
if (maxDigits > 0) {
// make sure that the parsed value does not exceed the configured number of digits
if (r < IntegralLimits.INT_MIN_VALUES[maxDigits] || r > IntegralLimits.INT_MAX_VALUES[maxDigits])
throw err(MessageParserException.NUMERIC_TOO_MANY_DIGITS, di, data);
}
return r;
} catch (NumberFormatException e) {
throw err(MessageParserException.NUMBER_PARSING_ERROR, di, data);
}
}
public short readPrimitiveShort(BasicNumericElementaryDataItem di, String data) throws MessageParserException {
ensureNotNull(di, data);
try {
final short r = Short.parseShort(data);
if (r < 0 && !di.getIsSigned())
throw err(MessageParserException.SUPERFLUOUS_SIGN, di, data);
final int maxDigits = di.getTotalDigits();
if (maxDigits > 0) {
// make sure that the parsed value does not exceed the configured number of digits
if (r < IntegralLimits.SHORT_MIN_VALUES[maxDigits] || r > IntegralLimits.SHORT_MAX_VALUES[maxDigits])
throw err(MessageParserException.NUMERIC_TOO_MANY_DIGITS, di, data);
}
return r;
} catch (NumberFormatException e) {
throw err(MessageParserException.NUMBER_PARSING_ERROR, di, data);
}
}
public byte readPrimitiveByte(BasicNumericElementaryDataItem di, String data) throws MessageParserException {
ensureNotNull(di, data);
try {
final byte r = Byte.parseByte(data);
if (r < 0 && !di.getIsSigned())
throw err(MessageParserException.SUPERFLUOUS_SIGN, di, data);
final int maxDigits = di.getTotalDigits();
if (maxDigits > 0) {
// make sure that the parsed value does not exceed the configured number of digits
if (r < IntegralLimits.BYTE_MIN_VALUES[maxDigits] || r > IntegralLimits.BYTE_MAX_VALUES[maxDigits])
throw err(MessageParserException.NUMERIC_TOO_MANY_DIGITS, di, data);
}
return r;
} catch (NumberFormatException e) {
throw err(MessageParserException.NUMBER_PARSING_ERROR, di, data);
}
}
}