diff --git a/server/src/main/java/com/cloud/api/ApiServer.java b/server/src/main/java/com/cloud/api/ApiServer.java index a97984a2be2..a30718b0650 100644 --- a/server/src/main/java/com/cloud/api/ApiServer.java +++ b/server/src/main/java/com/cloud/api/ApiServer.java @@ -42,6 +42,7 @@ import com.cloud.user.User; import com.cloud.user.UserAccount; import com.cloud.user.UserVO; import com.cloud.utils.ConstantTimeComparator; +import com.cloud.utils.DateUtil; import com.cloud.utils.HttpUtils; import com.cloud.utils.NumbersUtil; import com.cloud.utils.Pair; @@ -166,9 +167,7 @@ import java.net.URISyntaxException; import java.net.URLEncoder; import java.security.SecureRandom; import java.security.Security; -import java.text.DateFormat; import java.text.ParseException; -import java.text.SimpleDateFormat; import java.util.ArrayList; import java.util.Collections; import java.util.Date; @@ -228,7 +227,6 @@ public class ApiServer extends ManagerBase implements HttpRequestHandler, ApiSer private ApiAsyncJobDispatcher asyncDispatcher; private static int s_workerCount = 0; - private static final DateFormat DateFormatToUse = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ssZ"); private static Map>> s_apiNameCmdClassMap = new HashMap>>(); private static ExecutorService s_executor = new ThreadPoolExecutor(10, 150, 60, TimeUnit.SECONDS, new LinkedBlockingQueue(), new NamedThreadFactory( @@ -883,14 +881,14 @@ public class ApiServer extends ManagerBase implements HttpRequestHandler, ApiSer s_logger.debug("Missing Expires parameter -- ignoring request. Signature: " + signature + ", apiKey: " + apiKey); return false; } - synchronized (DateFormatToUse) { - try { - expiresTS = DateFormatToUse.parse(expires); - } catch (final ParseException pe) { - s_logger.debug("Incorrect date format for Expires parameter", pe); - return false; - } + + try { + expiresTS = DateUtil.parseTZDateString(expires); + } catch (final ParseException pe) { + s_logger.debug("Incorrect date format for Expires parameter", pe); + return false; } + final Date now = new Date(System.currentTimeMillis()); if (expiresTS.before(now)) { s_logger.debug("Request expired -- ignoring ...sig: " + signature + ", apiKey: " + apiKey); diff --git a/utils/src/main/java/com/cloud/utils/DateUtil.java b/utils/src/main/java/com/cloud/utils/DateUtil.java index 9f046d11446..8c55268782e 100644 --- a/utils/src/main/java/com/cloud/utils/DateUtil.java +++ b/utils/src/main/java/com/cloud/utils/DateUtil.java @@ -26,6 +26,10 @@ import java.util.Calendar; import java.util.Date; import java.util.TimeZone; +import java.time.format.DateTimeFormatter; +import java.time.format.DateTimeParseException; +import java.time.OffsetDateTime; + import com.cloud.utils.exception.CloudRuntimeException; public class DateUtil { @@ -33,19 +37,33 @@ public class DateUtil { public static final String YYYYMMDD_FORMAT = "yyyyMMddHHmmss"; private static final DateFormat s_outputFormat = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ssZ"); + private static final DateTimeFormatter[] parseFormats = new DateTimeFormatter[]{ + DateTimeFormatter.ISO_OFFSET_DATE_TIME, + DateTimeFormatter.ofPattern("yyyy-MM-dd'T'HH:mm:ssZ"), + DateTimeFormatter.ISO_INSTANT, + // with milliseconds + DateTimeFormatter.ofPattern("yyyy-MM-dd'T'HH:mm:ss.SSSSSSX"), + DateTimeFormatter.ofPattern("yyyy-MM-dd'T'HH:mm:ss.SSSSSSZ"), + DateTimeFormatter.ofPattern("yyyy-MM-dd'T'HH:mm:ss.SSSSSS'Z'"), + // legacy and non-sensical format + DateTimeFormatter.ofPattern("yyyy-MM-dd'T'HH:mm:ss'Z'Z") + }; + public static Date currentGMTTime() { // Date object always stores miliseconds offset based on GMT internally return new Date(); } - // yyyy-MM-ddTHH:mm:ssZZZZ or yyyy-MM-ddTHH:mm:ssZxxxx public static Date parseTZDateString(String str) throws ParseException { - try { - return s_outputFormat.parse(str); - } catch (ParseException e) { - final DateFormat dfParse = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss'Z'Z"); - return dfParse.parse(str); + for (DateTimeFormatter formatter : parseFormats) { + try { + OffsetDateTime dt = OffsetDateTime.parse(str, formatter); + return Date.from(dt.toInstant()); + } catch (DateTimeParseException e) { + // do nothing + } } + throw new ParseException("Unparseable date: \"" + str + "\"", 0); } public static Date parseDateString(TimeZone tz, String dateString) { diff --git a/utils/src/test/java/com/cloud/utils/DateUtilTest.java b/utils/src/test/java/com/cloud/utils/DateUtilTest.java index 190adeab2db..428666f0736 100644 --- a/utils/src/test/java/com/cloud/utils/DateUtilTest.java +++ b/utils/src/test/java/com/cloud/utils/DateUtilTest.java @@ -25,6 +25,10 @@ import java.util.Calendar; import java.util.Date; import java.util.TimeZone; +import java.time.format.DateTimeFormatter; +import java.time.OffsetDateTime; +import java.time.ZoneOffset; + import com.cloud.utils.DateUtil.IntervalType; import org.junit.Test; @@ -32,7 +36,6 @@ import org.junit.Test; import static org.junit.Assert.assertEquals; public class DateUtilTest { - // command line test tool public static void main(String[] args) { TimeZone localTimezone = Calendar.getInstance().getTimeZone(); @@ -56,7 +59,7 @@ public class DateUtilTest { String str = dfDate.format(time); Date dtParsed = DateUtil.parseTZDateString(str); - assertEquals(time.toString(), dtParsed.toString()); + assertEquals(str, time.toString(), dtParsed.toString()); } @Test @@ -66,6 +69,59 @@ public class DateUtilTest { String str = dfDate.format(time); Date dtParsed = DateUtil.parseTZDateString(str); - assertEquals(time.toString(), dtParsed.toString()); + assertEquals(str, time.toString(), dtParsed.toString()); + } + + @Test + public void zonedTimeFormatIsoOffsetDateTime() throws ParseException { + Date time = new Date(); + String str = OffsetDateTime.now().format(DateTimeFormatter.ISO_OFFSET_DATE_TIME); + + Date dtParsed = DateUtil.parseTZDateString(str); + + assertEquals(str, time.toString(), dtParsed.toString()); + } + + @Test + public void zonedTimeFormatIsoInstant() throws ParseException { + Date time = new Date(); + String str = OffsetDateTime.now().format(DateTimeFormatter.ISO_INSTANT); + + Date dtParsed = DateUtil.parseTZDateString(str); + + assertEquals(str, time.toString(), dtParsed.toString()); + } + + @Test + public void zonedTimeFormatIsoOffsetDateTimeMs() throws ParseException { + Date time = new Date(); + DateTimeFormatter formatter = DateTimeFormatter.ofPattern("yyyy-MM-dd'T'HH:mm:ss.SSSSSSX"); + String str = OffsetDateTime.now().format(formatter); + + Date dtParsed = DateUtil.parseTZDateString(str); + + assertEquals(str, time.toString(), dtParsed.toString()); + } + + @Test + public void zonedTimeFormatIsoInstantMs() throws ParseException { + Date time = new Date(); + DateTimeFormatter formatter = DateTimeFormatter.ofPattern("yyyy-MM-dd'T'HH:mm:ss.SSSSSS'Z'"); + String str = OffsetDateTime.now(ZoneOffset.UTC).format(formatter); + + Date dtParsed = DateUtil.parseTZDateString(str); + + assertEquals(str, time.toString(), dtParsed.toString()); + } + + @Test + public void zonedTimeFormatIsoNoColonZMs() throws ParseException { + Date time = new Date(); + DateTimeFormatter formatter = DateTimeFormatter.ofPattern("yyyy-MM-dd'T'HH:mm:ss.SSSSSSZ"); + String str = OffsetDateTime.now().format(formatter); + + Date dtParsed = DateUtil.parseTZDateString(str); + + assertEquals(str, time.toString(), dtParsed.toString()); } }