[hotfix][mysql] Trim default value for BIGINT and SMALLINT types when parsing MySQL DDL (#2418)
parent
54df7e93f0
commit
8e7378d8f9
@ -0,0 +1,527 @@
|
|||||||
|
/*
|
||||||
|
* Copyright Debezium Authors.
|
||||||
|
*
|
||||||
|
* Licensed under the Apache Software License version 2.0, available at http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*/
|
||||||
|
|
||||||
|
package io.debezium.connector.mysql;
|
||||||
|
|
||||||
|
import io.debezium.annotation.Immutable;
|
||||||
|
import io.debezium.relational.Column;
|
||||||
|
import io.debezium.relational.DefaultValueConverter;
|
||||||
|
import io.debezium.relational.ValueConverter;
|
||||||
|
import io.debezium.util.Collect;
|
||||||
|
import org.apache.kafka.connect.data.Field;
|
||||||
|
import org.apache.kafka.connect.data.Schema;
|
||||||
|
import org.apache.kafka.connect.data.SchemaBuilder;
|
||||||
|
import org.slf4j.Logger;
|
||||||
|
import org.slf4j.LoggerFactory;
|
||||||
|
|
||||||
|
import java.math.BigDecimal;
|
||||||
|
import java.math.RoundingMode;
|
||||||
|
import java.sql.Timestamp;
|
||||||
|
import java.sql.Types;
|
||||||
|
import java.time.Duration;
|
||||||
|
import java.time.Instant;
|
||||||
|
import java.time.LocalDate;
|
||||||
|
import java.time.LocalDateTime;
|
||||||
|
import java.time.ZoneId;
|
||||||
|
import java.time.format.DateTimeFormatter;
|
||||||
|
import java.time.format.DateTimeFormatterBuilder;
|
||||||
|
import java.time.temporal.ChronoField;
|
||||||
|
import java.util.Optional;
|
||||||
|
import java.util.Set;
|
||||||
|
import java.util.regex.Matcher;
|
||||||
|
import java.util.regex.Pattern;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Copied from Debezium project(1.9.7.final) to add BIGINT and SMALLINT to TRIM_DATA_TYPES. Remove
|
||||||
|
* this when https://issues.redhat.com/browse/DBZ-6824 is fixed.
|
||||||
|
*
|
||||||
|
* <p>Line 81 & 82: add BIGINT and SMALLINT to TRIM_DATA_TYPES.
|
||||||
|
*/
|
||||||
|
@Immutable
|
||||||
|
public class MySqlDefaultValueConverter implements DefaultValueConverter {
|
||||||
|
|
||||||
|
private static final Logger LOGGER = LoggerFactory.getLogger(MySqlDefaultValueConverter.class);
|
||||||
|
|
||||||
|
private static final Pattern EPOCH_EQUIVALENT_TIMESTAMP =
|
||||||
|
Pattern.compile(
|
||||||
|
"(\\d{4}-\\d{2}-00|\\d{4}-00-\\d{2}|0000-\\d{2}-\\d{2}) (00:00:00(\\.\\d{1,6})?)");
|
||||||
|
|
||||||
|
private static final Pattern EPOCH_EQUIVALENT_DATE =
|
||||||
|
Pattern.compile("\\d{4}-\\d{2}-00|\\d{4}-00-\\d{2}|0000-\\d{2}-\\d{2}");
|
||||||
|
|
||||||
|
private static final String EPOCH_TIMESTAMP = "1970-01-01 00:00:00";
|
||||||
|
|
||||||
|
private static final String EPOCH_DATE = "1970-01-01";
|
||||||
|
|
||||||
|
private static final Pattern TIMESTAMP_PATTERN =
|
||||||
|
Pattern.compile("([0-9]*-[0-9]*-[0-9]*) ([0-9]*:[0-9]*:[0-9]*(\\.([0-9]*))?)");
|
||||||
|
|
||||||
|
private static final Pattern CHARSET_INTRODUCER_PATTERN =
|
||||||
|
Pattern.compile("^_[A-Za-z0-9]+'(.*)'$");
|
||||||
|
|
||||||
|
@Immutable
|
||||||
|
private static final Set<Integer> TRIM_DATA_TYPES =
|
||||||
|
Collect.unmodifiableSet(
|
||||||
|
Types.TINYINT,
|
||||||
|
Types.INTEGER,
|
||||||
|
Types.DATE,
|
||||||
|
Types.TIMESTAMP,
|
||||||
|
Types.TIMESTAMP_WITH_TIMEZONE,
|
||||||
|
Types.TIME,
|
||||||
|
Types.BOOLEAN,
|
||||||
|
Types.BIT,
|
||||||
|
Types.NUMERIC,
|
||||||
|
Types.DECIMAL,
|
||||||
|
Types.FLOAT,
|
||||||
|
Types.DOUBLE,
|
||||||
|
Types.REAL,
|
||||||
|
Types.BIGINT,
|
||||||
|
Types.SMALLINT);
|
||||||
|
|
||||||
|
@Immutable
|
||||||
|
private static final Set<Integer> NUMBER_DATA_TYPES =
|
||||||
|
Collect.unmodifiableSet(
|
||||||
|
Types.BIT,
|
||||||
|
Types.TINYINT,
|
||||||
|
Types.SMALLINT,
|
||||||
|
Types.INTEGER,
|
||||||
|
Types.BIGINT,
|
||||||
|
Types.FLOAT,
|
||||||
|
Types.REAL,
|
||||||
|
Types.DOUBLE,
|
||||||
|
Types.NUMERIC,
|
||||||
|
Types.DECIMAL);
|
||||||
|
|
||||||
|
private static final DateTimeFormatter ISO_LOCAL_DATE_WITH_OPTIONAL_TIME =
|
||||||
|
new DateTimeFormatterBuilder()
|
||||||
|
.append(DateTimeFormatter.ISO_LOCAL_DATE)
|
||||||
|
.optionalStart()
|
||||||
|
.appendLiteral(" ")
|
||||||
|
.append(DateTimeFormatter.ISO_LOCAL_TIME)
|
||||||
|
.optionalEnd()
|
||||||
|
.toFormatter();
|
||||||
|
|
||||||
|
private final MySqlValueConverters converters;
|
||||||
|
|
||||||
|
public MySqlDefaultValueConverter(MySqlValueConverters converters) {
|
||||||
|
this.converters = converters;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* This interface is used by a DDL parser to convert the string default value to a Java type
|
||||||
|
* recognized by value converters for a subset of types.
|
||||||
|
*
|
||||||
|
* @param column the column definition describing the {@code data} value; never null
|
||||||
|
* @param defaultValueExpression the default value literal; may be null
|
||||||
|
* @return value converted to a Java type; optional
|
||||||
|
*/
|
||||||
|
@Override
|
||||||
|
public Optional<Object> parseDefaultValue(Column column, String defaultValueExpression) {
|
||||||
|
Object logicalDefaultValue = convert(column, defaultValueExpression);
|
||||||
|
if (logicalDefaultValue == null) {
|
||||||
|
return Optional.empty();
|
||||||
|
}
|
||||||
|
|
||||||
|
final SchemaBuilder schemaBuilder = converters.schemaBuilder(column);
|
||||||
|
if (schemaBuilder == null) {
|
||||||
|
return Optional.of(logicalDefaultValue);
|
||||||
|
}
|
||||||
|
final Schema schema = schemaBuilder.build();
|
||||||
|
|
||||||
|
// In order to get the valueConverter for this column, we have to create a field;
|
||||||
|
// The index value -1 in the field will never used when converting default value;
|
||||||
|
// So we can set any number here;
|
||||||
|
final Field field = new Field(column.name(), -1, schema);
|
||||||
|
final ValueConverter valueConverter = converters.converter(column, field);
|
||||||
|
|
||||||
|
return Optional.ofNullable(valueConverter.convert(logicalDefaultValue));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Converts a default value from the expected format to a logical object acceptable by the main
|
||||||
|
* JDBC converter.
|
||||||
|
*
|
||||||
|
* @param column column definition
|
||||||
|
* @param value string formatted default value
|
||||||
|
* @return value converted to a Java type
|
||||||
|
*/
|
||||||
|
public Object convert(Column column, String value) {
|
||||||
|
if (value == null) {
|
||||||
|
return value;
|
||||||
|
}
|
||||||
|
|
||||||
|
// trim non varchar data types before converting
|
||||||
|
if (TRIM_DATA_TYPES.contains(column.jdbcType())) {
|
||||||
|
value = value.trim();
|
||||||
|
}
|
||||||
|
|
||||||
|
// strip character set introducer on default value expressions
|
||||||
|
value = stripCharacterSetIntroducer(value);
|
||||||
|
|
||||||
|
// boolean is also INT(1) or TINYINT(1)
|
||||||
|
if (NUMBER_DATA_TYPES.contains(column.jdbcType())
|
||||||
|
&& ("true".equalsIgnoreCase(value) || "false".equalsIgnoreCase(value))) {
|
||||||
|
if (Types.DECIMAL == column.jdbcType() || Types.NUMERIC == column.jdbcType()) {
|
||||||
|
return convertToDecimal(column, value.equalsIgnoreCase("true") ? "1" : "0");
|
||||||
|
}
|
||||||
|
return value.equalsIgnoreCase("true") ? 1 : 0;
|
||||||
|
}
|
||||||
|
switch (column.jdbcType()) {
|
||||||
|
case Types.DATE:
|
||||||
|
return convertToLocalDate(column, value);
|
||||||
|
case Types.TIMESTAMP:
|
||||||
|
return convertToLocalDateTime(column, value);
|
||||||
|
case Types.TIMESTAMP_WITH_TIMEZONE:
|
||||||
|
return convertToTimestamp(column, value);
|
||||||
|
case Types.TIME:
|
||||||
|
return convertToDuration(column, value);
|
||||||
|
case Types.BOOLEAN:
|
||||||
|
return convertToBoolean(value);
|
||||||
|
case Types.BIT:
|
||||||
|
return convertToBits(column, value);
|
||||||
|
|
||||||
|
case Types.NUMERIC:
|
||||||
|
case Types.DECIMAL:
|
||||||
|
return convertToDecimal(column, value);
|
||||||
|
|
||||||
|
case Types.FLOAT:
|
||||||
|
case Types.DOUBLE:
|
||||||
|
case Types.REAL:
|
||||||
|
return convertToDouble(value);
|
||||||
|
}
|
||||||
|
return value;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Converts a string object for an object type of {@link LocalDate} or {@link LocalDateTime} in
|
||||||
|
* case of MySql Date type. If the column definition allows null and default value is
|
||||||
|
* 0000-00-00, we need return null; else 0000-00-00 will be replaced with 1970-01-01;
|
||||||
|
*
|
||||||
|
* @param column the column definition describing the {@code data} value; never null
|
||||||
|
* @param value the string object to be converted into a {@link LocalDate} type or {@link
|
||||||
|
* LocalDateTime} in case of MySql Date type;
|
||||||
|
* @return the converted value;
|
||||||
|
*/
|
||||||
|
private Object convertToLocalDate(Column column, String value) {
|
||||||
|
final boolean zero =
|
||||||
|
EPOCH_EQUIVALENT_DATE.matcher(value).matches()
|
||||||
|
|| EPOCH_EQUIVALENT_TIMESTAMP.matcher(value).matches()
|
||||||
|
|| "0".equals(value);
|
||||||
|
|
||||||
|
if (zero && column.isOptional()) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
if (zero) {
|
||||||
|
value = EPOCH_DATE;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
return LocalDate.from(ISO_LOCAL_DATE_WITH_OPTIONAL_TIME.parse(value));
|
||||||
|
} catch (Exception e) {
|
||||||
|
LOGGER.warn(
|
||||||
|
"Invalid default value '{}' for date column '{}'; {}",
|
||||||
|
value,
|
||||||
|
column.name(),
|
||||||
|
e.getMessage());
|
||||||
|
if (column.isOptional()) {
|
||||||
|
return null;
|
||||||
|
} else {
|
||||||
|
return LocalDate.from(ISO_LOCAL_DATE_WITH_OPTIONAL_TIME.parse(EPOCH_DATE));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Converts a string object for an object type of {@link LocalDateTime}. If the column
|
||||||
|
* definition allows null and default value is 0000-00-00 00:00:00, we need return null, else
|
||||||
|
* 0000-00-00 00:00:00 will be replaced with 1970-01-01 00:00:00;
|
||||||
|
*
|
||||||
|
* @param column the column definition describing the {@code data} value; never null
|
||||||
|
* @param value the string object to be converted into a {@link LocalDateTime} type;
|
||||||
|
* @return the converted value;
|
||||||
|
*/
|
||||||
|
private Object convertToLocalDateTime(Column column, String value) {
|
||||||
|
final boolean matches =
|
||||||
|
EPOCH_EQUIVALENT_TIMESTAMP.matcher(value).matches() || "0".equals(value);
|
||||||
|
if (matches) {
|
||||||
|
if (column.isOptional()) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
value = EPOCH_TIMESTAMP;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
return LocalDateTime.from(timestampFormat(column.length()).parse(value));
|
||||||
|
} catch (Exception e) {
|
||||||
|
LOGGER.warn(
|
||||||
|
"Invalid default value '{}' for datetime column '{}'; {}",
|
||||||
|
value,
|
||||||
|
column.name(),
|
||||||
|
e.getMessage());
|
||||||
|
if (column.isOptional()) {
|
||||||
|
return null;
|
||||||
|
} else {
|
||||||
|
return LocalDateTime.from(timestampFormat(column.length()).parse(EPOCH_TIMESTAMP));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Converts a string object for an object type of {@link Timestamp}. If the column definition
|
||||||
|
* allows null and default value is 0000-00-00 00:00:00, we need return null, else 0000-00-00
|
||||||
|
* 00:00:00 will be replaced with 1970-01-01 00:00:00;
|
||||||
|
*
|
||||||
|
* @param column the column definition describing the {@code data} value; never null
|
||||||
|
* @param value the string object to be converted into a {@link Timestamp} type;
|
||||||
|
* @return the converted value;
|
||||||
|
*/
|
||||||
|
private Object convertToTimestamp(Column column, String value) {
|
||||||
|
final boolean matches =
|
||||||
|
EPOCH_EQUIVALENT_TIMESTAMP.matcher(value).matches()
|
||||||
|
|| "0".equals(value)
|
||||||
|
|| EPOCH_TIMESTAMP.equals(value);
|
||||||
|
if (matches) {
|
||||||
|
if (column.isOptional()) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return Timestamp.from(Instant.EPOCH);
|
||||||
|
}
|
||||||
|
value = cleanTimestamp(value);
|
||||||
|
return Timestamp.valueOf(value).toInstant().atZone(ZoneId.systemDefault());
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Converts a string object for an object type of {@link Duration}.
|
||||||
|
*
|
||||||
|
* @param column the column definition describing the {@code data} value; never null
|
||||||
|
* @param value the string object to be converted into a {@link Duration} type;
|
||||||
|
* @return the converted value;
|
||||||
|
*/
|
||||||
|
private Object convertToDuration(Column column, String value) {
|
||||||
|
Matcher matcher = TIMESTAMP_PATTERN.matcher(value);
|
||||||
|
if (matcher.matches()) {
|
||||||
|
value = matcher.group(2);
|
||||||
|
}
|
||||||
|
return MySqlValueConverters.stringToDuration(value);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Converts a string object for an expected JDBC type of {@link Types#DOUBLE}.
|
||||||
|
*
|
||||||
|
* @param value the string object to be converted into a {@link Types#DOUBLE} type;
|
||||||
|
* @return the converted value;
|
||||||
|
*/
|
||||||
|
private Object convertToDouble(String value) {
|
||||||
|
return Double.parseDouble(value);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Converts a string object for an expected JDBC type of {@link Types#DECIMAL}.
|
||||||
|
*
|
||||||
|
* @param column the column definition describing the {@code data} value; never null
|
||||||
|
* @param value the string object to be converted into a {@link Types#DECIMAL} type;
|
||||||
|
* @return the converted value;
|
||||||
|
*/
|
||||||
|
private Object convertToDecimal(Column column, String value) {
|
||||||
|
return column.scale().isPresent()
|
||||||
|
? new BigDecimal(value).setScale(column.scale().get(), RoundingMode.HALF_UP)
|
||||||
|
: new BigDecimal(value);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Converts a string object for an expected JDBC type of {@link Types#BIT}.
|
||||||
|
*
|
||||||
|
* @param column the column definition describing the {@code data} value; never null
|
||||||
|
* @param value the string object to be converted into a {@link Types#BIT} type;
|
||||||
|
* @return the converted value;
|
||||||
|
*/
|
||||||
|
private Object convertToBits(Column column, String value) {
|
||||||
|
if (column.length() > 1) {
|
||||||
|
return convertToBits(value);
|
||||||
|
}
|
||||||
|
return convertToBit(value);
|
||||||
|
}
|
||||||
|
|
||||||
|
private Object convertToBit(String value) {
|
||||||
|
try {
|
||||||
|
return Short.parseShort(value) != 0;
|
||||||
|
} catch (NumberFormatException ignore) {
|
||||||
|
return Boolean.parseBoolean(value);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private Object convertToBits(String value) {
|
||||||
|
int nums = value.length() / Byte.SIZE + (value.length() % Byte.SIZE == 0 ? 0 : 1);
|
||||||
|
byte[] bytes = new byte[nums];
|
||||||
|
for (int i = 0; i < nums; i++) {
|
||||||
|
int s = value.length() - Byte.SIZE < 0 ? 0 : value.length() - Byte.SIZE;
|
||||||
|
int e = value.length();
|
||||||
|
bytes[nums - i - 1] = (byte) Integer.parseInt(value.substring(s, e), 2);
|
||||||
|
value = value.substring(0, s);
|
||||||
|
}
|
||||||
|
return bytes;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Converts a string object for an expected JDBC type of {@link Types#BOOLEAN}.
|
||||||
|
*
|
||||||
|
* @param value the string object to be converted into a {@link Types#BOOLEAN} type;
|
||||||
|
* @return the converted value;
|
||||||
|
*/
|
||||||
|
private Object convertToBoolean(String value) {
|
||||||
|
try {
|
||||||
|
return Integer.parseInt(value) != 0;
|
||||||
|
} catch (NumberFormatException ignore) {
|
||||||
|
return Boolean.parseBoolean(value);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private DateTimeFormatter timestampFormat(int length) {
|
||||||
|
final DateTimeFormatterBuilder dtf =
|
||||||
|
new DateTimeFormatterBuilder()
|
||||||
|
.appendPattern("yyyy-MM-dd")
|
||||||
|
.optionalStart()
|
||||||
|
.appendLiteral(" ")
|
||||||
|
.append(DateTimeFormatter.ISO_LOCAL_TIME)
|
||||||
|
.optionalEnd()
|
||||||
|
.parseDefaulting(ChronoField.HOUR_OF_DAY, 0)
|
||||||
|
.parseDefaulting(ChronoField.MINUTE_OF_HOUR, 0)
|
||||||
|
.parseDefaulting(ChronoField.SECOND_OF_MINUTE, 0);
|
||||||
|
if (length > 0) {
|
||||||
|
dtf.appendFraction(ChronoField.MICRO_OF_SECOND, 0, length, true);
|
||||||
|
}
|
||||||
|
return dtf.toFormatter();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Clean input timestamp to yyyy-mm-dd hh:mm:ss[.fffffffff] format.
|
||||||
|
*
|
||||||
|
* @param s input timestamp
|
||||||
|
* @return cleaned timestamp
|
||||||
|
*/
|
||||||
|
private String cleanTimestamp(String s) {
|
||||||
|
if (s == null) {
|
||||||
|
throw new java.lang.IllegalArgumentException("null string");
|
||||||
|
}
|
||||||
|
|
||||||
|
s = s.trim();
|
||||||
|
|
||||||
|
// clean first dash
|
||||||
|
s = replaceFirstNonNumericSubstring(s, 0, '-');
|
||||||
|
// clean second dash
|
||||||
|
s = replaceFirstNonNumericSubstring(s, s.indexOf('-') + 1, '-');
|
||||||
|
// clean dividing space
|
||||||
|
s = replaceFirstNonNumericSubstring(s, s.indexOf('-', s.indexOf('-') + 1) + 1, ' ');
|
||||||
|
if (s.indexOf(' ') != -1) {
|
||||||
|
// clean first colon
|
||||||
|
s = replaceFirstNonNumericSubstring(s, s.indexOf(' ') + 1, ':');
|
||||||
|
if (s.indexOf(':') != -1) {
|
||||||
|
// clean second colon
|
||||||
|
s = replaceFirstNonNumericSubstring(s, s.indexOf(':') + 1, ':');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
final int maxMonth = 12;
|
||||||
|
final int maxDay = 31;
|
||||||
|
|
||||||
|
// Parse the date
|
||||||
|
int firstDash = s.indexOf('-');
|
||||||
|
int secondDash = s.indexOf('-', firstDash + 1);
|
||||||
|
int dividingSpace = s.indexOf(' ');
|
||||||
|
|
||||||
|
// Parse the time
|
||||||
|
int firstColon = s.indexOf(':', dividingSpace + 1);
|
||||||
|
int secondColon = s.indexOf(':', firstColon + 1);
|
||||||
|
int period = s.indexOf('.', secondColon + 1);
|
||||||
|
|
||||||
|
int year = 0;
|
||||||
|
int month = 0;
|
||||||
|
int day = 0;
|
||||||
|
int hour = 0;
|
||||||
|
int minute = 0;
|
||||||
|
int second = 0;
|
||||||
|
|
||||||
|
// Get the date
|
||||||
|
int len = s.length();
|
||||||
|
boolean parsedDate = false;
|
||||||
|
if (firstDash > 0 && secondDash > firstDash) {
|
||||||
|
year = Integer.parseInt(s.substring(0, firstDash));
|
||||||
|
month = Integer.parseInt(s.substring(firstDash + 1, secondDash));
|
||||||
|
if (dividingSpace != -1) {
|
||||||
|
day = Integer.parseInt(s.substring(secondDash + 1, dividingSpace));
|
||||||
|
} else {
|
||||||
|
day = Integer.parseInt(s.substring(secondDash + 1, len));
|
||||||
|
}
|
||||||
|
|
||||||
|
if ((month >= 1 && month <= maxMonth) && (day >= 1 && day <= maxDay)) {
|
||||||
|
parsedDate = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (!parsedDate) {
|
||||||
|
throw new java.lang.IllegalArgumentException("Cannot parse the date from " + s);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get the time. Hour, minute, second and colons are all optional
|
||||||
|
if (dividingSpace != -1 && dividingSpace < len - 1) {
|
||||||
|
if (firstColon == -1) {
|
||||||
|
hour = Integer.parseInt(s.substring(dividingSpace + 1, len));
|
||||||
|
} else {
|
||||||
|
hour = Integer.parseInt(s.substring(dividingSpace + 1, firstColon));
|
||||||
|
if (firstColon < len - 1) {
|
||||||
|
if (secondColon == -1) {
|
||||||
|
minute = Integer.parseInt(s.substring(firstColon + 1, len));
|
||||||
|
} else {
|
||||||
|
minute = Integer.parseInt(s.substring(firstColon + 1, secondColon));
|
||||||
|
if (secondColon < len - 1) {
|
||||||
|
if (period == -1) {
|
||||||
|
second = Integer.parseInt(s.substring(secondColon + 1, len));
|
||||||
|
} else {
|
||||||
|
second = Integer.parseInt(s.substring(secondColon + 1, period));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
StringBuilder cleanedTimestamp = new StringBuilder();
|
||||||
|
cleanedTimestamp =
|
||||||
|
cleanedTimestamp.append(
|
||||||
|
String.format(
|
||||||
|
"%04d-%02d-%02d %02d:%02d:%02d",
|
||||||
|
year, month, day, hour, minute, second));
|
||||||
|
|
||||||
|
if (period != -1 && period < len - 1) {
|
||||||
|
cleanedTimestamp = cleanedTimestamp.append(".").append(s.substring(period + 1));
|
||||||
|
}
|
||||||
|
|
||||||
|
return cleanedTimestamp.toString();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Replace the first non-numeric substring.
|
||||||
|
*
|
||||||
|
* @param s the original string
|
||||||
|
* @param startIndex the beginning index, inclusive
|
||||||
|
* @param c the new character
|
||||||
|
* @return
|
||||||
|
*/
|
||||||
|
private String replaceFirstNonNumericSubstring(String s, int startIndex, char c) {
|
||||||
|
StringBuilder sb = new StringBuilder();
|
||||||
|
sb.append(s.substring(0, startIndex));
|
||||||
|
|
||||||
|
String rest = s.substring(startIndex);
|
||||||
|
sb.append(rest.replaceFirst("[^\\d]+", Character.toString(c)));
|
||||||
|
return sb.toString();
|
||||||
|
}
|
||||||
|
|
||||||
|
private String stripCharacterSetIntroducer(String value) {
|
||||||
|
final Matcher matcher = CHARSET_INTRODUCER_PATTERN.matcher(value);
|
||||||
|
return !matcher.matches() ? value : matcher.group(1);
|
||||||
|
}
|
||||||
|
}
|
Loading…
Reference in New Issue