diff --git a/common/script/script-api/src/main/java/org/thingsboard/script/api/tbel/TbDate.java b/common/script/script-api/src/main/java/org/thingsboard/script/api/tbel/TbDate.java index f06d4cec53..9bab4060ab 100644 --- a/common/script/script-api/src/main/java/org/thingsboard/script/api/tbel/TbDate.java +++ b/common/script/script-api/src/main/java/org/thingsboard/script/api/tbel/TbDate.java @@ -26,7 +26,6 @@ import java.text.SimpleDateFormat; import java.time.Instant; import java.time.LocalDate; import java.time.LocalDateTime; -import java.time.OffsetDateTime; import java.time.ZoneId; import java.time.ZoneOffset; import java.time.ZonedDateTime; @@ -58,15 +57,16 @@ public class TbDate implements Serializable, Cloneable { this.instant = parseInstant(s); } - public TbDate(String s, String pattern, Locale locale) { - instant = parseInstant(s, pattern, locale, ZoneId.systemDefault()); + public TbDate(String s, String pattern) { + this.instant = parseInstant(s, Locale.getDefault().toLanguageTag(), pattern); } - public TbDate(String s, String pattern, Locale locale, String zoneIdStr) { - ZoneId zoneId = ZoneId.of(zoneIdStr); - instant = parseInstant(s, pattern, locale, zoneId); + + public TbDate(String s, String pattern, String locale) { + this.instant = parseInstant(s, locale, pattern); } - public TbDate(String s, String pattern, Locale locale, ZoneId zoneId) { - instant = parseInstant(s, pattern, locale, zoneId); + + public TbDate(String s, String pattern, String locale, String zoneId) { + this.instant = parseInstant(s, pattern, locale, zoneId); } public TbDate(long dateMilliSecond) { @@ -488,18 +488,25 @@ public class TbDate implements Serializable, Cloneable { } private static Instant parseInstant(String s) { - try{ - if (s.length() > 0 && Character.isDigit(s.charAt(0))) { - // assuming UTC instant "2007-12-03T10:15:30.00Z" - return OffsetDateTime.parse(s).toInstant(); + boolean isIsoFormat = s.length() > 0 && Character.isDigit(s.charAt(0)); + if (isIsoFormat) { + return getInstant_ISO_OFFSET_DATE_TIME(s); + } else { + return getInstant_RFC_1123(s); + } + } + + private static Instant parseInstant(String s, String localeStr, String pattern) { + try { + DateTimeFormatter formatter = DateTimeFormatter.ofPattern(pattern, Locale.forLanguageTag(localeStr)); + return Instant.from(formatter.parse(s)); + } catch (Exception ex) { + try { + return parseInstant(s, pattern, localeStr, ZoneId.systemDefault().getId()); + } catch (final DateTimeParseException e) { + final ConversionException exception = new ConversionException("Cannot parse value [" + s + "] as instant", ex); + throw exception; } - else { - // assuming RFC-1123 value "Tue, 3 Jun 2008 11:05:30 GMT-02:00" - return Instant.from(DateTimeFormatter.RFC_1123_DATE_TIME.parse(s)); - } - } catch (final DateTimeParseException ex) { - final ConversionException exception = new ConversionException("Cannot parse value [" + s + "] as instant", ex); - throw exception; } } @@ -508,10 +515,55 @@ public class TbDate implements Serializable, Cloneable { ZonedDateTime zonedDateTime = ZonedDateTime.of(year, month, date, hrs, min, second, secondMilli*1000000, zoneId); return zonedDateTime.toInstant(); } - private static Instant parseInstant(String s, String pattern, Locale locale, ZoneId zoneId) { - DateTimeFormatter dateTimeFormatter = DateTimeFormatter.ofPattern(pattern, locale); + private static Instant parseInstant(String s, String pattern, String localeStr, String zoneIdStr) { + DateTimeFormatter dateTimeFormatter = DateTimeFormatter.ofPattern(pattern, Locale.forLanguageTag(localeStr)); LocalDateTime localDateTime = LocalDateTime.parse(s, dateTimeFormatter); - ZonedDateTime zonedDateTime = localDateTime.atZone(zoneId); + ZonedDateTime zonedDateTime = localDateTime.atZone(ZoneId.of(zoneIdStr)); return zonedDateTime.toInstant(); } + + private static Instant getInstant_ISO_OFFSET_DATE_TIME(String s) { + // assuming "2007-12-03T10:15:30.00Z" UTC instant + // assuming "2007-12-03T10:15:30.00" ZoneId.systemDefault() instant + // assuming "2007-12-03T10:15:30.00-04:00" TZ instant + // assuming "2007-12-03T10:15:30.00+04:00" TZ instant + DateTimeFormatter formatter = DateTimeFormatter.ISO_OFFSET_DATE_TIME; + try { + return Instant.from(formatter.parse(s)); + } catch (DateTimeParseException ex) { + try { + long timeMS = parse(s); + if (timeMS != -1) { + return Instant.ofEpochMilli(timeMS); + } else { + throw new ConversionException("Cannot parse value [" + s + "] as instant"); + } + } catch (final DateTimeParseException e) { + throw new ConversionException("Cannot parse value [" + s + "] as instant"); + } + } + } + private static Instant getInstant_RFC_1123(String s) { + // assuming RFC-1123 value "Tue, 3 Jun 2008 11:05:30 GMT" + // assuming RFC-1123 value "Tue, 3 Jun 2008 11:05:30 GMT-02:00" + // assuming RFC-1123 value "Tue, 3 Jun 2008 11:05:30 -0200" + DateTimeFormatter formatter = DateTimeFormatter.RFC_1123_DATE_TIME; + try { + return Instant.from(formatter.parse(s)); + } catch (DateTimeParseException ex) { + try { + return getInstantWithLocalZoneOffsetId_RFC_1123(s); + } catch (final DateTimeParseException e) { + throw new ConversionException("Cannot parse value [" + s + "] as instant"); + } + } + } + private static Instant getInstantWithLocalZoneOffsetId_RFC_1123(String value) { + String s = value.trim() + " GMT"; + Instant instant = Instant.from(DateTimeFormatter.RFC_1123_DATE_TIME.parse(s)); + ZoneId systemZone = ZoneId.systemDefault(); // my timezone + String id = systemZone.getRules().getOffset(instant).getId(); + value = value.trim() + " " + id.replaceAll(":", ""); + return Instant.from(DateTimeFormatter.RFC_1123_DATE_TIME.parse(value)); + } } diff --git a/common/script/script-api/src/test/java/org/thingsboard/script/api/tbel/TbDateConstructorTest.java b/common/script/script-api/src/test/java/org/thingsboard/script/api/tbel/TbDateConstructorTest.java new file mode 100644 index 0000000000..8a9d84b980 --- /dev/null +++ b/common/script/script-api/src/test/java/org/thingsboard/script/api/tbel/TbDateConstructorTest.java @@ -0,0 +1,102 @@ +/** + * Copyright © 2016-2023 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.thingsboard.script.api.tbel; + +import org.junit.Assert; +import org.junit.jupiter.api.AfterAll; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.Test; +import org.mvel2.CompileException; +import org.mvel2.ExecutionContext; +import org.mvel2.ParserContext; +import org.mvel2.SandboxedParserConfiguration; + +import java.io.Serializable; +import java.util.HashMap; + +import static org.junit.Assert.assertThrows; +import static org.junit.Assert.assertTrue; +import static org.mvel2.MVEL.compileExpression; +import static org.mvel2.MVEL.executeTbExpression; + +public class TbDateConstructorTest { + + private static ExecutionContext executionContext; + + @BeforeAll + public static void setup() { + SandboxedParserConfiguration parserConfig = ParserContext.enableSandboxedMode(); + parserConfig.addImport("JSON", TbJson.class); + parserConfig.registerDataType("Date", TbDate.class, date -> 8L); + executionContext = new ExecutionContext(parserConfig, 5 * 1024 * 1024); + } + + @AfterAll + public static void tearDown() { + ParserContext.disableSandboxedMode(); + } + + + @Test + void TestTbDateConstructorWithStringParameters () { + // one: date in String + String body = "var d = new Date(\"2023-08-06T04:04:05.123Z\"); \n" + + "d.toISOString()"; + Object res = executeScript(body); + Assert.assertNotEquals("2023-08-06T04:04:05.123Z".length(), res); + + // two: date in String + pattern + body = "var pattern = \"yyyy-MM-dd HH:mm:ss.SSSXXX\";\n" + + "var d = new Date(\"2023-08-06 04:04:05.000Z\", pattern);\n" + + "d.toISOString()"; + res = executeScript(body); + Assert.assertNotEquals("2023-08-06T04:04:05Z".length(), res); + + + // three: date in String + pattern + locale + body = "var pattern = \"hh:mm:ss a, EEE M/d/uuuu\";\n" + + "var d = new Date(\"02:15:30 PM, Sun 10/09/2022\", pattern, \"en-US\");" + + "d.toISOString()"; + res = executeScript(body); + Assert.assertNotEquals("2023-08-06T04:04:05Z".length(), res); + + // four: date in String + pattern + locale + TimeZone + body = "var pattern = \"hh:mm:ss a, EEE M/d/uuuu\";\n" + + "var d = new Date(\"02:15:30 PM, Sun 10/09/2022\", pattern, \"en-US\", \"America/New_York\");" + + "d.toISOString()"; + res = executeScript(body); + Assert.assertNotEquals("22022-10-09T18:15:30Z".length(), res); + } + + @Test + void TbDateConstructorWithStringParameters_PatternNotMatchLocale_Error () { + String expectedMessage = "could not create constructor: null"; + + String body = "var pattern = \"hh:mm:ss a, EEE M/d/uuuu\";\n" + + "var d = new Date(\"02:15:30 PM, Sun 10/09/2022\", pattern, \"de\");" + + "d.toISOString()"; + Exception actual = assertThrows(CompileException.class, () -> { + executeScript(body); + }); + assertTrue(actual.getMessage().contains(expectedMessage)); + + } + + private Object executeScript(String ex) { + Serializable compiled = compileExpression(ex, new ParserContext()); + return executeTbExpression(compiled, executionContext, new HashMap()); + } +} diff --git a/common/script/script-api/src/test/java/org/thingsboard/script/api/tbel/TbDateTest.java b/common/script/script-api/src/test/java/org/thingsboard/script/api/tbel/TbDateTest.java index ab0b2875bd..120a776c1a 100644 --- a/common/script/script-api/src/test/java/org/thingsboard/script/api/tbel/TbDateTest.java +++ b/common/script/script-api/src/test/java/org/thingsboard/script/api/tbel/TbDateTest.java @@ -15,8 +15,6 @@ */ package org.thingsboard.script.api.tbel; -import com.fasterxml.jackson.databind.JsonNode; -import com.fasterxml.jackson.databind.node.ObjectNode; import com.google.common.util.concurrent.FutureCallback; import com.google.common.util.concurrent.Futures; import com.google.common.util.concurrent.ListenableFuture; @@ -36,7 +34,6 @@ import java.time.Instant; import java.time.ZoneId; import java.util.ArrayList; import java.util.List; -import java.util.Locale; import java.util.Map; import java.util.concurrent.CountDownLatch; import java.util.concurrent.ExecutionException; @@ -156,7 +153,7 @@ class TbDateTest { void testToLocaleDateString() { String s = "02:15:30 PM, Sun 10/09/2022"; String pattern = "hh:mm:ss a, EEE M/d/uuuu"; - TbDate d = new TbDate(s, pattern, Locale.US); + TbDate d = new TbDate(s, pattern, "en-US"); // tz = local int localOffsetHrs = ZoneId.systemDefault().getRules().getOffset(d.getInstant()).getTotalSeconds()/60/60; int hrs = 14 - localOffsetHrs; @@ -169,9 +166,9 @@ class TbDateTest { Assert.assertEquals("2023-08-06T08:04:05Z", d.toISOString()); s = "02:15:30 PM, Sun 10/09/2022"; - d = new TbDate(s, pattern, Locale.US, "-04:00"); + d = new TbDate(s, pattern, "en-US", "-04:00"); Assert.assertEquals("2022-10-09T18:15:30Z", d.toISOString()); - d = new TbDate(s, pattern, Locale.US, "America/New_York"); + d = new TbDate(s, pattern,"en-US", "America/New_York"); Assert.assertEquals("2022-10-09T18:15:30Z", d.toISOString()); // tz = "+02:00" /** @@ -181,12 +178,12 @@ class TbDateTest { * `{ "AM", "PM" }` */ s = "09:15:30 nachm., So. 10/09/2022"; - d = new TbDate(s, pattern, Locale.GERMAN, ZoneId.of("Europe/Berlin")); + d = new TbDate(s, pattern, "de","Europe/Berlin"); Assert.assertEquals("2022-10-09T19:15:30Z", d.toISOString()); s = "02:15:30 пп, середа, 4 жовтня 2023 р."; pattern = "hh:mm:ss a, EEEE, d MMMM y 'р.'"; - d = new TbDate(s, pattern, Locale.forLanguageTag("uk-UA")); + d = new TbDate(s, pattern, "uk-UA"); localOffsetHrs = ZoneId.systemDefault().getRules().getOffset(d.getInstant()).getTotalSeconds()/60/60; hrs = 14 - localOffsetHrs; expected = "2023-10-04T" + hrs + ":15:30Z"; @@ -353,13 +350,19 @@ class TbDateTest { stringDateTZ = "2023-09-06T01:04:05.00-04:00"; d = new TbDate(stringDateTZ); Assert.assertEquals("2023-09-06T05:04:05Z", d.toISOString()); - stringDateTZ = "2023-09-06T01:04:05.00+04:30:56"; + stringDateTZ = "2023-09-06T01:04:05.00+04:30:56"; d = new TbDate(stringDateTZ); Assert.assertEquals("2023-09-05T20:33:09Z", d.toISOString()); stringDateTZ = "2023-09-06T01:04:05.00-02:00"; d = new TbDate(stringDateTZ); Assert.assertEquals("2023-09-06T03:04:05Z", d.toISOString()); - String stringDateRFC_1123 = "Sat, 3 Jun 2023 11:05:30 GMT"; + // Without_TZ + stringDateTZ = "2023-08-06T04:04:05.123"; + d = new TbDate(stringDateTZ); + Assert.assertEquals("2023-08-06 04:04:05", d.toLocaleString()); + + // With TZ RFC_1123 + String stringDateRFC_1123 = "Sat, 3 Jun 2023 11:05:30 GMT"; d = new TbDate(stringDateRFC_1123); Assert.assertEquals("2023-06-03T11:05:30Z", d.toISOString()); stringDateRFC_1123 = "Sat, 3 Jun 2023 01:04:05 +043056"; @@ -371,28 +374,57 @@ class TbDateTest { stringDateRFC_1123 = "Thu, 29 Feb 2024 11:05:30 -03"; d = new TbDate(stringDateRFC_1123); Assert.assertEquals("2024-02-29T14:05:30Z", d.toISOString()); + // Without TZ RFC_1123 + stringDateRFC_1123 = "Sat, 3 Jun 2023 11:05:30"; + d = new TbDate(stringDateRFC_1123); + Assert.assertEquals("2023-06-03 11:05:30", d.toLocaleString()); - String stringDateZ_error = "2023-09-06T01:04:05.00+045"; - Exception actual = assertThrows(ConversionException.class, () -> { - new TbDate(stringDateZ_error); - }); + // With pattern + locale - ok + String pattern = "hh:mm:ss a, EEE M/d/uuuu"; + stringDateRFC_1123 = "09:15:30 nachm., So. 10/09/2022"; + d = new TbDate(stringDateRFC_1123 , pattern, "de"); + Assert.assertEquals("2022-10-09 21:15:30", d.toLocaleString()); + + // failed TZ String expectedMessage = "Cannot parse value"; + String finalStringDateZ_error0 = "2023-09-06T01:04:05.00+045"; + Exception actual = assertThrows(ConversionException.class, () -> { + new TbDate(finalStringDateZ_error0); + }); + assertTrue(actual.getMessage().contains(expectedMessage)); + // failed TZ + String finalStringDateZ_error1 = "2023-08-06T04:04:05.123+04:00:00:00"; + actual = assertThrows(ConversionException.class, () -> { + new TbDate(finalStringDateZ_error1); + }); + assertTrue(actual.getMessage().contains(expectedMessage)); + // failed TZ + String finalStringDateZ_error2 ="2023-08-06T04:04:05.123+4"; + actual = assertThrows(ConversionException.class, () -> { + new TbDate(finalStringDateZ_error2); + }); + assertTrue(actual.getMessage().contains(expectedMessage)); + // The locale does not match the pattern RFC_1123 + String finalStringDateZ_error3= "02:15:30 PM, Sun 10/09/2022"; + pattern = "hh:mm:ss a, EEE M/d/uuuu"; + String finalPattern = pattern; + actual = assertThrows(ConversionException.class, () -> { + new TbDate(finalStringDateZ_error3, finalPattern, "de"); + }); assertTrue(actual.getMessage().contains(expectedMessage)); + // failed DayOfWeek RFC_1123 String stringDateRFC_1123_error = "Tue, 3 Jun 2023 11:05:30 GMT"; actual = assertThrows(ConversionException.class, () -> { new TbDate(stringDateRFC_1123_error); }); assertTrue(actual.getMessage().contains(expectedMessage)); } + @Test void TestParse () { - String stringDateUTC = "2023-09-06T01:04:05.345Z"; - TbDate d = new TbDate(stringDateUTC); - Assert.assertEquals(1693962245345L, d.parseSecondMilli()); - Assert.assertEquals(1693962245L, d.parseSecond()); String stringDateStart = "1970-01-01T00:00:00Z"; - d = new TbDate(stringDateStart); + TbDate d = new TbDate(stringDateStart); long actualMillis = TbDate.parse("1970-01-01 T00:00:00"); Assert.assertEquals(-d.getLocaleZoneOffset().getTotalSeconds() * 1000, actualMillis); String pattern = "yyyy-MM-dd HH:mm:ss.SSS"; @@ -454,6 +486,7 @@ class TbDateTest { @Test void Test_Year_Moth_Date_Hours_Min_Sec_Without_TZ() { + TbDate d = new TbDate(2023, 8, 18); Assert.assertEquals("2023-08-18 00:00:00", d.toLocaleString()); d = new TbDate(2023, 9, 17, 17, 34); @@ -466,6 +499,45 @@ class TbDateTest { Assert.assertEquals("2023-09-07 08:04:05", d.toLocaleString()); } + @Test + void Test_DateString_With_Pattern() { + String pattern = "yyyy-MM-dd HH:mm:ss.SSSXXX"; + TbDate d = new TbDate("2023-08-06 04:04:05.000-04:00", pattern); + Assert.assertEquals("2023-08-06T08:04:05Z", d.toISOString()); + } + @Test + void Test_DateString_With_TZ() { + int date = 7; + int tz = -4; + int hrs = 8; + String pattern = "2023-09-%s %s:04:05"; + String tzStr = "-04:00:00"; + String dateStr = "2023-09-0" + date + "T0" + hrs + ":04:05.123" + tzStr; + TbDate d = new TbDate(dateStr); + int localOffsetHrs = ZoneId.systemDefault().getRules().getOffset(d.getInstant()).getTotalSeconds()/60/60; + TbDateTestEntity tbDateTest = new TbDateTestEntity(23, 9, date, hrs + localOffsetHrs - tz); + String expected = String.format(pattern, tbDateTest.geDateStr(), tbDateTest.geHoursStr()); + Assert.assertEquals(expected, d.toLocaleString()); + + tz = +5; + tzStr = "+05:00"; + dateStr = "2023-09-0" + date + "T0" + hrs + ":04:05.123" + tzStr; + d = new TbDate(dateStr); + localOffsetHrs = ZoneId.systemDefault().getRules().getOffset(d.getInstant()).getTotalSeconds()/60/60; + tbDateTest = new TbDateTestEntity(23, 9, date, hrs + localOffsetHrs - tz); + expected = String.format(pattern, tbDateTest.geDateStr(), tbDateTest.geHoursStr()); + Assert.assertEquals(expected, d.toLocaleString()); + + tz = -2; + tzStr = "-02"; + dateStr = "2023-09-0" + date + "T0" + hrs + ":04:05.123" + tzStr; + d = new TbDate(dateStr); + localOffsetHrs = ZoneId.systemDefault().getRules().getOffset(d.getInstant()).getTotalSeconds()/60/60; + tbDateTest = new TbDateTestEntity(23, 9, date, hrs + localOffsetHrs - tz); + expected = String.format(pattern, tbDateTest.geDateStr(), tbDateTest.geHoursStr()); + Assert.assertEquals(expected, d.toLocaleString()); + } + @Test void Test_Get_LocalDateTime_With_TZ() { int hrs = 8;