mirror of https://github.com/apache/nifi.git
NIFI-8161 Migrated EL from SimpleDateFormat to DateTimeFormatter
This closes #7169 Signed-off-by: David Handermann <exceptionfactory@apache.org>
This commit is contained in:
parent
284c11fc87
commit
8a4c3cab84
|
@ -171,7 +171,7 @@ GREATER_THAN : 'gt';
|
|||
LESS_THAN : 'lt';
|
||||
GREATER_THAN_OR_EQUAL : 'ge';
|
||||
LESS_THAN_OR_EQUAL : 'le';
|
||||
FORMAT : 'format'; // takes string date format; uses SimpleDateFormat
|
||||
FORMAT : 'format'; // takes string date format; uses DateTimeFormatter
|
||||
FORMAT_INSTANT : 'formatInstant';
|
||||
TO_DATE : 'toDate'; // takes string date format; converts the subject to a Long based on the date format
|
||||
TO_INSTANT : 'toInstant';
|
||||
|
|
|
@ -26,21 +26,25 @@ import org.apache.nifi.attribute.expression.language.evaluation.StringQueryResul
|
|||
import org.apache.nifi.attribute.expression.language.exception.AttributeExpressionLanguageException;
|
||||
import org.apache.nifi.attribute.expression.language.exception.AttributeExpressionLanguageParsingException;
|
||||
import org.apache.nifi.expression.AttributeExpression.ResultType;
|
||||
import org.apache.nifi.util.FormatUtils;
|
||||
|
||||
import java.text.ParseException;
|
||||
import java.text.SimpleDateFormat;
|
||||
import java.time.Instant;
|
||||
import java.time.format.DateTimeFormatter;
|
||||
import java.time.format.DateTimeParseException;
|
||||
import java.util.Date;
|
||||
import java.util.Locale;
|
||||
import java.util.regex.Matcher;
|
||||
import java.util.regex.Pattern;
|
||||
|
||||
public class DateCastEvaluator extends DateEvaluator {
|
||||
|
||||
public static final String DATE_TO_STRING_FORMAT = "EEE MMM dd HH:mm:ss zzz yyyy";
|
||||
public static final DateTimeFormatter DATE_TO_STRING_FORMATTER = FormatUtils.prepareLenientCaseInsensitiveDateTimeFormatter(DATE_TO_STRING_FORMAT);
|
||||
public static final Pattern DATE_TO_STRING_PATTERN = Pattern.compile("(?:[a-zA-Z]{3} ){2}\\d{2} \\d{2}\\:\\d{2}\\:\\d{2} (?:.*?) \\d{4}");
|
||||
|
||||
public static final String ALTERNATE_FORMAT_WITHOUT_MILLIS = "yyyy/MM/dd HH:mm:ss";
|
||||
public static final String ALTERNATE_FORMAT_WITH_MILLIS = "yyyy/MM/dd HH:mm:ss.SSS";
|
||||
public static final DateTimeFormatter ALTERNATE_FORMATTER_WITHOUT_MILLIS = FormatUtils.prepareLenientCaseInsensitiveDateTimeFormatter(ALTERNATE_FORMAT_WITHOUT_MILLIS);
|
||||
public static final DateTimeFormatter ALTERNATE_FORMATTER_WITH_MILLIS = FormatUtils.prepareLenientCaseInsensitiveDateTimeFormatter(ALTERNATE_FORMAT_WITH_MILLIS);
|
||||
public static final Pattern ALTERNATE_PATTERN = Pattern.compile("\\d{4}/\\d{2}/\\d{2} \\d{2}\\:\\d{2}\\:\\d{2}(\\.\\d{3})?");
|
||||
|
||||
public static final Pattern NUMBER_PATTERN = Pattern.compile("\\d+");
|
||||
|
@ -68,12 +72,10 @@ public class DateCastEvaluator extends DateEvaluator {
|
|||
case STRING:
|
||||
final String value = ((StringQueryResult) result).getValue().trim();
|
||||
if (DATE_TO_STRING_PATTERN.matcher(value).matches()) {
|
||||
final SimpleDateFormat sdf = new SimpleDateFormat(DATE_TO_STRING_FORMAT, Locale.US);
|
||||
|
||||
try {
|
||||
final Date date = sdf.parse(value);
|
||||
final Date date = Date.from(DATE_TO_STRING_FORMATTER.parse(value, Instant::from));
|
||||
return new DateQueryResult(date);
|
||||
} catch (final ParseException pe) {
|
||||
} catch (final DateTimeParseException pe) {
|
||||
final String details = "Format: '" + DATE_TO_STRING_FORMAT + "' Value: '" + value + "'";
|
||||
throw new AttributeExpressionLanguageException("Could not parse date using " + details, pe);
|
||||
}
|
||||
|
@ -84,19 +86,17 @@ public class DateCastEvaluator extends DateEvaluator {
|
|||
if (altMatcher.matches()) {
|
||||
final String millisValue = altMatcher.group(1);
|
||||
|
||||
final String format;
|
||||
final DateTimeFormatter formatter;
|
||||
if (millisValue == null) {
|
||||
format = ALTERNATE_FORMAT_WITHOUT_MILLIS;
|
||||
formatter = ALTERNATE_FORMATTER_WITHOUT_MILLIS;
|
||||
} else {
|
||||
format = ALTERNATE_FORMAT_WITH_MILLIS;
|
||||
formatter = ALTERNATE_FORMATTER_WITH_MILLIS;
|
||||
}
|
||||
|
||||
final SimpleDateFormat sdf = new SimpleDateFormat(format, Locale.US);
|
||||
|
||||
try {
|
||||
final Date date = sdf.parse(value);
|
||||
final Date date = Date.from(FormatUtils.parseToInstant(formatter, value));
|
||||
return new DateQueryResult(date);
|
||||
} catch (final ParseException pe) {
|
||||
} catch (final DateTimeParseException pe) {
|
||||
throw new AttributeExpressionLanguageException("Could not parse input as date", pe);
|
||||
}
|
||||
} else {
|
||||
|
|
|
@ -17,16 +17,20 @@
|
|||
package org.apache.nifi.attribute.expression.language.evaluation.functions;
|
||||
|
||||
import org.apache.nifi.attribute.expression.language.EvaluationContext;
|
||||
import org.apache.nifi.attribute.expression.language.StandardEvaluationContext;
|
||||
import org.apache.nifi.attribute.expression.language.evaluation.DateEvaluator;
|
||||
import org.apache.nifi.attribute.expression.language.evaluation.Evaluator;
|
||||
import org.apache.nifi.attribute.expression.language.evaluation.QueryResult;
|
||||
import org.apache.nifi.attribute.expression.language.evaluation.StringEvaluator;
|
||||
import org.apache.nifi.attribute.expression.language.evaluation.StringQueryResult;
|
||||
import org.apache.nifi.attribute.expression.language.evaluation.literals.StringLiteralEvaluator;
|
||||
|
||||
import java.text.SimpleDateFormat;
|
||||
import java.time.ZoneId;
|
||||
import java.time.ZonedDateTime;
|
||||
import java.time.format.DateTimeFormatter;
|
||||
import java.util.Collections;
|
||||
import java.util.Date;
|
||||
import java.util.Locale;
|
||||
import java.util.TimeZone;
|
||||
|
||||
public class FormatEvaluator extends StringEvaluator {
|
||||
|
||||
|
@ -34,9 +38,31 @@ public class FormatEvaluator extends StringEvaluator {
|
|||
private final Evaluator<String> format;
|
||||
private final Evaluator<String> timeZone;
|
||||
|
||||
private final DateTimeFormatter preparedFormatter;
|
||||
private final boolean preparedFormatterHasRequestedTimeZone;
|
||||
|
||||
public FormatEvaluator(final DateEvaluator subject, final Evaluator<String> format, final Evaluator<String> timeZone) {
|
||||
this.subject = subject;
|
||||
this.format = format;
|
||||
// if the search string is a literal, we don't need to prepare formatter each time; we can just
|
||||
// prepare it once. Otherwise, it must be prepared for each time.
|
||||
if (format instanceof StringLiteralEvaluator) {
|
||||
String formatPattern = format.evaluate(new StandardEvaluationContext(Collections.emptyMap())).getValue();
|
||||
DateTimeFormatter dtf = DateTimeFormatter.ofPattern(formatPattern, Locale.US);
|
||||
if (timeZone == null) {
|
||||
preparedFormatter = dtf;
|
||||
preparedFormatterHasRequestedTimeZone = true;
|
||||
} else if (timeZone instanceof StringLiteralEvaluator) {
|
||||
preparedFormatter = dtf.withZone(ZoneId.of(timeZone.evaluate(new StandardEvaluationContext(Collections.emptyMap())).getValue()));
|
||||
preparedFormatterHasRequestedTimeZone = true;
|
||||
} else {
|
||||
preparedFormatter = dtf;
|
||||
preparedFormatterHasRequestedTimeZone = false;
|
||||
}
|
||||
} else {
|
||||
preparedFormatter = null;
|
||||
preparedFormatterHasRequestedTimeZone = false;
|
||||
}
|
||||
this.timeZone = timeZone;
|
||||
}
|
||||
|
||||
|
@ -47,23 +73,28 @@ public class FormatEvaluator extends StringEvaluator {
|
|||
return new StringQueryResult(null);
|
||||
}
|
||||
|
||||
final QueryResult<String> formatResult = format.evaluate(evaluationContext);
|
||||
final String format = formatResult.getValue();
|
||||
if (format == null) {
|
||||
return null;
|
||||
DateTimeFormatter dtf;
|
||||
if (preparedFormatter == null) {
|
||||
final QueryResult<String> formatResult = format.evaluate(evaluationContext);
|
||||
final String format = formatResult.getValue();
|
||||
if (format == null) {
|
||||
return null;
|
||||
}
|
||||
dtf = DateTimeFormatter.ofPattern(format, Locale.US);
|
||||
} else {
|
||||
dtf = preparedFormatter;
|
||||
}
|
||||
|
||||
final SimpleDateFormat sdf = new SimpleDateFormat(format, Locale.US);
|
||||
|
||||
if(timeZone != null) {
|
||||
if ((preparedFormatter == null || !preparedFormatterHasRequestedTimeZone) && timeZone != null) {
|
||||
final QueryResult<String> tzResult = timeZone.evaluate(evaluationContext);
|
||||
final String tz = tzResult.getValue();
|
||||
if(tz != null && TimeZone.getTimeZone(tz) != null) {
|
||||
sdf.setTimeZone(TimeZone.getTimeZone(tz));
|
||||
if (tz != null) {
|
||||
dtf = dtf.withZone(ZoneId.of(tz));
|
||||
}
|
||||
}
|
||||
|
||||
return new StringQueryResult(sdf.format(subjectValue));
|
||||
ZonedDateTime subjectDateTime = subjectValue.toInstant().atZone(ZoneId.systemDefault());
|
||||
return new StringQueryResult(dtf.format(subjectDateTime));
|
||||
}
|
||||
|
||||
@Override
|
||||
|
|
|
@ -17,17 +17,20 @@
|
|||
package org.apache.nifi.attribute.expression.language.evaluation.functions;
|
||||
|
||||
import org.apache.nifi.attribute.expression.language.EvaluationContext;
|
||||
import org.apache.nifi.attribute.expression.language.StandardEvaluationContext;
|
||||
import org.apache.nifi.attribute.expression.language.evaluation.DateEvaluator;
|
||||
import org.apache.nifi.attribute.expression.language.evaluation.DateQueryResult;
|
||||
import org.apache.nifi.attribute.expression.language.evaluation.Evaluator;
|
||||
import org.apache.nifi.attribute.expression.language.evaluation.QueryResult;
|
||||
import org.apache.nifi.attribute.expression.language.evaluation.literals.StringLiteralEvaluator;
|
||||
import org.apache.nifi.attribute.expression.language.exception.IllegalAttributeException;
|
||||
import org.apache.nifi.util.FormatUtils;
|
||||
|
||||
import java.text.ParseException;
|
||||
import java.text.SimpleDateFormat;
|
||||
import java.time.ZoneId;
|
||||
import java.time.format.DateTimeFormatter;
|
||||
import java.time.format.DateTimeParseException;
|
||||
import java.util.Collections;
|
||||
import java.util.Date;
|
||||
import java.util.Locale;
|
||||
import java.util.TimeZone;
|
||||
|
||||
public class StringToDateEvaluator extends DateEvaluator {
|
||||
|
||||
|
@ -35,9 +38,31 @@ public class StringToDateEvaluator extends DateEvaluator {
|
|||
private final Evaluator<String> format;
|
||||
private final Evaluator<String> timeZone;
|
||||
|
||||
private final DateTimeFormatter preparedFormatter;
|
||||
private final boolean preparedFormatterHasRequestedTimeZone;
|
||||
|
||||
public StringToDateEvaluator(final Evaluator<String> subject, final Evaluator<String> format, final Evaluator<String> timeZone) {
|
||||
this.subject = subject;
|
||||
this.format = format;
|
||||
// if the search string is a literal, we don't need to prepare formatter each time; we can just
|
||||
// prepare it once. Otherwise, it must be prepared for each time.
|
||||
if (format instanceof StringLiteralEvaluator) {
|
||||
String evaluatedFormat = format.evaluate(new StandardEvaluationContext(Collections.emptyMap())).getValue();
|
||||
DateTimeFormatter dtf = FormatUtils.prepareLenientCaseInsensitiveDateTimeFormatter(evaluatedFormat);
|
||||
if (timeZone == null) {
|
||||
preparedFormatter = dtf;
|
||||
preparedFormatterHasRequestedTimeZone = true;
|
||||
} else if (timeZone instanceof StringLiteralEvaluator) {
|
||||
preparedFormatter = dtf.withZone(ZoneId.of(timeZone.evaluate(new StandardEvaluationContext(Collections.emptyMap())).getValue()));
|
||||
preparedFormatterHasRequestedTimeZone = true;
|
||||
} else {
|
||||
preparedFormatter = dtf;
|
||||
preparedFormatterHasRequestedTimeZone = false;
|
||||
}
|
||||
} else {
|
||||
preparedFormatter = null;
|
||||
preparedFormatterHasRequestedTimeZone = false;
|
||||
}
|
||||
this.timeZone = timeZone;
|
||||
}
|
||||
|
||||
|
@ -49,21 +74,31 @@ public class StringToDateEvaluator extends DateEvaluator {
|
|||
return new DateQueryResult(null);
|
||||
}
|
||||
|
||||
final SimpleDateFormat sdf = new SimpleDateFormat(formatValue, Locale.US);
|
||||
DateTimeFormatter dtf;
|
||||
if (preparedFormatter != null) {
|
||||
dtf = preparedFormatter;
|
||||
} else {
|
||||
final QueryResult<String> formatResult = format.evaluate(evaluationContext);
|
||||
final String format = formatResult.getValue();
|
||||
if (format == null) {
|
||||
return null;
|
||||
}
|
||||
dtf = FormatUtils.prepareLenientCaseInsensitiveDateTimeFormatter(format);
|
||||
}
|
||||
|
||||
if(timeZone != null) {
|
||||
if ((preparedFormatter == null || !preparedFormatterHasRequestedTimeZone) && timeZone != null) {
|
||||
final QueryResult<String> tzResult = timeZone.evaluate(evaluationContext);
|
||||
final String tz = tzResult.getValue();
|
||||
if(tz != null && TimeZone.getTimeZone(tz) != null) {
|
||||
sdf.setTimeZone(TimeZone.getTimeZone(tz));
|
||||
if(tz != null) {
|
||||
dtf = dtf.withZone(ZoneId.of(tz));
|
||||
}
|
||||
}
|
||||
|
||||
try {
|
||||
return new DateQueryResult(sdf.parse(subjectValue));
|
||||
} catch (final ParseException e) {
|
||||
return new DateQueryResult(Date.from(FormatUtils.parseToInstant(dtf, subjectValue)));
|
||||
} catch (final DateTimeParseException e) {
|
||||
throw new IllegalAttributeException("Cannot parse attribute value as a date; date format: "
|
||||
+ formatValue + "; attribute value: " + subjectValue);
|
||||
+ formatValue + "; attribute value: " + subjectValue + ". Error: " + e.getMessage());
|
||||
} catch (final IllegalArgumentException e) {
|
||||
throw new IllegalAttributeException("Invalid date format: " + formatValue);
|
||||
}
|
||||
|
|
|
@ -37,7 +37,6 @@ import java.io.BufferedInputStream;
|
|||
import java.io.IOException;
|
||||
import java.io.InputStreamReader;
|
||||
import java.io.Reader;
|
||||
import java.text.SimpleDateFormat;
|
||||
import java.time.Instant;
|
||||
import java.time.ZoneId;
|
||||
import java.time.format.DateTimeFormatter;
|
||||
|
@ -52,6 +51,7 @@ import java.util.List;
|
|||
import java.util.Locale;
|
||||
import java.util.Map;
|
||||
import java.util.Optional;
|
||||
import java.util.TimeZone;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
|
||||
import static java.lang.Double.NEGATIVE_INFINITY;
|
||||
|
@ -387,10 +387,9 @@ public class TestQuery {
|
|||
|
||||
// the date.toString() above will end up truncating the milliseconds. So remove millis from the Date before
|
||||
// formatting it
|
||||
final SimpleDateFormat sdf = new SimpleDateFormat("yyyy/MM/dd HH:mm:ss.SSS", Locale.US);
|
||||
final long millis = date.getTime() % 1000L;
|
||||
final Date roundedToNearestSecond = new Date(date.getTime() - millis);
|
||||
final String formatted = sdf.format(roundedToNearestSecond);
|
||||
final DateTimeFormatter dtf = DateTimeFormatter.ofPattern("yyyy/MM/dd HH:mm:ss.SSS", Locale.US);
|
||||
final Instant truncatedSecond = date.toInstant().truncatedTo(ChronoUnit.SECONDS);
|
||||
final String formatted = dtf.format(truncatedSecond);
|
||||
|
||||
final QueryResult<?> result = query.evaluate(new StandardEvaluationContext(attributes));
|
||||
assertEquals(ResultType.STRING, result.getResultType());
|
||||
|
@ -777,7 +776,7 @@ public class TestQuery {
|
|||
@Test
|
||||
public void testProblematic1() {
|
||||
// There was a bug that prevented this expression from compiling. This test just verifies that it now compiles.
|
||||
final String queryString = "${xx:append( \"120101\" ):toDate( 'yyMMddHHmmss' ):format( \"yy-MM-dd’T’HH:mm:ss\") }";
|
||||
final String queryString = "${xx:append( \"120101\" ):toDate( 'yyMMddHHmmss' ):format( \"yy-MM-dd'T'HH:mm:ss\") }";
|
||||
Query.compile(queryString);
|
||||
}
|
||||
|
||||
|
@ -944,11 +943,18 @@ public class TestQuery {
|
|||
final String format = "yyyy-MM-dd'T'HH:mm:ss.SSS'Z'";
|
||||
|
||||
final String query = "startDateTime=\"${date:toNumber():toDate():format(\"" + format + "\")}\"";
|
||||
final String result = Query.evaluateExpressions(query, attributes, null);
|
||||
|
||||
final String expectedTime = new SimpleDateFormat(format, Locale.US).format(timestamp);
|
||||
assertEquals("startDateTime=\"" + expectedTime + "\"", result);
|
||||
TimeZone current = TimeZone.getDefault();
|
||||
TimeZone defaultTimeZone = TimeZone.getTimeZone("Europe/Kiev");
|
||||
TimeZone.setDefault(defaultTimeZone);
|
||||
try {
|
||||
final String result = Query.evaluateExpressions(query, attributes, null);
|
||||
|
||||
final String expectedTime = DateTimeFormatter.ofPattern(format, Locale.US).format(Instant.ofEpochMilli(timestamp).atZone(defaultTimeZone.toZoneId()));
|
||||
assertEquals("startDateTime=\"" + expectedTime + "\"", result);
|
||||
} finally {
|
||||
TimeZone.setDefault(current);
|
||||
}
|
||||
final List<Range> ranges = Query.extractExpressionRanges(query);
|
||||
assertEquals(1, ranges.size());
|
||||
}
|
||||
|
@ -973,6 +979,28 @@ public class TestQuery {
|
|||
assertEquals(1, ranges.size());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testFormatUsesLocalTimeZoneUnlessIsSpecified() {
|
||||
TimeZone current = TimeZone.getDefault();
|
||||
TimeZone.setDefault(TimeZone.getTimeZone("Europe/Kiev"));
|
||||
try {
|
||||
final String formatWithZoneInvocation = "format(\"yyyy-MM-dd HH:mm:ss\", \"GMT\")";
|
||||
assertEquals("2020-01-01 00:00:00", evaluateFormatDate("2020-01-01 00:00:00", formatWithZoneInvocation));
|
||||
|
||||
final String formatWithoutZoneInvocation = "format(\"yyyy-MM-dd HH:mm:ss\")";
|
||||
assertEquals("2020-02-01 02:00:00", evaluateFormatDate("2020-02-01 00:00:00", formatWithoutZoneInvocation));
|
||||
} finally {
|
||||
TimeZone.setDefault(current);
|
||||
}
|
||||
}
|
||||
|
||||
private String evaluateFormatDate(String givenDateStringInGMT, String formatInvocation) {
|
||||
final Map<String, String> attributes = new HashMap<>();
|
||||
attributes.put("date", String.valueOf(givenDateStringInGMT));
|
||||
final String query = "${date:toDate(\"yyyy-MM-dd HH:mm:ss\", \"GMT\"):" + formatInvocation + "}";
|
||||
return Query.evaluateExpressions(query, attributes, null);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testDateConversion() {
|
||||
final Map<String, String> attributes = new HashMap<>();
|
||||
|
@ -1935,6 +1963,13 @@ public class TestQuery {
|
|||
verifyEquals("${blue:toDate('yyyyMMddHHmmss'):format(\"yyyy/MM/dd HH:mm:ss.SSS'Z'\")}", attributes, "2013/09/17 16:26:43.000Z");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testDateFormatConversionIsCaseInsensitive() {
|
||||
final Map<String, String> attributes = new HashMap<>();
|
||||
attributes.put("blue", "10may2004");
|
||||
verifyEquals("${blue:toDate('ddMMMyyyy'):format('yyyy/MM/dd')}", attributes, "2004/05/10");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testDateFormatConversionWithTimeZone() {
|
||||
final Map<String, String> attributes = new HashMap<>();
|
||||
|
|
|
@ -17,8 +17,17 @@
|
|||
package org.apache.nifi.util;
|
||||
|
||||
import java.text.NumberFormat;
|
||||
import java.time.Instant;
|
||||
import java.time.LocalDate;
|
||||
import java.time.LocalDateTime;
|
||||
import java.time.LocalTime;
|
||||
import java.time.ZoneId;
|
||||
import java.time.format.DateTimeFormatter;
|
||||
import java.time.format.DateTimeFormatterBuilder;
|
||||
import java.time.temporal.TemporalAccessor;
|
||||
import java.util.Arrays;
|
||||
import java.util.List;
|
||||
import java.util.Locale;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
import java.util.regex.Matcher;
|
||||
import java.util.regex.Pattern;
|
||||
|
@ -46,6 +55,8 @@ public class FormatUtils {
|
|||
public static final Pattern TIME_DURATION_PATTERN = Pattern.compile(TIME_DURATION_REGEX);
|
||||
private static final List<Long> TIME_UNIT_MULTIPLIERS = Arrays.asList(1000L, 1000L, 1000L, 60L, 60L, 24L);
|
||||
|
||||
private static final LocalDate EPOCH_INITIAL_DATE = LocalDate.of(1970, 1, 1);
|
||||
|
||||
/**
|
||||
* Formats the specified count by adding commas.
|
||||
*
|
||||
|
@ -425,4 +436,40 @@ public class FormatUtils {
|
|||
|
||||
return sb.toString();
|
||||
}
|
||||
|
||||
public static DateTimeFormatter prepareLenientCaseInsensitiveDateTimeFormatter(String pattern) {
|
||||
return new DateTimeFormatterBuilder()
|
||||
.parseLenient()
|
||||
.parseCaseInsensitive()
|
||||
.appendPattern(pattern)
|
||||
.toFormatter(Locale.US);
|
||||
}
|
||||
|
||||
/**
|
||||
* Parse text to Instant - support different formats like: zoned date time, date time, date, time (similar to those supported in SimpleDateFormat)
|
||||
* @param formatter configured formatter
|
||||
* @param text text which will be parsed
|
||||
* @return parsed Instant
|
||||
*/
|
||||
public static Instant parseToInstant(DateTimeFormatter formatter, String text) {
|
||||
if (text == null) {
|
||||
throw new IllegalArgumentException("Text cannot be null");
|
||||
}
|
||||
|
||||
TemporalAccessor parsed = formatter.parseBest(text, Instant::from, LocalDateTime::from, LocalDate::from, LocalTime::from);
|
||||
if (parsed instanceof Instant) {
|
||||
return (Instant) parsed;
|
||||
} else if (parsed instanceof LocalDateTime) {
|
||||
return toInstantInSystemDefaultTimeZone((LocalDateTime) parsed);
|
||||
} else if (parsed instanceof LocalDate) {
|
||||
return toInstantInSystemDefaultTimeZone(((LocalDate) parsed).atTime(0, 0));
|
||||
} else {
|
||||
return toInstantInSystemDefaultTimeZone(((LocalTime) parsed).atDate(EPOCH_INITIAL_DATE));
|
||||
}
|
||||
}
|
||||
|
||||
private static Instant toInstantInSystemDefaultTimeZone(LocalDateTime dateTime) {
|
||||
return dateTime.atZone(ZoneId.systemDefault()).toInstant();
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -20,12 +20,23 @@ import org.apache.nifi.util.FormatUtils;
|
|||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import java.text.DecimalFormatSymbols;
|
||||
import java.text.SimpleDateFormat;
|
||||
import java.time.Instant;
|
||||
import java.time.LocalDateTime;
|
||||
import java.time.ZoneId;
|
||||
import java.time.ZoneOffset;
|
||||
import java.time.format.DateTimeFormatter;
|
||||
import java.util.Locale;
|
||||
import java.util.TimeZone;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||
|
||||
public class TestFormatUtils {
|
||||
|
||||
private static String NEW_YORK_TIME_ZONE_ID = "America/New_York";
|
||||
private static String KIEV_TIME_ZONE_ID = "Europe/Kiev";
|
||||
|
||||
@Test
|
||||
public void testParse() {
|
||||
assertEquals(3, FormatUtils.getTimeDuration("3000 ms", TimeUnit.SECONDS));
|
||||
|
@ -103,4 +114,74 @@ public class TestFormatUtils {
|
|||
|
||||
assertEquals(String.format("181%s9 TB", decimalFormatSymbols.getDecimalSeparator()), FormatUtils.formatDataSize(200_000_000_000_000d));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testParseToInstantUsingFormatterWithoutZones() throws Exception {
|
||||
// GMT-
|
||||
checkSameResultsWithSimpleDateFormat("yyyy-MM-dd HH:mm:ss", "2020-01-01 02:00:00", NEW_YORK_TIME_ZONE_ID, null, "2020-01-01T07:00:00");
|
||||
checkSameResultsWithSimpleDateFormat("yyyy-MM-dd", "2020-01-01", NEW_YORK_TIME_ZONE_ID, null, "2020-01-01T05:00:00");
|
||||
checkSameResultsWithSimpleDateFormat("HH:mm:ss", "03:00:00", NEW_YORK_TIME_ZONE_ID, null, "1970-01-01T08:00:00");
|
||||
checkSameResultsWithSimpleDateFormat("yyyy-MMM-dd", "2020-may-01", NEW_YORK_TIME_ZONE_ID, null, "2020-05-01T04:00:00");
|
||||
|
||||
// GMT+
|
||||
checkSameResultsWithSimpleDateFormat("yyyy-MM-dd HH:mm:ss", "2020-01-01 02:00:00", KIEV_TIME_ZONE_ID, null, "2020-01-01T00:00:00");
|
||||
checkSameResultsWithSimpleDateFormat("yyyy-MM-dd", "2020-01-01", KIEV_TIME_ZONE_ID, null, "2019-12-31T22:00:00");
|
||||
checkSameResultsWithSimpleDateFormat("HH:mm:ss", "03:00:00", KIEV_TIME_ZONE_ID, null, "1970-01-01T00:00:00");
|
||||
checkSameResultsWithSimpleDateFormat("yyyy-MMM-dd", "2020-may-01", KIEV_TIME_ZONE_ID, null, "2020-04-30T21:00:00");
|
||||
|
||||
// UTC
|
||||
checkSameResultsWithSimpleDateFormat("yyyy-MM-dd HH:mm:ss", "2020-01-01 02:00:00", ZoneOffset.UTC.getId(), null, "2020-01-01T02:00:00");
|
||||
checkSameResultsWithSimpleDateFormat("yyyy-MM-dd", "2020-01-01", ZoneOffset.UTC.getId(), null, "2020-01-01T00:00:00");
|
||||
checkSameResultsWithSimpleDateFormat("HH:mm:ss", "03:00:00", ZoneOffset.UTC.getId(), null, "1970-01-01T03:00:00");
|
||||
checkSameResultsWithSimpleDateFormat("yyyy-MMM-dd", "2020-may-01", ZoneOffset.UTC.getId(), null, "2020-05-01T00:00:00");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testParseToInstantUsingFormatterWithZone() throws Exception {
|
||||
for (String systemDefaultZoneId : new String[]{ NEW_YORK_TIME_ZONE_ID, KIEV_TIME_ZONE_ID, ZoneOffset.UTC.getId()}) {
|
||||
checkSameResultsWithSimpleDateFormat("yyyy-MM-dd HH:mm:ss", "2020-01-01 02:00:00", systemDefaultZoneId, NEW_YORK_TIME_ZONE_ID, "2020-01-01T07:00:00");
|
||||
checkSameResultsWithSimpleDateFormat("yyyy-MM-dd HH:mm:ss", "2020-01-01 02:00:00", systemDefaultZoneId, KIEV_TIME_ZONE_ID, "2020-01-01T00:00:00");
|
||||
checkSameResultsWithSimpleDateFormat("yyyy-MM-dd HH:mm:ss", "2020-01-01 02:00:00", systemDefaultZoneId, ZoneOffset.UTC.getId(), "2020-01-01T02:00:00");
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testParseToInstantWithZonePassedInText() throws Exception {
|
||||
for (String systemDefaultZoneId : new String[]{ NEW_YORK_TIME_ZONE_ID, KIEV_TIME_ZONE_ID, ZoneOffset.UTC.getId()}) {
|
||||
checkSameResultsWithSimpleDateFormat("yyyy-MM-dd HH:mm:ss Z", "2020-01-01 02:00:00 -0100", systemDefaultZoneId, null, "2020-01-01T03:00:00");
|
||||
checkSameResultsWithSimpleDateFormat("yyyy-MM-dd HH:mm:ss Z", "2020-01-01 02:00:00 +0100", systemDefaultZoneId, null, "2020-01-01T01:00:00");
|
||||
checkSameResultsWithSimpleDateFormat("yyyy-MM-dd HH:mm:ss Z", "2020-01-01 02:00:00 +0000", systemDefaultZoneId, null, "2020-01-01T02:00:00");
|
||||
}
|
||||
}
|
||||
|
||||
private void checkSameResultsWithSimpleDateFormat(String pattern, String parsedDateTime, String systemDefaultZoneId, String formatZoneId, String expectedUtcDateTime) throws Exception {
|
||||
TimeZone current = TimeZone.getDefault();
|
||||
TimeZone.setDefault(TimeZone.getTimeZone(systemDefaultZoneId));
|
||||
try {
|
||||
checkSameResultsWithSimpleDateFormat(pattern, parsedDateTime, formatZoneId, expectedUtcDateTime);
|
||||
} finally {
|
||||
TimeZone.setDefault(current);
|
||||
}
|
||||
}
|
||||
|
||||
private void checkSameResultsWithSimpleDateFormat(String pattern, String parsedDateTime, String formatterZoneId, String expectedUtcDateTime) throws Exception {
|
||||
Instant expectedInstant = LocalDateTime.parse(expectedUtcDateTime).atZone(ZoneOffset.UTC).toInstant();
|
||||
|
||||
// reference implementation
|
||||
SimpleDateFormat sdf = new SimpleDateFormat(pattern, Locale.US);
|
||||
if (formatterZoneId != null) {
|
||||
sdf.setTimeZone(TimeZone.getTimeZone(formatterZoneId));
|
||||
}
|
||||
Instant simpleDateFormatResult = sdf.parse(parsedDateTime).toInstant();
|
||||
assertEquals(expectedInstant, simpleDateFormatResult);
|
||||
|
||||
// current implementation
|
||||
DateTimeFormatter dtf = FormatUtils.prepareLenientCaseInsensitiveDateTimeFormatter(pattern);
|
||||
if (formatterZoneId != null) {
|
||||
dtf = dtf.withZone(ZoneId.of(formatterZoneId));
|
||||
}
|
||||
Instant result = FormatUtils.parseToInstant(dtf, parsedDateTime);
|
||||
assertEquals(expectedInstant, result);
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -261,7 +261,7 @@ Language supports four different data types:
|
|||
- *Date*: A Date is an object that holds a Date and Time. Utilizing the <<dates>> and <<type_cast>> functions this data
|
||||
type can be converted to/from Strings and numbers. If the whole Expression Language expression is evaluated to be a
|
||||
date then it will be converted to a String with the format: "<Day of Week> <Month> <Day of Month> <Hour>:<Minute>:<Second> <Time Zone> <Year>".
|
||||
Also expressed as "E MMM dd HH:mm:ss z yyyy" in Java SimpleDateFormat format. For example: "Wed Dec 31 12:00:04 UTC 2016".
|
||||
Also expressed as "E MMM dd HH:mm:ss z yyyy" in Java DateTimeFormatter format. For example: "Wed Dec 31 12:00:04 UTC 2016".
|
||||
- *Boolean*: A Boolean is one of either `true` or `false`.
|
||||
|
||||
After evaluating expression language functions, all attributes are stored as type String.
|
||||
|
@ -2238,7 +2238,7 @@ In order to run the correct method, the parameter types must be correct. The Exp
|
|||
=== format
|
||||
|
||||
*Description*: [.description]#Formats a number as a date/time according to the format specified by the argument. The argument
|
||||
must be a String that is a valid Java SimpleDateFormat format. The Subject is expected to be a Number that
|
||||
must be a String that is a valid Java DateTimeFormatter format. The Subject is expected to be a Number that
|
||||
represents the number of milliseconds since Midnight GMT on January 1, 1970. The number will be evaluated using the local
|
||||
time zone unless specified in the second optional argument.#
|
||||
|
||||
|
@ -2246,7 +2246,7 @@ In order to run the correct method, the parameter types must be correct. The Exp
|
|||
|
||||
*Arguments*:
|
||||
|
||||
- [.argName]#_format_# : [.argDesc]#The format to use in the Java SimpleDateFormat syntax#
|
||||
- [.argName]#_format_# : [.argDesc]#The format to use in the Java DateTimeFormatter syntax#
|
||||
- [.argName]#_time zone_# : [.argDesc]#Optional argument that specifies the time zone to use (in the Java TimeZone syntax)#
|
||||
|
||||
*Return Type*: [.returnType]#String#
|
||||
|
@ -2308,14 +2308,14 @@ then the following Expressions will yield the following results:
|
|||
=== toDate
|
||||
|
||||
*Description*: [.description]#Converts a String into a Date data type, based on the format specified by the argument. The argument
|
||||
must be a String that is a valid Java SimpleDateFormat syntax. The Subject is expected to be a String that is formatted
|
||||
must be a String that is a valid Java DateTimeFormatter syntax. The Subject is expected to be a String that is formatted
|
||||
according the argument. The date will be evaluated using the local time zone unless specified in the second optional argument.#
|
||||
|
||||
*Subject Type*: [.subject]#String#
|
||||
|
||||
*Arguments*:
|
||||
|
||||
- [.argName]#_format_# : [.argDesc]#The current format to use when parsing the Subject, in the Java SimpleDateFormat syntax.#
|
||||
- [.argName]#_format_# : [.argDesc]#The current format to use when parsing the Subject, in the Java DateTimeFormatter syntax.#
|
||||
- [.argName]#_time zone_# : [.argDesc]#Optional argument that specifies the time zone to use when parsing the Subject, in the Java TimeZone syntax.#
|
||||
|
||||
|
||||
|
|
Loading…
Reference in New Issue