diff --git a/cwms-data-api/src/main/java/cwms/cda/api/LocationController.java b/cwms-data-api/src/main/java/cwms/cda/api/LocationController.java index dd6ea03bf2..773e716723 100644 --- a/cwms-data-api/src/main/java/cwms/cda/api/LocationController.java +++ b/cwms-data-api/src/main/java/cwms/cda/api/LocationController.java @@ -59,6 +59,7 @@ import cwms.cda.api.errors.CdaError; import cwms.cda.api.errors.DeleteConflictException; import cwms.cda.api.errors.NotFoundException; +import cwms.cda.data.dao.JooqDao; import cwms.cda.data.dao.LocationsDao; import cwms.cda.data.dao.LocationsDaoImpl; import cwms.cda.data.dto.Location; @@ -66,6 +67,7 @@ import cwms.cda.formatters.ContentType; import cwms.cda.formatters.Formats; import cwms.cda.formatters.UnsupportedFormatException; +import cwms.cda.helpers.ZoneIdHelper; import io.javalin.apibuilder.CrudHandler; import io.javalin.core.util.Header; import io.javalin.http.Context; @@ -77,7 +79,6 @@ import io.javalin.plugin.openapi.annotations.OpenApiResponse; import java.io.IOException; import java.sql.SQLException; -import java.time.ZoneId; import java.util.List; import java.util.logging.Level; import java.util.logging.Logger; @@ -508,7 +509,7 @@ private Location getUpdatedLocation(Location existingLocation, Location updatedL String updatedOfficeId = updatedLocation.getOfficeId() == null ? existingLocation.getOfficeId() : updatedLocation.getOfficeId(); return new Location.Builder(updatedName, updatedLocationKind, - ZoneId.of(updatedTimeZoneId), updatedLatitude, updatedLongitude, + ZoneIdHelper.parseZoneIdWithAliases(updatedTimeZoneId), updatedLatitude, updatedLongitude, updatedHorizontalDatum, updatedOfficeId) .withActive(updatedIsActive) .withPublicName(updatedPublicName) diff --git a/cwms-data-api/src/main/java/cwms/cda/data/dao/JooqDao.java b/cwms-data-api/src/main/java/cwms/cda/data/dao/JooqDao.java index ecbff1221f..89fce14e3c 100644 --- a/cwms-data-api/src/main/java/cwms/cda/data/dao/JooqDao.java +++ b/cwms-data-api/src/main/java/cwms/cda/data/dao/JooqDao.java @@ -24,6 +24,11 @@ package cwms.cda.data.dao; +import java.io.IOException; +import java.io.InputStream; +import java.sql.Timestamp; +import java.time.Instant; + import static org.jooq.SQLDialect.ORACLE; import com.google.common.flogger.FluentLogger; @@ -48,8 +53,11 @@ import java.time.ZoneId; import java.util.Arrays; import java.util.Collections; +import java.util.HashMap; import java.util.List; +import java.util.Map; import java.util.Optional; +import java.util.Properties; import java.util.regex.Matcher; import java.util.regex.Pattern; import javax.servlet.http.HttpServletResponse; @@ -112,6 +120,7 @@ public DeleteRule getRule() { } } + protected JooqDao(DSLContext dsl) { super(dsl); } @@ -182,7 +191,7 @@ private static Connection setClientInfo(Context ctx, Connection connection) { try { final String apiVersion = ApiServlet.getApiVersion(); connection.setClientInfo("OCSID.ECID", - ApiServlet.APPLICATION_TITLE + " " + + ApiServlet.APPLICATION_TITLE + " " + apiVersion.substring(0,Math.min(ORACLE_ECID_MAX_LENGTH,apiVersion.length()))); if (ctx.handlerType() == HandlerType.BEFORE) { connection.setClientInfo("OCSID.MODULE", "BEFORE-HANDLER"); @@ -787,4 +796,5 @@ public static BigDecimal toBigDecimal(Number number) { public static double buildDouble(BigDecimal bigDecimal) { return (bigDecimal == null) ? 0.0 : bigDecimal.doubleValue(); } + } diff --git a/cwms-data-api/src/main/java/cwms/cda/data/dao/LocationsDaoImpl.java b/cwms-data-api/src/main/java/cwms/cda/data/dao/LocationsDaoImpl.java index 355b451d6e..4843aeae9e 100644 --- a/cwms-data-api/src/main/java/cwms/cda/data/dao/LocationsDaoImpl.java +++ b/cwms-data-api/src/main/java/cwms/cda/data/dao/LocationsDaoImpl.java @@ -64,6 +64,7 @@ import java.util.Set; import java.util.logging.Level; import java.util.logging.Logger; +import cwms.cda.helpers.ZoneIdHelper; import org.geojson.Feature; import org.geojson.FeatureCollection; import org.geojson.Point; @@ -92,6 +93,8 @@ public class LocationsDaoImpl extends JooqDao implements LocationsDao private static final Logger logger = Logger.getLogger(LocationsDaoImpl.class.getName()); private static final long DELETED_TS_MARKER = 0L; + + public LocationsDaoImpl(DSLContext dsl) { super(dsl); } @@ -148,7 +151,7 @@ private Location buildLocation(Record loc) { String timeZoneName = loc.get(AV_LOC.TIME_ZONE_NAME); // may be null... ZoneId zone = null; if (timeZoneName != null) { - zone = ZoneId.of(timeZoneName); + zone = ZoneIdHelper.parseZoneIdWithAliases(timeZoneName); } Double latDouble = null; diff --git a/cwms-data-api/src/main/java/cwms/cda/data/dao/location/kind/LocationUtil.java b/cwms-data-api/src/main/java/cwms/cda/data/dao/location/kind/LocationUtil.java index 8bd40df0ce..e5220b47ce 100644 --- a/cwms-data-api/src/main/java/cwms/cda/data/dao/location/kind/LocationUtil.java +++ b/cwms-data-api/src/main/java/cwms/cda/data/dao/location/kind/LocationUtil.java @@ -25,11 +25,12 @@ package cwms.cda.data.dao.location.kind; import cwms.cda.api.enums.Nation; +import cwms.cda.data.dao.JooqDao; import cwms.cda.data.dto.Location; import cwms.cda.data.dto.CwmsId; import cwms.cda.data.dto.LookupType; -import java.time.ZoneId; import java.util.Optional; +import cwms.cda.helpers.ZoneIdHelper; import usace.cwms.db.jooq.codegen.udt.records.LOCATION_OBJ_T; import usace.cwms.db.jooq.codegen.udt.records.LOCATION_REF_T; import usace.cwms.db.jooq.codegen.udt.records.LOOKUP_TYPE_OBJ_T; @@ -128,7 +129,7 @@ public static Location getLocation(LOCATION_OBJ_T location) { if (location != null) { retval = new Location.Builder(getLocationId(location.getLOCATION_REF()), location.getLOCATION_KIND_ID(), - ZoneId.of(location.getTIME_ZONE_NAME()), + ZoneIdHelper.parseZoneIdWithAliases(location.getTIME_ZONE_NAME()), buildDouble(location.getLATITUDE()), buildDouble(location.getLONGITUDE()), location.getHORIZONTAL_DATUM(), diff --git a/cwms-data-api/src/main/java/cwms/cda/formatters/json/JsonV1.java b/cwms-data-api/src/main/java/cwms/cda/formatters/json/JsonV1.java index d3442a70ab..bae1098bb2 100644 --- a/cwms-data-api/src/main/java/cwms/cda/formatters/json/JsonV1.java +++ b/cwms-data-api/src/main/java/cwms/cda/formatters/json/JsonV1.java @@ -6,6 +6,7 @@ import com.fasterxml.jackson.databind.ObjectMapper; import com.fasterxml.jackson.databind.PropertyNamingStrategies; import com.fasterxml.jackson.databind.SerializationFeature; +import com.fasterxml.jackson.databind.module.SimpleModule; import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule; import cwms.cda.data.dto.CwmsDTOBase; import cwms.cda.data.dto.Office; @@ -14,9 +15,11 @@ import cwms.cda.formatters.OfficeFormatV1; import cwms.cda.formatters.OutputFormatter; import cwms.cda.formatters.annotations.FormattableWith; +import cwms.cda.formatters.json.adapters.ZoneIdDeserializer; import io.javalin.http.BadRequestResponse; import java.io.IOException; import java.io.InputStream; +import java.time.ZoneId; import java.util.Arrays; import java.util.List; import java.util.stream.Collectors; @@ -30,12 +33,8 @@ public class JsonV1 implements OutputFormatter { private final ObjectMapper om; public JsonV1() { - this.om = new ObjectMapper(); - this.om.setPropertyNamingStrategy(PropertyNamingStrategies.KEBAB_CASE); - this.om.setSerializationInclusion(JsonInclude.Include.NON_NULL); - this.om.disable(SerializationFeature.WRITE_DATE_TIMESTAMPS_AS_NANOSECONDS); - this.om.disable(DeserializationFeature.READ_DATE_TIMESTAMPS_AS_NANOSECONDS); - this.om.registerModule(new JavaTimeModule()); + this.om = buildObjectMapper(); + } @NotNull @@ -47,6 +46,11 @@ public static ObjectMapper buildObjectMapper() { retVal.disable(SerializationFeature.WRITE_DATE_TIMESTAMPS_AS_NANOSECONDS); retVal.disable(DeserializationFeature.READ_DATE_TIMESTAMPS_AS_NANOSECONDS); retVal.registerModule(new JavaTimeModule()); + + SimpleModule module = new SimpleModule(); + module.addDeserializer(ZoneId.class, new ZoneIdDeserializer()); + retVal.registerModule(module); + return retVal; } @@ -130,7 +134,7 @@ private Object buildFormatting(CwmsDTOBase dto) { private boolean isFormattableWith(Class klass) { FormattableWith[] formats = klass.getAnnotationsByType(FormattableWith.class); for (FormattableWith format : formats) { - /** + /* * Compare against the actual formatter not the name */ if (format.formatter().equals(JsonV1.class)) { diff --git a/cwms-data-api/src/main/java/cwms/cda/formatters/json/JsonV2.java b/cwms-data-api/src/main/java/cwms/cda/formatters/json/JsonV2.java index 2decd11b9d..82d325c014 100644 --- a/cwms-data-api/src/main/java/cwms/cda/formatters/json/JsonV2.java +++ b/cwms-data-api/src/main/java/cwms/cda/formatters/json/JsonV2.java @@ -30,15 +30,18 @@ import com.fasterxml.jackson.databind.ObjectMapper; import com.fasterxml.jackson.databind.PropertyNamingStrategies; import com.fasterxml.jackson.databind.SerializationFeature; +import com.fasterxml.jackson.databind.module.SimpleModule; import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule; import cwms.cda.data.dto.CwmsDTOBase; import cwms.cda.formatters.Formats; import cwms.cda.formatters.FormattingException; import cwms.cda.formatters.OutputFormatter; +import cwms.cda.formatters.json.adapters.ZoneIdDeserializer; import org.jetbrains.annotations.NotNull; import java.io.IOException; import java.io.InputStream; +import java.time.ZoneId; import java.util.List; /** @@ -64,6 +67,11 @@ public static ObjectMapper buildObjectMapper() { retVal.setPropertyNamingStrategy(PropertyNamingStrategies.KEBAB_CASE); retVal.setSerializationInclusion(JsonInclude.Include.NON_NULL); retVal.registerModule(new JavaTimeModule()); + + SimpleModule module = new SimpleModule(); + module.addDeserializer(ZoneId.class, new ZoneIdDeserializer()); + retVal.registerModule(module); + return retVal; } diff --git a/cwms-data-api/src/main/java/cwms/cda/formatters/json/adapters/ZoneIdDeserializer.java b/cwms-data-api/src/main/java/cwms/cda/formatters/json/adapters/ZoneIdDeserializer.java new file mode 100644 index 0000000000..3d40514226 --- /dev/null +++ b/cwms-data-api/src/main/java/cwms/cda/formatters/json/adapters/ZoneIdDeserializer.java @@ -0,0 +1,16 @@ +package cwms.cda.formatters.json.adapters; + +import com.fasterxml.jackson.core.JsonParser; +import com.fasterxml.jackson.databind.DeserializationContext; +import com.fasterxml.jackson.databind.JsonDeserializer; +import cwms.cda.helpers.ZoneIdHelper; +import java.io.IOException; +import java.time.ZoneId; + +public class ZoneIdDeserializer extends JsonDeserializer { + + @Override + public ZoneId deserialize(JsonParser p, DeserializationContext deserializationContext) throws IOException { + return ZoneIdHelper.parseZoneIdWithAliases(p.getValueAsString()); + } +} diff --git a/cwms-data-api/src/main/java/cwms/cda/formatters/xml/XMLv1.java b/cwms-data-api/src/main/java/cwms/cda/formatters/xml/XMLv1.java index f357b8f646..9c3287156f 100644 --- a/cwms-data-api/src/main/java/cwms/cda/formatters/xml/XMLv1.java +++ b/cwms-data-api/src/main/java/cwms/cda/formatters/xml/XMLv1.java @@ -4,6 +4,7 @@ import com.fasterxml.jackson.databind.DeserializationFeature; import com.fasterxml.jackson.databind.PropertyNamingStrategies; import com.fasterxml.jackson.databind.SerializationFeature; +import com.fasterxml.jackson.databind.module.SimpleModule; import com.fasterxml.jackson.dataformat.xml.XmlMapper; import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule; import cwms.cda.data.dto.CwmsDTOBase; @@ -12,11 +13,13 @@ import cwms.cda.formatters.Formats; import cwms.cda.formatters.FormattingException; import cwms.cda.formatters.OutputFormatter; +import cwms.cda.formatters.json.adapters.ZoneIdDeserializer; import io.javalin.http.InternalServerErrorResponse; import org.jetbrains.annotations.NotNull; import java.io.IOException; import java.io.InputStream; +import java.time.ZoneId; import java.util.Collections; import java.util.List; import java.util.logging.Level; @@ -100,6 +103,10 @@ public T parseContent(InputStream content, Class type retval.registerModule(new JavaTimeModule()); retval.addMixIn(VerticalDatumInfo.class, VerticalDatumInfoMixin.class); retval.addMixIn(VerticalDatumInfo.Builder.class, VerticalDatumInfoMixin.Builder.class); + + SimpleModule module = new SimpleModule(); + module.addDeserializer(ZoneId.class, new ZoneIdDeserializer()); + retval.registerModule(module); return retval; } diff --git a/cwms-data-api/src/main/java/cwms/cda/formatters/xml/XMLv2.java b/cwms-data-api/src/main/java/cwms/cda/formatters/xml/XMLv2.java index 2166f28e83..ccc9510f72 100644 --- a/cwms-data-api/src/main/java/cwms/cda/formatters/xml/XMLv2.java +++ b/cwms-data-api/src/main/java/cwms/cda/formatters/xml/XMLv2.java @@ -5,6 +5,7 @@ import com.fasterxml.jackson.databind.DeserializationFeature; import com.fasterxml.jackson.databind.PropertyNamingStrategies; import com.fasterxml.jackson.databind.SerializationFeature; +import com.fasterxml.jackson.databind.module.SimpleModule; import com.fasterxml.jackson.dataformat.xml.XmlMapper; import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule; import cwms.cda.data.dto.CwmsDTOBase; @@ -12,11 +13,13 @@ import cwms.cda.formatters.Formats; import cwms.cda.formatters.FormattingException; import cwms.cda.formatters.OutputFormatter; +import cwms.cda.formatters.json.adapters.ZoneIdDeserializer; import io.javalin.http.InternalServerErrorResponse; import org.jetbrains.annotations.NotNull; import java.io.IOException; import java.io.InputStream; +import java.time.ZoneId; import java.util.List; import java.util.logging.Level; import java.util.logging.Logger; @@ -90,6 +93,10 @@ public T parseContent(InputStream content, Class type retval.setSerializationInclusion(JsonInclude.Include.NON_NULL); retval.registerModule(new JavaTimeModule()); retval.addMixIn(TimeSeries.class, TimeSeriesXmlMixin.class); + + SimpleModule module = new SimpleModule(); + module.addDeserializer(ZoneId.class, new ZoneIdDeserializer()); + retval.registerModule(module); return retval; } } diff --git a/cwms-data-api/src/main/java/cwms/cda/helpers/ZoneIdHelper.java b/cwms-data-api/src/main/java/cwms/cda/helpers/ZoneIdHelper.java new file mode 100644 index 0000000000..7460e9ae80 --- /dev/null +++ b/cwms-data-api/src/main/java/cwms/cda/helpers/ZoneIdHelper.java @@ -0,0 +1,163 @@ +package cwms.cda.helpers; + +import com.google.common.flogger.FluentLogger; +import org.jetbrains.annotations.NotNull; +import java.io.IOException; +import java.io.InputStream; +import java.time.DateTimeException; +import java.time.ZoneId; +import java.util.Collections; +import java.util.HashMap; +import java.util.LinkedHashMap; +import java.util.Map; +import java.util.Properties; + +import javax.annotation.Nullable; + +/** + * Helper class for handling timezone IDs and their aliases. + * Provides functionality to map non-standard timezone identifiers to standard ZoneId values. + */ +public class ZoneIdHelper { + private static final FluentLogger logger = FluentLogger.forEnclosingClass(); + + /** + * Default resource location for timezone aliases properties file. + * Can be overridden via the system property "cwms.timezone.aliases.resource". + */ + public static final String RESOURCE_LOCATION = System.getProperty("cwms.timezone.aliases.resource", + "cwms/cda/data/dao/timezone-aliases.properties"); + + private static final Map tzAliases = Collections.unmodifiableMap(buildTimeZoneAliases()); + + private ZoneIdHelper() { + // Prevent instantiation + } + + /** + * Builds a map of default timezone aliases. + * These are hardcoded fallback values used when no resource file is available. + * + * @return A map of timezone aliases where keys are non-standard timezone IDs and + * values are their corresponding standard timezone IDs + */ + public static Map buildDefaultAliases() { + Map aliases = new HashMap<>(); + aliases.put("Canada/East-Saskatchewan", "Canada/Saskatchewan"); + aliases.put("ROC", "Asia/Taipei"); + aliases.put("US/Pacific-New", "US/Pacific"); + aliases.put("Unknown or Not Applicable", "UTC"); + return aliases; + } + + /** + * Builds a map of timezone aliases from the default resource location. + * + * @return A map of timezone aliases loaded from the resource file, or an empty map if + * the resource cannot be loaded + */ + public static Map buildResourceAliases() { + return buildResourceAliases(RESOURCE_LOCATION); + } + + /** + * Builds a map of timezone aliases from the specified resource location. + * + * @param resourceLocation The location of the properties file containing timezone aliases + * @return A map of timezone aliases loaded from the resource file, or an empty map if + * the resource cannot be loaded + */ + public static @NotNull Map buildResourceAliases(@Nullable String resourceLocation) { + if (resourceLocation == null || resourceLocation.isEmpty()) { + logger.atWarning().log("Resource location is null or empty"); + return Collections.emptyMap(); + } + + try (InputStream resource = ZoneIdHelper.class.getClassLoader().getResourceAsStream(resourceLocation)) { + if (resource != null) { + Properties props = new Properties(); + props.load(resource); + return buildAliasMap(props); + } else { + logger.atWarning().log("Timezone aliases properties file not found at " + resourceLocation); + } + } catch (IOException e) { + logger.atWarning().withCause(e).log("Failed to load timezone aliases from resource file."); + } + return Collections.emptyMap(); + } + + /** + * Builds a complete map of timezone aliases by combining resource-based aliases + * with default aliases when necessary. + * + * @return A map of timezone aliases where keys are non-standard timezone IDs and + * values are their corresponding standard timezone IDs + */ + public static @NotNull Map buildTimeZoneAliases() { + Map aliases = buildResourceAliases(); + + if (aliases.isEmpty()) { + logger.atWarning().log("No timezone aliases found in resource file, using default aliases."); + aliases = buildDefaultAliases(); + } + + return aliases; + } + + /** + * Builds a map of timezone aliases from the provided Properties object. + * The properties should follow the format: + * alias.N.from=NonStandardTimezoneId + * alias.N.to=StandardTimezoneId + * where N is a number. + * + * @param props Properties object containing timezone alias definitions + * @return A map of timezone aliases parsed from the properties + */ + public static @NotNull Map buildAliasMap(@Nullable Properties props) { + if (props == null) { + return Collections.emptyMap(); + } + + Map aliases = new LinkedHashMap<>(); + // Find all alias entries by looking for .from properties + for (String key : props.stringPropertyNames()) { + if (key.matches("alias\\.\\d+\\.from")) { + String aliasNumber = key.substring(6, key.lastIndexOf('.')); + String fromKey = "alias." + aliasNumber + ".from"; + String toKey = "alias." + aliasNumber + ".to"; + + String fromValue = props.getProperty(fromKey); + String toValue = props.getProperty(toKey); + + if (fromValue != null && toValue != null) { + aliases.put(fromValue, toValue); + } + } + } + return aliases; + } + + /** + * Parses a string to a ZoneId, using the timezone aliases if necessary. + * + * @param zoneId The timezone ID string to parse + * @return The parsed ZoneId + * @throws DateTimeException if the zone ID has an invalid format or the zone ID is not available + */ + public static ZoneId parseZoneIdWithAliases(String zoneId) { + + return ZoneId.of(zoneId, tzAliases); + } + + /** + * Returns an unmodifiable map of all available timezone aliases. + * + * @return An unmodifiable map of timezone aliases where keys are non-standard timezone IDs and + * values are their corresponding standard timezone IDs + */ + public static Map getTimezoneAliases() { + return tzAliases; + } +} diff --git a/cwms-data-api/src/main/resources/cwms/cda/data/dao/timezone-aliases.properties b/cwms-data-api/src/main/resources/cwms/cda/data/dao/timezone-aliases.properties new file mode 100644 index 0000000000..97df74d5a2 --- /dev/null +++ b/cwms-data-api/src/main/resources/cwms/cda/data/dao/timezone-aliases.properties @@ -0,0 +1,9 @@ +# Timezone aliases configuration +alias.1.from=Canada/East-Saskatchewan +alias.1.to=Canada/Saskatchewan +alias.2.from=ROC +alias.2.to=Asia/Taipei +alias.3.from=US/Pacific-New +alias.3.to=US/Pacific +alias.4.from=Unknown or Not Applicable +alias.4.to=UTC diff --git a/cwms-data-api/src/test/java/cwms/cda/api/DataApiTestIT.java b/cwms-data-api/src/test/java/cwms/cda/api/DataApiTestIT.java index d8f81ee997..7b57b10c99 100644 --- a/cwms-data-api/src/test/java/cwms/cda/api/DataApiTestIT.java +++ b/cwms-data-api/src/test/java/cwms/cda/api/DataApiTestIT.java @@ -28,9 +28,11 @@ import static cwms.cda.data.dao.JooqDao.SESSION_USE_LRTS_ID_FORMAT; import com.google.common.flogger.FluentLogger; +import cwms.cda.data.dao.JooqDao; import cwms.cda.data.dto.Location; import cwms.cda.data.dto.LocationCategory; import cwms.cda.data.dto.LocationGroup; +import cwms.cda.helpers.ZoneIdHelper; import fixtures.CwmsDataApiSetupCallback; import fixtures.IntegrationTestNameGenerator; import fixtures.KeyCloakExtension; @@ -45,7 +47,6 @@ import java.sql.Connection; import java.sql.PreparedStatement; import java.sql.SQLException; -import java.time.ZoneId; import java.util.ArrayList; import java.util.Collections; import java.util.HashMap; @@ -88,10 +89,10 @@ public class DataApiTestIT { protected static String createLocationQuery = null; protected static String createTimeseriesQuery = null; protected static String createTimeseriesOffsetQuery = null; - protected final static String registerApiKey = "insert into at_api_keys(userid,key_name,apikey) values(UPPER(?),?,?)"; - protected final static String removeApiKeys = "delete from at_api_keys where UPPER(userid) = UPPER(?) and apikey = ?"; + protected static final String registerApiKey = "insert into at_api_keys(userid,key_name,apikey) values(UPPER(?),?,?)"; + protected static final String removeApiKeys = "delete from at_api_keys where UPPER(userid) = UPPER(?) and apikey = ?"; - protected final static Configuration freemarkerConfig = new Configuration(Configuration.VERSION_2_3_32); + protected static final Configuration freemarkerConfig = new Configuration(Configuration.VERSION_2_3_32); private ArrayList groupsCreated = new ArrayList<>(); private ArrayList categoriesCreated = new ArrayList<>(); @@ -238,7 +239,7 @@ protected static void createLocation(String location, boolean active, String off CwmsDatabaseContainer db = CwmsDataApiSetupCallback.getDatabaseLink(); Location loc = new Location.Builder(location, kind, - ZoneId.of(timeZone), + ZoneIdHelper.parseZoneIdWithAliases(timeZone), latitude, longitude, horizontalDatum, diff --git a/cwms-data-api/src/test/java/cwms/cda/data/dto/LocationTest.java b/cwms-data-api/src/test/java/cwms/cda/data/dto/LocationTest.java index c6800c9f69..7983824c6b 100644 --- a/cwms-data-api/src/test/java/cwms/cda/data/dto/LocationTest.java +++ b/cwms-data-api/src/test/java/cwms/cda/data/dto/LocationTest.java @@ -50,6 +50,29 @@ void serializedOutputNewline() throws JsonProcessingException } + @Test + void deserializeWithAliasedTimezoneName() throws JsonProcessingException { + String input = "{\n" + + " \"office-id\" : \"LRL\",\n" + + " \"name\" : \"TEST_LOCATION2\",\n" + + " \"latitude\" : 50.0,\n" + + " \"longitude\" : 50.0,\n" + + " \"active\" : true,\n" + + " \"public-name\" : \"TEST_LOCATION2\",\n" + + " \"long-name\" : \"TEST_LOCATION\",\n" + + " \"description\" : \"for testing\",\n" + + " \"timezone-name\" : \"Unknown or Not Applicable\",\n" + // This is the key line for the test + " \"location-kind\" : \"SITE\"\n" + + "}"; + + ObjectMapper om = JsonV1.buildObjectMapper(); + Location location2 = om.readValue(input, Location.class); + assertNotNull(location2); + + assertEquals("UTC", location2.getTimezoneName()); + } + + @ParameterizedTest @ValueSource(strings = { Formats.JSONV2, Formats.XMLV2 }) void testSerializationRoundTrip(String format) throws JsonProcessingException @@ -134,4 +157,7 @@ private Location buildTestLocationNewLine() { } + + + } diff --git a/cwms-data-api/src/test/java/cwms/cda/helpers/ZoneIdHelperTest.java b/cwms-data-api/src/test/java/cwms/cda/helpers/ZoneIdHelperTest.java new file mode 100644 index 0000000000..c594301d71 --- /dev/null +++ b/cwms-data-api/src/test/java/cwms/cda/helpers/ZoneIdHelperTest.java @@ -0,0 +1,155 @@ +package cwms.cda.helpers; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import java.io.ByteArrayInputStream; +import java.io.IOException; +import java.io.InputStream; +import java.nio.charset.StandardCharsets; +import java.time.DateTimeException; +import java.time.ZoneId; +import java.util.Map; +import java.util.Properties; + +import org.junit.jupiter.api.Test; + +class ZoneIdHelperTest { + + @Test + void test_buildDefaultAliases() { + Map aliases = ZoneIdHelper.buildDefaultAliases(); + + // Verify the default aliases are correctly set + assertEquals("Canada/Saskatchewan", aliases.get("Canada/East-Saskatchewan")); + assertEquals("Asia/Taipei", aliases.get("ROC")); + assertEquals("US/Pacific", aliases.get("US/Pacific-New")); + assertEquals("UTC", aliases.get("Unknown or Not Applicable")); + + // Verify the size of the map + assertEquals(4, aliases.size()); + } + + @Test + void test_parseZoneIdWithAliases_validInput() { + // Test with standard zone ID + ZoneId zoneId = ZoneIdHelper.parseZoneIdWithAliases("UTC"); + assertEquals(ZoneId.of("UTC"), zoneId); + + // Test with aliased zone ID + ZoneId aliasedZoneId = ZoneIdHelper.parseZoneIdWithAliases("US/Pacific-New"); + assertEquals(ZoneId.of("US/Pacific"), aliasedZoneId); + + // Test with another aliased zone ID + ZoneId anotherAliasedZoneId = ZoneIdHelper.parseZoneIdWithAliases("Unknown or Not Applicable"); + assertEquals(ZoneId.of("UTC"), anotherAliasedZoneId); + } + + @Test + void test_parseZoneIdWithAliases_invalidInput() { + // Test with invalid zone ID + assertThrows(DateTimeException.class, () -> ZoneIdHelper.parseZoneIdWithAliases("InvalidZoneId")); + } + + @Test + void test_buildTimeZoneAliases() { + Map aliases = ZoneIdHelper.buildTimeZoneAliases(); + + // Verify that the map is not null and not empty + assertNotNull(aliases); + assertFalse(aliases.isEmpty()); + + // Verify that it contains at least the default aliases + assertTrue(aliases.containsKey("Canada/East-Saskatchewan")); + assertTrue(aliases.containsKey("ROC")); + assertTrue(aliases.containsKey("US/Pacific-New")); + assertTrue(aliases.containsKey("Unknown or Not Applicable")); + } + + @Test + void test_buildResourceAliases() { + Map aliases = ZoneIdHelper.buildResourceAliases(); + + // we just verify that the method returns a map (which might be empty) + assertNotNull(aliases); + } + + @Test + void test_buildResourceAliases_nonExistentResource() { + Map aliases = ZoneIdHelper.buildResourceAliases("non/existent/resource.properties"); + + // Verify that an empty map is returned for a non-existent resource + assertNotNull(aliases); + assertTrue(aliases.isEmpty()); + } + + @Test + void test_buildAliasMap_validProperties() { + Properties props = new Properties(); + props.setProperty("alias.1.from", "TestFrom1"); + props.setProperty("alias.1.to", "TestTo1"); + props.setProperty("alias.2.from", "TestFrom2"); + props.setProperty("alias.2.to", "TestTo2"); + + Map aliases = ZoneIdHelper.buildAliasMap(props); + + // Verify the aliases are correctly built + assertEquals("TestTo1", aliases.get("TestFrom1")); + assertEquals("TestTo2", aliases.get("TestFrom2")); + assertEquals(2, aliases.size()); + } + + @Test + void test_buildAliasMap_emptyProperties() { + Properties props = new Properties(); + + Map aliases = ZoneIdHelper.buildAliasMap(props); + + // Verify that an empty map is returned for empty properties + assertNotNull(aliases); + assertTrue(aliases.isEmpty()); + } + + @Test + void test_buildAliasMap_malformedProperties() { + Properties props = new Properties(); + // Missing 'to' property + props.setProperty("alias.1.from", "TestFrom1"); + // Missing 'from' property + props.setProperty("alias.2.to", "TestTo2"); + // Invalid key format + props.setProperty("invalid.key", "InvalidValue"); + + Map aliases = ZoneIdHelper.buildAliasMap(props); + + // Verify that no aliases are created for malformed properties + assertNotNull(aliases); + assertTrue(aliases.isEmpty()); + } + + @Test + void test_buildResourceAliases_customResource() throws IOException { + // Create a test properties content + String propertiesContent = + "alias.1.from=CustomFrom1\n" + + "alias.1.to=CustomTo1\n" + + "alias.2.from=CustomFrom2\n" + + "alias.2.to=CustomTo2\n"; + + + Properties props = new Properties(); + try (InputStream is = new ByteArrayInputStream(propertiesContent.getBytes(StandardCharsets.UTF_8))) { + props.load(is); + } + + Map aliases = ZoneIdHelper.buildAliasMap(props); + + // Verify the aliases are correctly built + assertEquals("CustomTo1", aliases.get("CustomFrom1")); + assertEquals("CustomTo2", aliases.get("CustomFrom2")); + assertEquals(2, aliases.size()); + } +} \ No newline at end of file