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';
|
LESS_THAN : 'lt';
|
||||||
GREATER_THAN_OR_EQUAL : 'ge';
|
GREATER_THAN_OR_EQUAL : 'ge';
|
||||||
LESS_THAN_OR_EQUAL : 'le';
|
LESS_THAN_OR_EQUAL : 'le';
|
||||||
FORMAT : 'format'; // takes string date format; uses SimpleDateFormat
|
FORMAT : 'format'; // takes string date format; uses DateTimeFormatter
|
||||||
FORMAT_INSTANT : 'formatInstant';
|
FORMAT_INSTANT : 'formatInstant';
|
||||||
TO_DATE : 'toDate'; // takes string date format; converts the subject to a Long based on the date format
|
TO_DATE : 'toDate'; // takes string date format; converts the subject to a Long based on the date format
|
||||||
TO_INSTANT : 'toInstant';
|
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.AttributeExpressionLanguageException;
|
||||||
import org.apache.nifi.attribute.expression.language.exception.AttributeExpressionLanguageParsingException;
|
import org.apache.nifi.attribute.expression.language.exception.AttributeExpressionLanguageParsingException;
|
||||||
import org.apache.nifi.expression.AttributeExpression.ResultType;
|
import org.apache.nifi.expression.AttributeExpression.ResultType;
|
||||||
|
import org.apache.nifi.util.FormatUtils;
|
||||||
|
|
||||||
import java.text.ParseException;
|
import java.time.Instant;
|
||||||
import java.text.SimpleDateFormat;
|
import java.time.format.DateTimeFormatter;
|
||||||
|
import java.time.format.DateTimeParseException;
|
||||||
import java.util.Date;
|
import java.util.Date;
|
||||||
import java.util.Locale;
|
|
||||||
import java.util.regex.Matcher;
|
import java.util.regex.Matcher;
|
||||||
import java.util.regex.Pattern;
|
import java.util.regex.Pattern;
|
||||||
|
|
||||||
public class DateCastEvaluator extends DateEvaluator {
|
public class DateCastEvaluator extends DateEvaluator {
|
||||||
|
|
||||||
public static final String DATE_TO_STRING_FORMAT = "EEE MMM dd HH:mm:ss zzz yyyy";
|
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 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_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 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 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+");
|
public static final Pattern NUMBER_PATTERN = Pattern.compile("\\d+");
|
||||||
|
@ -68,12 +72,10 @@ public class DateCastEvaluator extends DateEvaluator {
|
||||||
case STRING:
|
case STRING:
|
||||||
final String value = ((StringQueryResult) result).getValue().trim();
|
final String value = ((StringQueryResult) result).getValue().trim();
|
||||||
if (DATE_TO_STRING_PATTERN.matcher(value).matches()) {
|
if (DATE_TO_STRING_PATTERN.matcher(value).matches()) {
|
||||||
final SimpleDateFormat sdf = new SimpleDateFormat(DATE_TO_STRING_FORMAT, Locale.US);
|
|
||||||
|
|
||||||
try {
|
try {
|
||||||
final Date date = sdf.parse(value);
|
final Date date = Date.from(DATE_TO_STRING_FORMATTER.parse(value, Instant::from));
|
||||||
return new DateQueryResult(date);
|
return new DateQueryResult(date);
|
||||||
} catch (final ParseException pe) {
|
} catch (final DateTimeParseException pe) {
|
||||||
final String details = "Format: '" + DATE_TO_STRING_FORMAT + "' Value: '" + value + "'";
|
final String details = "Format: '" + DATE_TO_STRING_FORMAT + "' Value: '" + value + "'";
|
||||||
throw new AttributeExpressionLanguageException("Could not parse date using " + details, pe);
|
throw new AttributeExpressionLanguageException("Could not parse date using " + details, pe);
|
||||||
}
|
}
|
||||||
|
@ -84,19 +86,17 @@ public class DateCastEvaluator extends DateEvaluator {
|
||||||
if (altMatcher.matches()) {
|
if (altMatcher.matches()) {
|
||||||
final String millisValue = altMatcher.group(1);
|
final String millisValue = altMatcher.group(1);
|
||||||
|
|
||||||
final String format;
|
final DateTimeFormatter formatter;
|
||||||
if (millisValue == null) {
|
if (millisValue == null) {
|
||||||
format = ALTERNATE_FORMAT_WITHOUT_MILLIS;
|
formatter = ALTERNATE_FORMATTER_WITHOUT_MILLIS;
|
||||||
} else {
|
} else {
|
||||||
format = ALTERNATE_FORMAT_WITH_MILLIS;
|
formatter = ALTERNATE_FORMATTER_WITH_MILLIS;
|
||||||
}
|
}
|
||||||
|
|
||||||
final SimpleDateFormat sdf = new SimpleDateFormat(format, Locale.US);
|
|
||||||
|
|
||||||
try {
|
try {
|
||||||
final Date date = sdf.parse(value);
|
final Date date = Date.from(FormatUtils.parseToInstant(formatter, value));
|
||||||
return new DateQueryResult(date);
|
return new DateQueryResult(date);
|
||||||
} catch (final ParseException pe) {
|
} catch (final DateTimeParseException pe) {
|
||||||
throw new AttributeExpressionLanguageException("Could not parse input as date", pe);
|
throw new AttributeExpressionLanguageException("Could not parse input as date", pe);
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
|
|
|
@ -17,16 +17,20 @@
|
||||||
package org.apache.nifi.attribute.expression.language.evaluation.functions;
|
package org.apache.nifi.attribute.expression.language.evaluation.functions;
|
||||||
|
|
||||||
import org.apache.nifi.attribute.expression.language.EvaluationContext;
|
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.DateEvaluator;
|
||||||
import org.apache.nifi.attribute.expression.language.evaluation.Evaluator;
|
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.QueryResult;
|
||||||
import org.apache.nifi.attribute.expression.language.evaluation.StringEvaluator;
|
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.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.Date;
|
||||||
import java.util.Locale;
|
import java.util.Locale;
|
||||||
import java.util.TimeZone;
|
|
||||||
|
|
||||||
public class FormatEvaluator extends StringEvaluator {
|
public class FormatEvaluator extends StringEvaluator {
|
||||||
|
|
||||||
|
@ -34,9 +38,31 @@ public class FormatEvaluator extends StringEvaluator {
|
||||||
private final Evaluator<String> format;
|
private final Evaluator<String> format;
|
||||||
private final Evaluator<String> timeZone;
|
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) {
|
public FormatEvaluator(final DateEvaluator subject, final Evaluator<String> format, final Evaluator<String> timeZone) {
|
||||||
this.subject = subject;
|
this.subject = subject;
|
||||||
this.format = format;
|
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;
|
this.timeZone = timeZone;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -47,23 +73,28 @@ public class FormatEvaluator extends StringEvaluator {
|
||||||
return new StringQueryResult(null);
|
return new StringQueryResult(null);
|
||||||
}
|
}
|
||||||
|
|
||||||
final QueryResult<String> formatResult = format.evaluate(evaluationContext);
|
DateTimeFormatter dtf;
|
||||||
final String format = formatResult.getValue();
|
if (preparedFormatter == null) {
|
||||||
if (format == null) {
|
final QueryResult<String> formatResult = format.evaluate(evaluationContext);
|
||||||
return null;
|
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 ((preparedFormatter == null || !preparedFormatterHasRequestedTimeZone) && timeZone != null) {
|
||||||
|
|
||||||
if(timeZone != null) {
|
|
||||||
final QueryResult<String> tzResult = timeZone.evaluate(evaluationContext);
|
final QueryResult<String> tzResult = timeZone.evaluate(evaluationContext);
|
||||||
final String tz = tzResult.getValue();
|
final String tz = tzResult.getValue();
|
||||||
if(tz != null && TimeZone.getTimeZone(tz) != null) {
|
if (tz != null) {
|
||||||
sdf.setTimeZone(TimeZone.getTimeZone(tz));
|
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
|
@Override
|
||||||
|
|
|
@ -17,17 +17,20 @@
|
||||||
package org.apache.nifi.attribute.expression.language.evaluation.functions;
|
package org.apache.nifi.attribute.expression.language.evaluation.functions;
|
||||||
|
|
||||||
import org.apache.nifi.attribute.expression.language.EvaluationContext;
|
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.DateEvaluator;
|
||||||
import org.apache.nifi.attribute.expression.language.evaluation.DateQueryResult;
|
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.Evaluator;
|
||||||
import org.apache.nifi.attribute.expression.language.evaluation.QueryResult;
|
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.attribute.expression.language.exception.IllegalAttributeException;
|
||||||
|
import org.apache.nifi.util.FormatUtils;
|
||||||
|
|
||||||
import java.text.ParseException;
|
import java.time.ZoneId;
|
||||||
import java.text.SimpleDateFormat;
|
import java.time.format.DateTimeFormatter;
|
||||||
|
import java.time.format.DateTimeParseException;
|
||||||
|
import java.util.Collections;
|
||||||
import java.util.Date;
|
import java.util.Date;
|
||||||
import java.util.Locale;
|
|
||||||
import java.util.TimeZone;
|
|
||||||
|
|
||||||
public class StringToDateEvaluator extends DateEvaluator {
|
public class StringToDateEvaluator extends DateEvaluator {
|
||||||
|
|
||||||
|
@ -35,9 +38,31 @@ public class StringToDateEvaluator extends DateEvaluator {
|
||||||
private final Evaluator<String> format;
|
private final Evaluator<String> format;
|
||||||
private final Evaluator<String> timeZone;
|
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) {
|
public StringToDateEvaluator(final Evaluator<String> subject, final Evaluator<String> format, final Evaluator<String> timeZone) {
|
||||||
this.subject = subject;
|
this.subject = subject;
|
||||||
this.format = format;
|
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;
|
this.timeZone = timeZone;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -49,21 +74,31 @@ public class StringToDateEvaluator extends DateEvaluator {
|
||||||
return new DateQueryResult(null);
|
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 QueryResult<String> tzResult = timeZone.evaluate(evaluationContext);
|
||||||
final String tz = tzResult.getValue();
|
final String tz = tzResult.getValue();
|
||||||
if(tz != null && TimeZone.getTimeZone(tz) != null) {
|
if(tz != null) {
|
||||||
sdf.setTimeZone(TimeZone.getTimeZone(tz));
|
dtf = dtf.withZone(ZoneId.of(tz));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
return new DateQueryResult(sdf.parse(subjectValue));
|
return new DateQueryResult(Date.from(FormatUtils.parseToInstant(dtf, subjectValue)));
|
||||||
} catch (final ParseException e) {
|
} catch (final DateTimeParseException e) {
|
||||||
throw new IllegalAttributeException("Cannot parse attribute value as a date; date format: "
|
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) {
|
} catch (final IllegalArgumentException e) {
|
||||||
throw new IllegalAttributeException("Invalid date format: " + formatValue);
|
throw new IllegalAttributeException("Invalid date format: " + formatValue);
|
||||||
}
|
}
|
||||||
|
|
|
@ -37,7 +37,6 @@ import java.io.BufferedInputStream;
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
import java.io.InputStreamReader;
|
import java.io.InputStreamReader;
|
||||||
import java.io.Reader;
|
import java.io.Reader;
|
||||||
import java.text.SimpleDateFormat;
|
|
||||||
import java.time.Instant;
|
import java.time.Instant;
|
||||||
import java.time.ZoneId;
|
import java.time.ZoneId;
|
||||||
import java.time.format.DateTimeFormatter;
|
import java.time.format.DateTimeFormatter;
|
||||||
|
@ -52,6 +51,7 @@ import java.util.List;
|
||||||
import java.util.Locale;
|
import java.util.Locale;
|
||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
import java.util.Optional;
|
import java.util.Optional;
|
||||||
|
import java.util.TimeZone;
|
||||||
import java.util.concurrent.TimeUnit;
|
import java.util.concurrent.TimeUnit;
|
||||||
|
|
||||||
import static java.lang.Double.NEGATIVE_INFINITY;
|
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
|
// the date.toString() above will end up truncating the milliseconds. So remove millis from the Date before
|
||||||
// formatting it
|
// formatting it
|
||||||
final SimpleDateFormat sdf = new SimpleDateFormat("yyyy/MM/dd HH:mm:ss.SSS", Locale.US);
|
final DateTimeFormatter dtf = DateTimeFormatter.ofPattern("yyyy/MM/dd HH:mm:ss.SSS", Locale.US);
|
||||||
final long millis = date.getTime() % 1000L;
|
final Instant truncatedSecond = date.toInstant().truncatedTo(ChronoUnit.SECONDS);
|
||||||
final Date roundedToNearestSecond = new Date(date.getTime() - millis);
|
final String formatted = dtf.format(truncatedSecond);
|
||||||
final String formatted = sdf.format(roundedToNearestSecond);
|
|
||||||
|
|
||||||
final QueryResult<?> result = query.evaluate(new StandardEvaluationContext(attributes));
|
final QueryResult<?> result = query.evaluate(new StandardEvaluationContext(attributes));
|
||||||
assertEquals(ResultType.STRING, result.getResultType());
|
assertEquals(ResultType.STRING, result.getResultType());
|
||||||
|
@ -777,7 +776,7 @@ public class TestQuery {
|
||||||
@Test
|
@Test
|
||||||
public void testProblematic1() {
|
public void testProblematic1() {
|
||||||
// There was a bug that prevented this expression from compiling. This test just verifies that it now compiles.
|
// 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);
|
Query.compile(queryString);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -944,11 +943,18 @@ public class TestQuery {
|
||||||
final String format = "yyyy-MM-dd'T'HH:mm:ss.SSS'Z'";
|
final String format = "yyyy-MM-dd'T'HH:mm:ss.SSS'Z'";
|
||||||
|
|
||||||
final String query = "startDateTime=\"${date:toNumber():toDate():format(\"" + format + "\")}\"";
|
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);
|
TimeZone current = TimeZone.getDefault();
|
||||||
assertEquals("startDateTime=\"" + expectedTime + "\"", result);
|
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);
|
final List<Range> ranges = Query.extractExpressionRanges(query);
|
||||||
assertEquals(1, ranges.size());
|
assertEquals(1, ranges.size());
|
||||||
}
|
}
|
||||||
|
@ -973,6 +979,28 @@ public class TestQuery {
|
||||||
assertEquals(1, ranges.size());
|
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
|
@Test
|
||||||
public void testDateConversion() {
|
public void testDateConversion() {
|
||||||
final Map<String, String> attributes = new HashMap<>();
|
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");
|
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
|
@Test
|
||||||
public void testDateFormatConversionWithTimeZone() {
|
public void testDateFormatConversionWithTimeZone() {
|
||||||
final Map<String, String> attributes = new HashMap<>();
|
final Map<String, String> attributes = new HashMap<>();
|
||||||
|
|
|
@ -17,8 +17,17 @@
|
||||||
package org.apache.nifi.util;
|
package org.apache.nifi.util;
|
||||||
|
|
||||||
import java.text.NumberFormat;
|
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.Arrays;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
import java.util.Locale;
|
||||||
import java.util.concurrent.TimeUnit;
|
import java.util.concurrent.TimeUnit;
|
||||||
import java.util.regex.Matcher;
|
import java.util.regex.Matcher;
|
||||||
import java.util.regex.Pattern;
|
import java.util.regex.Pattern;
|
||||||
|
@ -46,6 +55,8 @@ public class FormatUtils {
|
||||||
public static final Pattern TIME_DURATION_PATTERN = Pattern.compile(TIME_DURATION_REGEX);
|
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 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.
|
* Formats the specified count by adding commas.
|
||||||
*
|
*
|
||||||
|
@ -425,4 +436,40 @@ public class FormatUtils {
|
||||||
|
|
||||||
return sb.toString();
|
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 org.junit.jupiter.api.Test;
|
||||||
|
|
||||||
import java.text.DecimalFormatSymbols;
|
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 java.util.concurrent.TimeUnit;
|
||||||
|
|
||||||
import static org.junit.jupiter.api.Assertions.assertEquals;
|
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||||
|
|
||||||
public class TestFormatUtils {
|
public class TestFormatUtils {
|
||||||
|
|
||||||
|
private static String NEW_YORK_TIME_ZONE_ID = "America/New_York";
|
||||||
|
private static String KIEV_TIME_ZONE_ID = "Europe/Kiev";
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
public void testParse() {
|
public void testParse() {
|
||||||
assertEquals(3, FormatUtils.getTimeDuration("3000 ms", TimeUnit.SECONDS));
|
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));
|
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
|
- *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
|
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>".
|
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`.
|
- *Boolean*: A Boolean is one of either `true` or `false`.
|
||||||
|
|
||||||
After evaluating expression language functions, all attributes are stored as type String.
|
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
|
=== format
|
||||||
|
|
||||||
*Description*: [.description]#Formats a number as a date/time according to the format specified by the argument. The argument
|
*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
|
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.#
|
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*:
|
*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)#
|
- [.argName]#_time zone_# : [.argDesc]#Optional argument that specifies the time zone to use (in the Java TimeZone syntax)#
|
||||||
|
|
||||||
*Return Type*: [.returnType]#String#
|
*Return Type*: [.returnType]#String#
|
||||||
|
@ -2308,14 +2308,14 @@ then the following Expressions will yield the following results:
|
||||||
=== toDate
|
=== toDate
|
||||||
|
|
||||||
*Description*: [.description]#Converts a String into a Date data type, based on the format specified by the argument. The argument
|
*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.#
|
according the argument. The date will be evaluated using the local time zone unless specified in the second optional argument.#
|
||||||
|
|
||||||
*Subject Type*: [.subject]#String#
|
*Subject Type*: [.subject]#String#
|
||||||
|
|
||||||
*Arguments*:
|
*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.#
|
- [.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