This is an automated email from the ASF dual-hosted git repository.
nvazquez pushed a commit to branch 4.20
in repository https://gitbox.apache.org/repos/asf/cloudstack.git
The following commit(s) were added to refs/heads/4.20 by this push:
new 5deb6a958df Improve date/timestamp handling performance in
GenericDaoBase and DateUtil (#13809)
5deb6a958df is described below
commit 5deb6a958df549b68232f9e7dedff8de4422e603
Author: Manoj Kumar <[email protected]>
AuthorDate: Thu Oct 1 03:30:06 2026 +0530
Improve date/timestamp handling performance in GenericDaoBase and DateUtil
(#13809)
* Improve date/timestamp handling performance in GenericDaoBase and DateUtil
* address review comment
---
.../org/apache/cloudstack/backup/BackupVO.java | 2 +-
.../java/com/cloud/utils/db/GenericDaoBase.java | 78 +++++++++-----
.../com/cloud/utils/db/GenericDaoBaseTest.java | 75 ++++++++++++++
server/src/main/java/com/cloud/api/ApiServer.java | 7 +-
.../src/test/java/com/cloud/api/ApiServerTest.java | 69 +++++++++++++
utils/src/main/java/com/cloud/utils/DateUtil.java | 63 +++++-------
.../test/java/com/cloud/utils/DateUtilTest.java | 113 +++++++++++++++++++--
7 files changed, 330 insertions(+), 77 deletions(-)
diff --git
a/engine/schema/src/main/java/org/apache/cloudstack/backup/BackupVO.java
b/engine/schema/src/main/java/org/apache/cloudstack/backup/BackupVO.java
index b4cd2f7bada..a11de51ed63 100644
--- a/engine/schema/src/main/java/org/apache/cloudstack/backup/BackupVO.java
+++ b/engine/schema/src/main/java/org/apache/cloudstack/backup/BackupVO.java
@@ -60,7 +60,7 @@ public class BackupVO implements Backup {
private String backupType;
@Column(name = "date")
- @Temporal(value = TemporalType.DATE)
+ @Temporal(value = TemporalType.TIMESTAMP)
private Date date;
@Column(name = GenericDao.REMOVED_COLUMN)
diff --git a/framework/db/src/main/java/com/cloud/utils/db/GenericDaoBase.java
b/framework/db/src/main/java/com/cloud/utils/db/GenericDaoBase.java
index e6a4bb8f9b9..3290d7125f2 100644
--- a/framework/db/src/main/java/com/cloud/utils/db/GenericDaoBase.java
+++ b/framework/db/src/main/java/com/cloud/utils/db/GenericDaoBase.java
@@ -34,6 +34,9 @@ import java.sql.ResultSet;
import java.sql.ResultSetMetaData;
import java.sql.SQLException;
import java.sql.Statement;
+import java.sql.Time;
+import java.sql.Timestamp;
+import java.sql.Types;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Calendar;
@@ -67,7 +70,6 @@ import org.apache.commons.lang3.StringUtils;
import org.apache.commons.lang3.exception.ExceptionUtils;
import com.amazonaws.util.CollectionUtils;
-import com.cloud.utils.DateUtil;
import com.cloud.utils.NumbersUtil;
import com.cloud.utils.Pair;
import com.cloud.utils.Ternary;
@@ -129,6 +131,28 @@ public abstract class GenericDaoBase<T, ID extends
Serializable> extends Compone
protected final static TimeZone s_gmtTimeZone =
TimeZone.getTimeZone("GMT");
+ /**
+ * Returns a fresh GMT {@link Calendar} for a single JDBC get/set
timestamp call. Calendar is
+ * mutable and JDBC drivers may mutate the instance passed to them, so a
new one is used per call
+ * rather than sharing a single instance across concurrent DAO operations.
+ */
+ protected static Calendar gmtCalendar() {
+ return Calendar.getInstance(s_gmtTimeZone);
+ }
+
+ /**
+ * Returns the SQL type ({@link Types}) matching the temporal flag of the
given attribute, so a
+ * null date/time/timestamp column is bound with the correct type instead
of always TIMESTAMP.
+ */
+ protected static int temporalSqlType(Attribute attr) {
+ if (attr.is(Attribute.Flag.Date)) {
+ return Types.DATE;
+ } else if (attr.is(Attribute.Flag.Time)) {
+ return Types.TIME;
+ }
+ return Types.TIMESTAMP;
+ }
+
protected final static Map<Class<?>, GenericDao<?, ? extends
Serializable>> s_daoMaps = new ConcurrentHashMap<Class<?>, GenericDao<?, ?
extends Serializable>>(71);
private final ConversionSupport _conversionSupport;
@@ -598,20 +622,16 @@ public abstract class GenericDaoBase<T, ID extends
Serializable> extends Compone
field.set(entity, rs.getInt(index));
}
} else if (type == Date.class) {
- final Object data = rs.getDate(index);
- if (data == null) {
- field.set(entity, null);
- return;
- }
- field.set(entity, DateUtil.parseDateString(s_gmtTimeZone,
rs.getString(index)));
+ final Timestamp ts = rs.getTimestamp(index, gmtCalendar());
+ field.set(entity, ts == null ? null : new Date(ts.getTime()));
} else if (type == Calendar.class) {
- final Object data = rs.getDate(index);
+ final Timestamp data = rs.getTimestamp(index, gmtCalendar());
if (data == null) {
field.set(entity, null);
return;
}
- final Calendar cal = Calendar.getInstance();
- cal.setTime(DateUtil.parseDateString(s_gmtTimeZone,
rs.getString(index)));
+ final Calendar cal = Calendar.getInstance(s_gmtTimeZone);
+ cal.setTime(data);
field.set(entity, cal);
} else if (type == boolean.class) {
field.setBoolean(entity, rs.getBoolean(index));
@@ -732,11 +752,11 @@ public abstract class GenericDaoBase<T, ID extends
Serializable> extends Compone
return (M) (Long) rs.getLong(index);
}
} else if (type == Date.class) {
- final Object data = rs.getDate(index);
- if (data == null) {
+ final Timestamp ts = rs.getTimestamp(index, gmtCalendar());
+ if (ts == null) {
return null;
} else {
- return (M)DateUtil.parseDateString(s_gmtTimeZone,
rs.getString(index));
+ return (M) new Date(ts.getTime());
}
} else if (type == short.class) {
return (M) (Short) rs.getShort(index);
@@ -779,12 +799,12 @@ public abstract class GenericDaoBase<T, ID extends
Serializable> extends Compone
return (M) (Byte) rs.getByte(index);
}
} else if (type == Calendar.class) {
- final Object data = rs.getDate(index);
+ final Timestamp data = rs.getTimestamp(index, gmtCalendar());
if (data == null) {
return null;
} else {
- final Calendar cal = Calendar.getInstance();
- cal.setTime(DateUtil.parseDateString(s_gmtTimeZone,
rs.getString(index)));
+ final Calendar cal = Calendar.getInstance(s_gmtTimeZone);
+ cal.setTime(data);
return (M)cal;
}
} else if (type == byte[].class) {
@@ -1696,7 +1716,12 @@ public abstract class GenericDaoBase<T, ID extends
Serializable> extends Compone
while (en.hasMoreElements()) {
pstmt = txn.prepareAutoCloseStatement(ec.insertSql);
if (ec.targetClass == Date.class) {
- pstmt.setString(1,
DateUtil.getDateDisplayString(s_gmtTimeZone, (Date)en.nextElement()));
+ Date d = (Date) en.nextElement();
+ if (d == null) {
+ pstmt.setNull(1, Types.TIMESTAMP);
+ } else {
+ pstmt.setTimestamp(1, new Timestamp(d.getTime()),
gmtCalendar());
+ }
} else {
pstmt.setObject(1, en.nextElement());
}
@@ -1800,28 +1825,28 @@ public abstract class GenericDaoBase<T, ID extends
Serializable> extends Compone
} else if (attr.field.getType() == Date.class) {
final Date date = (Date)value;
if (date == null || date.equals(DATE_TO_NULL)) {
- pstmt.setObject(j, null);
+ pstmt.setNull(j, temporalSqlType(attr));
return;
}
if (attr.is(Attribute.Flag.Date)) {
- pstmt.setString(j,
DateUtil.getDateDisplayString(s_gmtTimeZone, date));
+ pstmt.setDate(j, new java.sql.Date(date.getTime()),
gmtCalendar());
} else if (attr.is(Attribute.Flag.TimeStamp)) {
- pstmt.setString(j,
DateUtil.getDateDisplayString(s_gmtTimeZone, date));
+ pstmt.setTimestamp(j, new Timestamp(date.getTime()),
gmtCalendar());
} else if (attr.is(Attribute.Flag.Time)) {
- pstmt.setString(j,
DateUtil.getDateDisplayString(s_gmtTimeZone, date));
+ pstmt.setTime(j, new java.sql.Time(date.getTime()),
gmtCalendar());
}
} else if (attr.field.getType() == Calendar.class) {
final Calendar cal = (Calendar)value;
if (cal == null) {
- pstmt.setObject(j, null);
+ pstmt.setNull(j, temporalSqlType(attr));
return;
}
if (attr.is(Attribute.Flag.Date)) {
- pstmt.setString(j,
DateUtil.getDateDisplayString(s_gmtTimeZone, cal.getTime()));
+ pstmt.setDate(j, new java.sql.Date(cal.getTimeInMillis()),
gmtCalendar());
} else if (attr.is(Attribute.Flag.TimeStamp)) {
- pstmt.setString(j,
DateUtil.getDateDisplayString(s_gmtTimeZone, cal.getTime()));
+ pstmt.setTimestamp(j, new Timestamp(cal.getTimeInMillis()),
gmtCalendar());
} else if (attr.is(Attribute.Flag.Time)) {
- pstmt.setString(j,
DateUtil.getDateDisplayString(s_gmtTimeZone, cal.getTime()));
+ pstmt.setTime(j, new Time(cal.getTimeInMillis()),
gmtCalendar());
}
} else if (attr.field.getType().isEnum()) {
final Enumerated enumerated =
attr.field.getAnnotation(Enumerated.class);
@@ -1955,7 +1980,8 @@ public abstract class GenericDaoBase<T, ID extends
Serializable> extends Compone
}
} else if (ec.targetClass == Date.class) {
while (rs.next()) {
- lst.add(DateUtil.parseDateString(s_gmtTimeZone,
rs.getString(1)));
+ final Timestamp ts = rs.getTimestamp(1, gmtCalendar());
+ lst.add(ts == null ? null : new Date(ts.getTime()));
}
} else if (ec.targetClass == Boolean.class) {
while (rs.next()) {
diff --git
a/framework/db/src/test/java/com/cloud/utils/db/GenericDaoBaseTest.java
b/framework/db/src/test/java/com/cloud/utils/db/GenericDaoBaseTest.java
index ebf514f532f..41a7be4ab86 100644
--- a/framework/db/src/test/java/com/cloud/utils/db/GenericDaoBaseTest.java
+++ b/framework/db/src/test/java/com/cloud/utils/db/GenericDaoBaseTest.java
@@ -18,9 +18,14 @@ package com.cloud.utils.db;
import java.sql.ResultSet;
import java.sql.SQLException;
+import java.sql.Timestamp;
+import java.sql.Types;
import java.util.ArrayList;
+import java.util.Calendar;
import java.util.Collection;
+import java.util.Date;
import java.util.List;
+import java.util.TimeZone;
import org.junit.Assert;
import org.junit.Before;
@@ -331,4 +336,74 @@ public class GenericDaoBaseTest {
Assert.assertNotNull(result);
Assert.assertEquals(expectedResult, result);
}
+
+ @Test
+ public void gmtCalendarUsesGmtTimeZone() {
+ Calendar calendar = GenericDaoBase.gmtCalendar();
+
+ Assert.assertEquals(TimeZone.getTimeZone("GMT"),
calendar.getTimeZone());
+ }
+
+ @Test
+ public void gmtCalendarReturnsFreshInstancePerCall() {
+ Assert.assertNotSame(GenericDaoBase.gmtCalendar(),
GenericDaoBase.gmtCalendar());
+ }
+
+ @Test
+ public void temporalSqlTypeDate() {
+ Attribute attr = new Attribute("table", "column");
+ attr.flags = Attribute.Flag.Date.setTrue(attr.flags);
+
+ Assert.assertEquals(Types.DATE, GenericDaoBase.temporalSqlType(attr));
+ }
+
+ @Test
+ public void temporalSqlTypeTime() {
+ Attribute attr = new Attribute("table", "column");
+ attr.flags = Attribute.Flag.Time.setTrue(attr.flags);
+
+ Assert.assertEquals(Types.TIME, GenericDaoBase.temporalSqlType(attr));
+ }
+
+ @Test
+ public void temporalSqlTypeDefaultsToTimestamp() {
+ Attribute attr = new Attribute("table", "column");
+ attr.flags = Attribute.Flag.TimeStamp.setTrue(attr.flags);
+
+ Assert.assertEquals(Types.TIMESTAMP,
GenericDaoBase.temporalSqlType(attr));
+ }
+
+ @Test
+ public void getObjectDateReadsViaGmtTimestamp() throws SQLException {
+ Timestamp ts = new Timestamp(1_700_000_000_000L);
+ Mockito.when(resultSet.getTimestamp(Mockito.eq(2),
Mockito.any(Calendar.class))).thenReturn(ts);
+
+ Date result = GenericDaoBase.getObject(Date.class, resultSet, 2);
+
+ Assert.assertEquals(ts.getTime(), result.getTime());
+ }
+
+ @Test
+ public void getObjectDateNullTimestampReturnsNull() throws SQLException {
+ Mockito.when(resultSet.getTimestamp(Mockito.eq(3),
Mockito.any(Calendar.class))).thenReturn(null);
+
+ Assert.assertNull(GenericDaoBase.getObject(Date.class, resultSet, 3));
+ }
+
+ @Test
+ public void getObjectCalendarReadsViaGmtTimestamp() throws SQLException {
+ Timestamp ts = new Timestamp(1_700_000_000_000L);
+ Mockito.when(resultSet.getTimestamp(Mockito.eq(4),
Mockito.any(Calendar.class))).thenReturn(ts);
+
+ Calendar result = GenericDaoBase.getObject(Calendar.class, resultSet,
4);
+
+ Assert.assertEquals(ts.getTime(), result.getTimeInMillis());
+ }
+
+ @Test
+ public void getObjectCalendarNullTimestampReturnsNull() throws
SQLException {
+ Mockito.when(resultSet.getTimestamp(Mockito.eq(5),
Mockito.any(Calendar.class))).thenReturn(null);
+
+ Assert.assertNull(GenericDaoBase.getObject(Calendar.class, resultSet,
5));
+ }
}
diff --git a/server/src/main/java/com/cloud/api/ApiServer.java
b/server/src/main/java/com/cloud/api/ApiServer.java
index 6e058746ccb..396ec505948 100644
--- a/server/src/main/java/com/cloud/api/ApiServer.java
+++ b/server/src/main/java/com/cloud/api/ApiServer.java
@@ -192,6 +192,7 @@ import com.google.gson.reflect.TypeToken;
import static com.cloud.user.AccountManagerImpl.apiKeyAccess;
import static
org.apache.cloudstack.user.UserPasswordResetManager.UserPasswordResetEnabled;
+import static org.apache.commons.lang3.StringUtils.deleteWhitespace;
@Component
public class ApiServer extends ManagerBase implements HttpRequestHandler,
ApiServerService, Configurable {
@@ -1372,11 +1373,10 @@ public class ApiServer extends ManagerBase implements
HttpRequestHandler, ApiSer
throw new PermissionDeniedException("User is null for role based
API access check for command" + commandName);
}
- final Account account = accountMgr.getAccount(user.getAccountId());
- final String accessAllowedCidrs =
ApiServiceConfiguration.ApiAllowedSourceCidrList.valueIn(account.getId()).replaceAll("\\s","");
final Boolean apiSourceCidrChecksEnabled =
ApiServiceConfiguration.ApiSourceCidrChecksEnabled.value();
-
if (apiSourceCidrChecksEnabled) {
+ final Account account = accountMgr.getAccount(user.getAccountId());
+ final String accessAllowedCidrs =
deleteWhitespace(ApiServiceConfiguration.ApiAllowedSourceCidrList.valueIn(account.getId()));
logger.debug("CIDRs from which account '" + account.toString() +
"' is allowed to perform API calls: " + accessAllowedCidrs);
if (!NetUtils.isIpInCidrList(remoteAddress,
accessAllowedCidrs.split(","))) {
logger.warn("Request by account '" + account.toString() + "'
was denied since " + remoteAddress + " does not match " + accessAllowedCidrs);
@@ -1384,7 +1384,6 @@ public class ApiServer extends ManagerBase implements
HttpRequestHandler, ApiSer
}
}
-
for (final APIChecker apiChecker : apiAccessCheckers) {
apiChecker.checkAccess(user, commandName);
}
diff --git a/server/src/test/java/com/cloud/api/ApiServerTest.java
b/server/src/test/java/com/cloud/api/ApiServerTest.java
index dedd6e02ec5..8ecdc458018 100644
--- a/server/src/test/java/com/cloud/api/ApiServerTest.java
+++ b/server/src/test/java/com/cloud/api/ApiServerTest.java
@@ -17,14 +17,20 @@
package com.cloud.api;
import com.cloud.domain.Domain;
+import com.cloud.exception.OriginDeniedException;
+import com.cloud.exception.PermissionDeniedException;
import com.cloud.user.Account;
+import com.cloud.user.AccountManager;
import com.cloud.user.User;
import com.cloud.user.UserAccount;
import com.cloud.utils.exception.CloudRuntimeException;
+import org.apache.cloudstack.config.ApiServiceConfiguration;
import org.apache.cloudstack.framework.config.ConfigKey;
import org.apache.cloudstack.user.UserPasswordResetManager;
+import org.junit.After;
import org.junit.AfterClass;
import org.junit.Assert;
+import org.junit.Before;
import org.junit.BeforeClass;
import org.junit.Test;
import org.junit.runner.RunWith;
@@ -33,9 +39,12 @@ import org.mockito.Mock;
import org.mockito.MockedConstruction;
import org.mockito.Mockito;
import org.mockito.junit.MockitoJUnitRunner;
+import org.springframework.test.util.ReflectionTestUtils;
import java.lang.reflect.Field;
+import java.net.InetAddress;
import java.util.ArrayList;
+import java.util.Collections;
import java.util.List;
import static
org.apache.cloudstack.user.UserPasswordResetManager.UserPasswordResetEnabled;
@@ -49,6 +58,12 @@ public class ApiServerTest {
@Mock
UserPasswordResetManager userPasswordResetManager;
+ @Mock
+ AccountManager accountMgr;
+
+ private static final String DEFAULT_CIDR_CHECKS_ENABLED =
ApiServiceConfiguration.ApiSourceCidrChecksEnabled.defaultValue();
+ private static final String DEFAULT_ALLOWED_CIDRS =
ApiServiceConfiguration.ApiAllowedSourceCidrList.defaultValue();
+
@BeforeClass
public static void beforeClass() throws Exception {
overrideDefaultConfigValue(UserPasswordResetEnabled, "_value", true);
@@ -59,6 +74,17 @@ public class ApiServerTest {
overrideDefaultConfigValue(UserPasswordResetEnabled, "_value", false);
}
+ @Before
+ public void setupCommandAvailableChecks() {
+ apiServer.setApiAccessCheckers(Collections.emptyList());
+ }
+
+ @After
+ public void resetCidrConfig() throws Exception {
+
overrideDefaultConfigValue(ApiServiceConfiguration.ApiSourceCidrChecksEnabled,
"_defaultValue", DEFAULT_CIDR_CHECKS_ENABLED);
+
overrideDefaultConfigValue(ApiServiceConfiguration.ApiAllowedSourceCidrList,
"_defaultValue", DEFAULT_ALLOWED_CIDRS);
+ }
+
private static void overrideDefaultConfigValue(final ConfigKey configKey,
final String name, final Object o) throws IllegalAccessException,
NoSuchFieldException {
Field f = ConfigKey.class.getDeclaredField(name);
f.setAccessible(true);
@@ -176,4 +202,47 @@ public class ApiServerTest {
Mockito.when(account.getApiKeyAccess()).thenReturn(null);
Assert.assertEquals(true, apiServer.verifyApiKeyAccessAllowed(user,
account));
}
+
+ @Test
+ public void testCheckCommandAvailableSkipsCidrLookupWhenDisabled() throws
Exception {
+
overrideDefaultConfigValue(ApiServiceConfiguration.ApiSourceCidrChecksEnabled,
"_defaultValue", "false");
+ User user = Mockito.mock(User.class);
+
+ ReflectionTestUtils.invokeMethod(apiServer, "checkCommandAvailable",
user, "listVirtualMachines", InetAddress.getByName("127.0.0.1"));
+
+ Mockito.verify(accountMgr,
Mockito.never()).getAccount(Mockito.anyLong());
+ }
+
+ @Test
+ public void testCheckCommandAvailableAllowsMatchingCidr() throws Exception
{
+
overrideDefaultConfigValue(ApiServiceConfiguration.ApiSourceCidrChecksEnabled,
"_defaultValue", "true");
+
overrideDefaultConfigValue(ApiServiceConfiguration.ApiAllowedSourceCidrList,
"_defaultValue", "127.0.0.1/32");
+ User user = Mockito.mock(User.class);
+ Mockito.when(user.getAccountId()).thenReturn(1L);
+ Account account = Mockito.mock(Account.class);
+ Mockito.when(account.getId()).thenReturn(1L);
+ Mockito.when(accountMgr.getAccount(1L)).thenReturn(account);
+
+ ReflectionTestUtils.invokeMethod(apiServer, "checkCommandAvailable",
user, "listVirtualMachines", InetAddress.getByName("127.0.0.1"));
+
+ Mockito.verify(accountMgr).getAccount(1L);
+ }
+
+ @Test(expected = OriginDeniedException.class)
+ public void testCheckCommandAvailableDeniesNonMatchingCidr() throws
Exception {
+
overrideDefaultConfigValue(ApiServiceConfiguration.ApiSourceCidrChecksEnabled,
"_defaultValue", "true");
+
overrideDefaultConfigValue(ApiServiceConfiguration.ApiAllowedSourceCidrList,
"_defaultValue", "10.0.0.0/8");
+ User user = Mockito.mock(User.class);
+ Mockito.when(user.getAccountId()).thenReturn(1L);
+ Account account = Mockito.mock(Account.class);
+ Mockito.when(account.getId()).thenReturn(1L);
+ Mockito.when(accountMgr.getAccount(1L)).thenReturn(account);
+
+ ReflectionTestUtils.invokeMethod(apiServer, "checkCommandAvailable",
user, "listVirtualMachines", InetAddress.getByName("127.0.0.1"));
+ }
+
+ @Test(expected = PermissionDeniedException.class)
+ public void testCheckCommandAvailableThrowsWhenUserNull() throws Exception
{
+ ReflectionTestUtils.invokeMethod(apiServer, "checkCommandAvailable",
null, "listVirtualMachines", InetAddress.getByName("127.0.0.1"));
+ }
}
diff --git a/utils/src/main/java/com/cloud/utils/DateUtil.java
b/utils/src/main/java/com/cloud/utils/DateUtil.java
index 00ae5565dad..73036285cdc 100644
--- a/utils/src/main/java/com/cloud/utils/DateUtil.java
+++ b/utils/src/main/java/com/cloud/utils/DateUtil.java
@@ -19,17 +19,16 @@
package com.cloud.utils;
-import java.text.DateFormat;
import java.text.ParseException;
-import java.text.SimpleDateFormat;
-import java.time.LocalDateTime;
-import java.time.ZoneId;
import java.time.ZonedDateTime;
import java.time.YearMonth;
import java.util.Calendar;
import java.util.Date;
import java.util.TimeZone;
+import java.util.concurrent.ConcurrentHashMap;
+import java.time.LocalDateTime;
+import java.time.ZoneId;
import java.time.format.DateTimeFormatter;
import java.time.format.DateTimeParseException;
import java.time.OffsetDateTime;
@@ -49,7 +48,11 @@ public class DateUtil {
public static final TimeZone GMT_TIMEZONE = TimeZone.getTimeZone("GMT");
public static final String YYYYMMDD_FORMAT = "yyyyMMddHHmmss";
public static final String ZONED_DATETIME_FORMAT =
"yyyy-MM-dd'T'HH:mm:ssZ";
- private static final DateFormat ZONED_DATETIME_SIMPLE_FORMATTER = new
SimpleDateFormat(ZONED_DATETIME_FORMAT);
+
+ private static final DateTimeFormatter OUTPUT_FORMATTER =
+
DateTimeFormatter.ofPattern(ZONED_DATETIME_FORMAT).withZone(ZoneId.systemDefault());
+
+ private static final ConcurrentHashMap<String, DateTimeFormatter>
s_formatterCache = new ConcurrentHashMap<>();
private static final DateTimeFormatter[] parseFormats = new
DateTimeFormatter[]{
DateTimeFormatter.ISO_OFFSET_DATE_TIME,
@@ -68,6 +71,11 @@ public class DateUtil {
return new Date();
}
+ private static DateTimeFormatter getFormatter(String pattern, ZoneId zone)
{
+ String key = pattern + "|" + zone.getId();
+ return s_formatterCache.computeIfAbsent(key, k ->
DateTimeFormatter.ofPattern(pattern).withZone(zone));
+ }
+
public static Date parseTZDateString(String str) throws ParseException {
for (DateTimeFormatter formatter : parseFormats) {
try {
@@ -85,13 +93,12 @@ public class DateUtil {
}
public static Date parseDateString(TimeZone tz, String dateString, String
formatString) {
- DateFormat df = new SimpleDateFormat(formatString);
- df.setTimeZone(tz);
-
+ ZoneId zoneId = tz.toZoneId();
+ DateTimeFormatter formatter = getFormatter(formatString, zoneId);
try {
- return df.parse(dateString);
- } catch (ParseException e) {
- throw new CloudRuntimeException("why why ", e);
+ return Date.from(LocalDateTime.parse(dateString,
formatter).atZone(zoneId).toInstant());
+ } catch (DateTimeParseException e) {
+ throw new CloudRuntimeException("Failed to parse date string: " +
dateString, e);
}
}
@@ -108,21 +115,14 @@ public class DateUtil {
return null;
}
- DateFormat df = new SimpleDateFormat(formatString);
- df.setTimeZone(tz);
-
- return df.format(time);
+ return getFormatter(formatString,
tz.toZoneId()).format(time.toInstant());
}
public static String getOutputString(Date date) {
if (date == null) {
return "";
}
- String formattedString;
- synchronized (ZONED_DATETIME_SIMPLE_FORMATTER) {
- formattedString = ZONED_DATETIME_SIMPLE_FORMATTER.format(date);
- }
- return formattedString;
+ return OUTPUT_FORMATTER.format(date.toInstant());
}
public static Date now() {
@@ -155,7 +155,7 @@ public class DateUtil {
/**
* Return next run time
- * @param intervalType hourly/daily/weekly/monthly
+ * @param type hourly/daily/weekly/monthly
* @param schedule MM[:HH][:DD] format. DD is day of week for weekly and
day of month for monthly
* @param timezone The timezone in which the schedule string is specified
* @param startDate if specified, returns next run time after the
specified startDate
@@ -177,7 +177,8 @@ public class DateUtil {
int minutes = 0;
int hour = 0;
int day = 0;
- Date execDate = null;
+ Date execDate;
+ Date now = new Date();
switch (type) {
case HOURLY:
@@ -199,7 +200,7 @@ public class DateUtil {
// During testing we use a test clock which runs much faster
than the real clock
// So startDate and execDate will always be ahead in the future
// and we will never increase the time here
- if (execDate.before(new Date()) || !execDate.after(startDate))
{
+ if (execDate.before(now) || !execDate.after(startDate)) {
scheduleTime.add(Calendar.HOUR_OF_DAY, 1);
}
break;
@@ -225,7 +226,7 @@ public class DateUtil {
// During testing we use a test clock which runs much faster
than the real clock
// So startDate and execDate will always be ahead in the future
// and we will never increase the time here
- if (execDate.before(new Date()) || !execDate.after(startDate))
{
+ if (execDate.before(now) || !execDate.after(startDate)) {
scheduleTime.add(Calendar.DAY_OF_YEAR, 1);
}
break;
@@ -252,10 +253,9 @@ public class DateUtil {
// During testing we use a test clock which runs much faster
than the real clock
// So startDate and execDate will always be ahead in the future
// and we will never increase the time here
- if (execDate.before(new Date()) || !execDate.after(startDate))
{
+ if (execDate.before(now) || !execDate.after(startDate)) {
scheduleTime.add(Calendar.DAY_OF_WEEK, 7);
}
- ;
break;
case MONTHLY:
if (scheduleParts.length < 3) {
@@ -283,7 +283,7 @@ public class DateUtil {
// During testing we use a test clock which runs much faster
than the real clock
// So startDate and execDate will always be ahead in the future
// and we will never increase the time here
- if (execDate.before(new Date()) || !execDate.after(startDate))
{
+ if (execDate.before(now) || !execDate.after(startDate)) {
scheduleTime.add(Calendar.MONTH, 1);
}
break;
@@ -302,14 +302,7 @@ public class DateUtil {
}
public static long getTimeDifference(Date date1, Date date2){
-
- Calendar dateCalendar1 = Calendar.getInstance();
- dateCalendar1.setTime(date1);
- Calendar dateCalendar2 = Calendar.getInstance();
- dateCalendar2.setTime(date2);
-
- return (dateCalendar1.getTimeInMillis() -
dateCalendar2.getTimeInMillis() )/1000;
-
+ return (date1.getTime() - date2.getTime()) / 1000;
}
public static CronExpression parseSchedule(String schedule) {
diff --git a/utils/src/test/java/com/cloud/utils/DateUtilTest.java
b/utils/src/test/java/com/cloud/utils/DateUtilTest.java
index 98b4d11c9d7..77079986cbe 100644
--- a/utils/src/test/java/com/cloud/utils/DateUtilTest.java
+++ b/utils/src/test/java/com/cloud/utils/DateUtilTest.java
@@ -18,30 +18,36 @@
//
package com.cloud.utils;
+import com.cloud.utils.DateUtil.IntervalType;
+import com.cloud.utils.exception.CloudRuntimeException;
+import org.junit.Test;
+
import java.text.DateFormat;
import java.text.ParseException;
import java.text.SimpleDateFormat;
import java.time.Instant;
+import java.time.OffsetDateTime;
import java.time.ZoneId;
+import java.time.format.DateTimeFormatter;
import java.util.Calendar;
import java.util.Date;
import java.util.TimeZone;
-
-import java.time.format.DateTimeFormatter;
-import java.time.OffsetDateTime;
-
-import com.cloud.utils.DateUtil.IntervalType;
-
-import org.junit.Test;
-
import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertTrue;
public class DateUtilTest {
+ private static final String TEST_DATE_GMT = "2023-06-15 10:30:00";
+ private static final String TEST_DATE_EST = "2023-06-15 05:30:00";
+ private static final String TEST_DATE_ISO = "2023-06-15T10:30:00Z";
+ private static final String TEST_DATE_YYMMDD = "20230615103000";
+ private static final TimeZone GMT = DateUtil.GMT_TIMEZONE;
+ private static final TimeZone EST = TimeZone.getTimeZone("EST");
+
// command line test tool
public static void main(String[] args) {
TimeZone localTimezone = Calendar.getInstance().getTimeZone();
- TimeZone gmtTimezone = TimeZone.getTimeZone("GMT");
- TimeZone estTimezone = TimeZone.getTimeZone("EST");
+ TimeZone gmtTimezone = GMT;
+ TimeZone estTimezone = EST;
Date time = new Date();
System.out.println("local time :" +
DateUtil.getDateDisplayString(localTimezone, time));
@@ -49,7 +55,9 @@ public class DateUtilTest {
System.out.println("EST time :" +
DateUtil.getDateDisplayString(estTimezone, time));
//Test next run time. Expects interval and schedule as arguments
if (args.length == 2) {
- System.out.println("Next run time: " +
DateUtil.getNextRunTime(IntervalType.getIntervalType(args[0]), args[1], "GMT",
time).toString());
+ System.out.println("Next run time: " +
DateUtil.getNextRunTime(IntervalType.getIntervalType(args[0]),
+ args[1], "GMT", time)
+ .toString());
}
}
@@ -130,4 +138,87 @@ public class DateUtilTest {
assertEquals(str, time.toString(), dtParsed.toString());
}
+
+ @Test
+ public void parseDateStringDefaultFormat() {
+ TimeZone gmt = GMT;
+ Date date = DateUtil.parseDateString(gmt, TEST_DATE_GMT);
+ assertEquals(TEST_DATE_GMT, DateUtil.getDateDisplayString(gmt, date));
+ }
+
+ @Test
+ public void parseDateStringInterpretedInRequestedTimezone() {
+ TimeZone est = EST;
+ Date date = DateUtil.parseDateString(est, TEST_DATE_EST);
+ assertEquals(TEST_DATE_GMT, DateUtil.getDateDisplayString(GMT, date));
+ }
+
+ @Test
+ public void parseDateStringCustomFormat() {
+ TimeZone gmt = GMT;
+ Date date = DateUtil.parseDateString(gmt, TEST_DATE_YYMMDD,
DateUtil.YYYYMMDD_FORMAT);
+ assertEquals(TEST_DATE_GMT, DateUtil.getDateDisplayString(gmt, date));
+ }
+
+ @Test(expected = CloudRuntimeException.class)
+ public void parseDateStringInvalidInputThrows() {
+ DateUtil.parseDateString(GMT, "not-a-date");
+ }
+
+ @Test(expected = CloudRuntimeException.class)
+ public void parseDateStringFormatMismatchThrows() {
+ DateUtil.parseDateString(GMT, TEST_DATE_GMT, DateUtil.YYYYMMDD_FORMAT);
+ }
+
+ @Test
+ public void displayDateInTimezoneGmt() {
+ Date date = Date.from(Instant.parse(TEST_DATE_ISO));
+ assertEquals("2023-06-15T10:30:00+0000",
DateUtil.displayDateInTimezone(GMT, date));
+ }
+
+ @Test
+ public void displayDateInTimezoneEst() {
+ Date date = Date.from(Instant.parse(TEST_DATE_ISO));
+ assertEquals("2023-06-15T05:30:00-0500",
DateUtil.displayDateInTimezone(EST, date));
+ }
+
+ @Test
+ public void getDateDisplayStringGmt() {
+ Date date = Date.from(Instant.parse(TEST_DATE_ISO));
+ assertEquals(TEST_DATE_GMT, DateUtil.getDateDisplayString(GMT, date));
+ }
+
+ @Test
+ public void getDateDisplayStringTimezoneShift() {
+ Date date = Date.from(Instant.parse(TEST_DATE_ISO));
+ assertEquals(TEST_DATE_EST, DateUtil.getDateDisplayString(EST, date));
+ }
+
+ @Test
+ public void getDateDisplayStringCustomFormat() {
+ Date date = Date.from(Instant.parse(TEST_DATE_ISO));
+ assertEquals(TEST_DATE_YYMMDD, DateUtil.getDateDisplayString(GMT,
date, DateUtil.YYYYMMDD_FORMAT));
+ }
+
+ @Test
+ public void getDateDisplayStringNullDate() {
+ assertEquals(null, DateUtil.getDateDisplayString(GMT, null));
+ }
+
+ @Test
+ public void displayDateInTimezoneNullDate() {
+ assertEquals(null, DateUtil.displayDateInTimezone(GMT, null));
+ }
+
+ @Test
+ public void getOutputStringNull() {
+ assertEquals("", DateUtil.getOutputString(null));
+ }
+
+ @Test
+ public void getOutputStringNonNull() {
+ Date date = Date.from(Instant.parse(TEST_DATE_ISO));
+ String result = DateUtil.getOutputString(date);
+
assertTrue(result.matches("\\d{4}-\\d{2}-\\d{2}T\\d{2}:\\d{2}:\\d{2}[+-]\\d{4}"));
+ }
}