diff --git a/package-lock.json b/package-lock.json index 4f806e283d0c8e34f7e5a3a888f5cb88dcc1f993..98d48f4c4c03c687554409041d59f9818032d1d3 100644 --- a/package-lock.json +++ b/package-lock.json @@ -112,6 +112,19 @@ "path-exists": "^3.0.0" } }, + "moment": { + "version": "2.24.0", + "resolved": "https://registry.npmjs.org/moment/-/moment-2.24.0.tgz", + "integrity": "sha512-bV7f+6l2QigeBBZSM/6yTNq4P2fNpSWj/0e7jQcy87A8e7o2nAfP/34/2ky5Vw4B9S446EtIhodAzkFCcR4dQg==" + }, + "moment-timezone": { + "version": "0.5.27", + "resolved": "https://registry.npmjs.org/moment-timezone/-/moment-timezone-0.5.27.tgz", + "integrity": "sha512-EIKQs7h5sAsjhPCqN6ggx6cEbs94GK050254TIJySD1bzoM5JTYDwAU1IoVOeTOL6Gm27kYJ51/uuvq1kIlrbw==", + "requires": { + "moment": ">= 2.9.0" + } + }, "ms": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", diff --git a/package.json b/package.json index 5993bd71b2e9c9111a67b8bc2620f284651802ae..0de67f033cde395d4c5a6f2d7628654ec8d6f34f 100644 --- a/package.json +++ b/package.json @@ -11,6 +11,7 @@ "license": "EPL-2.0", "dependencies": { "axios": "^0.19.0", + "moment-timezone": "^0.5.27", "random-words": "^1.1.0", "uuid": "^3.3.3", "yargs": "^14.0.0" diff --git a/src/main/java/org/eclipsefoundation/marketplace/dto/Listing.java b/src/main/java/org/eclipsefoundation/marketplace/dto/Listing.java index e70d9fa81f10cf3afebdeded9373962e21c2e652..2a76e21563ef2a4932f55073d02067e4eb8b8c85 100644 --- a/src/main/java/org/eclipsefoundation/marketplace/dto/Listing.java +++ b/src/main/java/org/eclipsefoundation/marketplace/dto/Listing.java @@ -48,11 +48,11 @@ public class Listing extends NodeBase { @SortableField(name = DatabaseFieldNames.CREATION_DATE) @JsonbProperty(DatabaseFieldNames.CREATION_DATE) - private long creationDate; + private String creationDate; @SortableField(name = DatabaseFieldNames.UPDATE_DATE) @JsonbProperty(DatabaseFieldNames.UPDATE_DATE) - private long updateDate; + private String updateDate; @JsonbProperty(DatabaseFieldNames.LICENSE_TYPE) private String license; private List<String> marketIds; @@ -224,28 +224,28 @@ public class Listing extends NodeBase { /** * @return the creationDate */ - public long getCreationDate() { + public String getCreationDate() { return creationDate; } /** * @param creationDate the creationDate to set */ - public void setCreationDate(long creationDate) { + public void setCreationDate(String creationDate) { this.creationDate = creationDate; } /** * @return the updateDate */ - public long getUpdateDate() { + public String getUpdateDate() { return updateDate; } /** * @param updateDate the updateDate to set */ - public void setUpdateDate(long updateDate) { + public void setUpdateDate(String updateDate) { this.updateDate = updateDate; } diff --git a/src/main/java/org/eclipsefoundation/marketplace/dto/codecs/ListingCodec.java b/src/main/java/org/eclipsefoundation/marketplace/dto/codecs/ListingCodec.java index 9265d119d3df81cc39c7fa914fd1204d7dff4901..c8f956adbfdb9d5d96a26b6ab17579c4cc756daf 100644 --- a/src/main/java/org/eclipsefoundation/marketplace/dto/codecs/ListingCodec.java +++ b/src/main/java/org/eclipsefoundation/marketplace/dto/codecs/ListingCodec.java @@ -6,7 +6,6 @@ */ package org.eclipsefoundation.marketplace.dto.codecs; -import java.util.Date; import java.util.UUID; import java.util.stream.Collectors; @@ -25,6 +24,7 @@ import org.eclipsefoundation.marketplace.dto.converters.CategoryConverter; import org.eclipsefoundation.marketplace.dto.converters.OrganizationConverter; import org.eclipsefoundation.marketplace.dto.converters.ListingVersionConverter; import org.eclipsefoundation.marketplace.dto.converters.TagConverter; +import org.eclipsefoundation.marketplace.helper.DateTimeHelper; import org.eclipsefoundation.marketplace.namespace.DatabaseFieldNames; import com.mongodb.MongoClient; @@ -76,13 +76,13 @@ public class ListingCodec implements CollectibleCodec<Listing> { doc.put(DatabaseFieldNames.TOTAL_NSTALLS, value.getInstallsTotal()); doc.put(DatabaseFieldNames.LICENSE_TYPE, value.getLicense()); doc.put(DatabaseFieldNames.LISTING_STATUS, value.getStatus()); - doc.put(DatabaseFieldNames.UPDATE_DATE, new Date(value.getUpdateDate())); - doc.put(DatabaseFieldNames.CREATION_DATE, new Date(value.getCreationDate())); + doc.put(DatabaseFieldNames.UPDATE_DATE, DateTimeHelper.toRFC3339(value.getUpdateDate())); + doc.put(DatabaseFieldNames.CREATION_DATE, DateTimeHelper.toRFC3339(value.getCreationDate())); doc.put(DatabaseFieldNames.FOUNDATION_MEMBER_FLAG, value.isFoundationMember()); doc.put(DatabaseFieldNames.CATEGORY_IDS, value.getCategoryIds()); doc.put(DatabaseFieldNames.SCREENSHOTS, value.getScreenshots()); doc.put(DatabaseFieldNames.MARKET_IDS, value.getMarketIds()); - + // for nested document types, use the converters to safely transform into BSON // documents doc.put(DatabaseFieldNames.LISTING_ORGANIZATIONS, organizationConverter.convert(value.getOrganization())); @@ -102,7 +102,7 @@ public class ListingCodec implements CollectibleCodec<Listing> { public Listing decode(BsonReader reader, DecoderContext decoderContext) { Document document = documentCodec.decode(reader, decoderContext); Listing out = new Listing(); - + // for each field, get the value from the encoded object and set it in POJO out.setId(document.getString(DatabaseFieldNames.DOCID)); out.setTitle(document.getString(DatabaseFieldNames.TITLE)); @@ -133,9 +133,9 @@ public class ListingCodec implements CollectibleCodec<Listing> { out.setCategories(document.getList(DatabaseFieldNames.LISTING_CATEGORIES, Document.class).stream() .map(categoryConverter::convert).collect(Collectors.toList())); - // convert date to epoch milli - out.setCreationDate(document.getDate(DatabaseFieldNames.CREATION_DATE).toInstant().toEpochMilli()); - out.setUpdateDate(document.getDate(DatabaseFieldNames.UPDATE_DATE).toInstant().toEpochMilli()); + // convert date to date string + out.setCreationDate(DateTimeHelper.toRFC3339(document.getDate(DatabaseFieldNames.CREATION_DATE))); + out.setUpdateDate(DateTimeHelper.toRFC3339(document.getDate(DatabaseFieldNames.UPDATE_DATE))); return out; } diff --git a/src/main/java/org/eclipsefoundation/marketplace/helper/DateTimeHelper.java b/src/main/java/org/eclipsefoundation/marketplace/helper/DateTimeHelper.java new file mode 100644 index 0000000000000000000000000000000000000000..d6b4157d6c445e202a21fb8f5f9e6221417d3ac1 --- /dev/null +++ b/src/main/java/org/eclipsefoundation/marketplace/helper/DateTimeHelper.java @@ -0,0 +1,61 @@ +/* Copyright (c) 2019 Eclipse Foundation and others. + * This program and the accompanying materials are made available + * under the terms of the Eclipse Public License 2.0 + * which is available at http://www.eclipse.org/legal/epl-v20.html, + * SPDX-License-Identifier: EPL-2.0 + */ +package org.eclipsefoundation.marketplace.helper; + +import java.time.ZoneId; +import java.time.ZonedDateTime; +import java.time.format.DateTimeFormatter; +import java.time.format.DateTimeParseException; +import java.util.Date; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * Central implementation for handling date time conversion in the service. + * Class uses Java8 DateTime formatters, creating an internal format that + * represents RFC 3339 + * + * @author Martin Lowe + */ +public class DateTimeHelper { + private static final Logger LOGGER = LoggerFactory.getLogger(DateTimeHelper.class); + private static final DateTimeFormatter formatter = DateTimeFormatter.ofPattern("uuuu-MM-dd'T'HH:mm:ssXXX"); + + /** + * Converts RFC 3339 compliant date string to date object. If non compliant + * string is passed, issue is logged and null is returned. If negative UTC + * timezone (-00:00) is passed, UTC time zone is assumed. + * + * @param dateString an RFC 3339 date string. + * @return a date object representing time in date string, or null if not in RFC + * 3339 format. + */ + public static Date toRFC3339(String dateString) { + try { + return Date.from(ZonedDateTime.parse(dateString, formatter).toInstant()); + } catch (DateTimeParseException e) { + LOGGER.warn("Could not parse date from string '{}'", dateString, e); + return null; + } + } + + /** + * Converts passed date to RFC 3339 compliant date string. Time is adjusted to + * be in UTC time. + * + * @param date the date object to convert to RFC 3339 format. + * @return the RFC 3339 format date string. + */ + public static String toRFC3339(Date date) { + return formatter.format(date.toInstant().atZone(ZoneId.of("UTC"))); + } + + // hide constructor + private DateTimeHelper() { + } +} diff --git a/src/main/node/index.js b/src/main/node/index.js index 441bdecf9954b16025abc089995a6cf38ab059c5..bb88462e57c5451747ee3d7d7516ea5f758eceaa 100644 --- a/src/main/node/index.js +++ b/src/main/node/index.js @@ -26,6 +26,7 @@ const argv = require('yargs') }).argv; let max = argv.c; +var moment = require('moment-timezone'); const lic_types = ["EPL-2.0","EPL-1.0","GPL"]; const platforms = ["windows","macos","linux"]; const eclipseVs = ["4.6","4.7","4.8","4.9","4.10","4.11","4.12"]; @@ -143,6 +144,8 @@ function generateJSON(id) { "status": "draft", "support_url": "https://jakarta.ee/about/faq", "license_type": lic_types[Math.floor(Math.random()*lic_types.length)], + "created": moment.tz((new Date()).toISOString(), "America/Toronto").format(), + "changed": moment.tz((new Date()).toISOString(), "America/Toronto").format(), "authors": [ { "full_name": "Martin Lowe", diff --git a/src/test/java/org/eclipsefoundation/marketplace/helper/DateTimeHelperTest.java b/src/test/java/org/eclipsefoundation/marketplace/helper/DateTimeHelperTest.java new file mode 100644 index 0000000000000000000000000000000000000000..084893bede63eb7bd65eef1ff8656ca4605f8727 --- /dev/null +++ b/src/test/java/org/eclipsefoundation/marketplace/helper/DateTimeHelperTest.java @@ -0,0 +1,100 @@ +/* Copyright (c) 2019 Eclipse Foundation and others. + * This program and the accompanying materials are made available + * under the terms of the Eclipse Public License 2.0 + * which is available at http://www.eclipse.org/legal/epl-v20.html, + * SPDX-License-Identifier: EPL-2.0 + */ +package org.eclipsefoundation.marketplace.helper; + +import java.time.ZoneId; +import java.util.Calendar; +import java.util.Date; +import java.util.TimeZone; +import java.util.concurrent.TimeUnit; + +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.Test; + +import io.quarkus.test.junit.QuarkusTest; + +/** + * Test class for {@linkplain DateTimeHelper} + * + * @author Martin Lowe + */ +@QuarkusTest +public class DateTimeHelperTest { + + @Test + public void validRFC3339DateStringIn() { + // Test standard time + String dateString = "1996-12-19T16:39:57-08:00"; + // Timezone needs to be set as otherwise it defaults to system local offset. + TimeZone tz = TimeZone.getTimeZone(ZoneId.of("GMT-08:00")); + Calendar c = Calendar.getInstance(tz); + + Date d = DateTimeHelper.toRFC3339(dateString); + c.setTime(d); + + Assertions.assertEquals(1996, c.get(Calendar.YEAR)); + // 0 based month + Assertions.assertEquals(11, c.get(Calendar.MONTH)); + Assertions.assertEquals(19, c.get(Calendar.DATE)); + Assertions.assertEquals(16, c.get(Calendar.HOUR_OF_DAY)); + Assertions.assertEquals(39, c.get(Calendar.MINUTE)); + Assertions.assertEquals(57, c.get(Calendar.SECOND)); + Assertions.assertEquals(-TimeUnit.MILLISECONDS.convert(8, TimeUnit.HOURS), + c.get(Calendar.ZONE_OFFSET) + c.get(Calendar.DST_OFFSET)); + + // Test unknown offset time + String unknownOffsetDateString = "1996-12-19T16:39:57-00:00"; + c = Calendar.getInstance(TimeZone.getTimeZone(ZoneId.of("UTC"))); + + // set calendar to actual value for easy reading + d = DateTimeHelper.toRFC3339(unknownOffsetDateString); + c.setTime(d); + + System.out.println(c.toInstant().toString()); + Assertions.assertEquals(1996, c.get(Calendar.YEAR)); + // 0 based month + Assertions.assertEquals(11, c.get(Calendar.MONTH)); + Assertions.assertEquals(19, c.get(Calendar.DATE)); + Assertions.assertEquals(16, c.get(Calendar.HOUR_OF_DAY)); + Assertions.assertEquals(39, c.get(Calendar.MINUTE)); + Assertions.assertEquals(57, c.get(Calendar.SECOND)); + Assertions.assertEquals(0, c.get(Calendar.ZONE_OFFSET) + c.get(Calendar.DST_OFFSET)); + } + + @Test + public void validRFC3339DateStringOut() { + // Set a time equal to "1996-12-19T16:39:57-08:00" + // Timezone needs to be set as otherwise it defaults to system local offset. + TimeZone tz = TimeZone.getTimeZone(ZoneId.of("GMT-08:00")); + // create a calendar instance to represent the given date + Calendar c = Calendar.getInstance(tz); + c.set(1996, 11, 19, 16, 39, 57); + + // expect UTC time in return + String expected = "1996-12-20T00:39:57Z"; + String actual = DateTimeHelper.toRFC3339(c.getTime()); + + Assertions.assertEquals(expected, actual); + } + + @Test + public void invalidRFC3339DateStringIn() { + // test various permutations of the date string to ensure format enforcement + Assertions.assertNull(DateTimeHelper.toRFC3339("19991220"), + "Expected no returned date object for string: 19991220"); + Assertions.assertNull(DateTimeHelper.toRFC3339("1996/12/20"), + "Expected no returned date object for string: 1996/12/20"); + Assertions.assertNull(DateTimeHelper.toRFC3339("1996-12-20"), + "Expected no returned date object for string: 1996-12-20"); + Assertions.assertNull(DateTimeHelper.toRFC3339("1996-12-20 16:39:57-08:00"), + "Expected no returned date object for string: 1996-12-20 16:39:57-08:00"); + Assertions.assertNull(DateTimeHelper.toRFC3339("1996-12-20T16:39:57"), + "Expected no returned date object for string: 1996-12-20T16:39:57"); + Assertions.assertNull(DateTimeHelper.toRFC3339("1996-12-20T16:39:57-0800"), + "Expected no returned date object for string: 1996-12-20T16:39:57-0800"); + } +}