From ec77b619582166c409b52973e944385ad5eb40b8 Mon Sep 17 00:00:00 2001 From: Bryson Spilman Date: Tue, 25 Feb 2025 14:48:23 -0800 Subject: [PATCH 1/7] CWMSVUE-634 - Implements Access To Water DTO --- .../AccessToWaterTimeSeriesIdentifier.java | 97 +++++++++++ ...AccessToWaterTimeSeriesIdentifierTest.java | 157 ++++++++++++++++++ .../test/java/cwms/cda/helpers/DTOMatch.java | 13 ++ .../dto/access_to_water_time_series_data.json | 12 ++ 4 files changed, 279 insertions(+) create mode 100644 cwms-data-api/src/main/java/cwms/cda/data/dto/AccessToWaterTimeSeriesIdentifier.java create mode 100644 cwms-data-api/src/test/java/cwms/cda/data/dto/AccessToWaterTimeSeriesIdentifierTest.java create mode 100644 cwms-data-api/src/test/resources/cwms/cda/data/dto/access_to_water_time_series_data.json diff --git a/cwms-data-api/src/main/java/cwms/cda/data/dto/AccessToWaterTimeSeriesIdentifier.java b/cwms-data-api/src/main/java/cwms/cda/data/dto/AccessToWaterTimeSeriesIdentifier.java new file mode 100644 index 000000000..3434d75e5 --- /dev/null +++ b/cwms-data-api/src/main/java/cwms/cda/data/dto/AccessToWaterTimeSeriesIdentifier.java @@ -0,0 +1,97 @@ +/* + * MIT License + * + * Copyright (c) 2025 Hydrologic Engineering Center + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ +package cwms.cda.data.dto; + +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.annotation.JsonProperty; +import com.fasterxml.jackson.databind.PropertyNamingStrategies; +import com.fasterxml.jackson.databind.annotation.JsonDeserialize; +import com.fasterxml.jackson.databind.annotation.JsonNaming; +import cwms.cda.formatters.Formats; +import cwms.cda.formatters.annotations.FormattableWith; +import cwms.cda.formatters.json.JsonV1; + +@FormattableWith(contentType = Formats.JSONV1, formatter = JsonV1.class, aliases = {Formats.JSON, Formats.DEFAULT}) +@JsonDeserialize(builder = AccessToWaterTimeSeriesIdentifier.Builder.class) +@JsonInclude(JsonInclude.Include.NON_NULL) +@JsonNaming(PropertyNamingStrategies.KebabCaseStrategy.class) +public final class AccessToWaterTimeSeriesIdentifier extends CwmsDTO { + @JsonProperty(required = true) + private final String locationId; + @JsonProperty(required = true) + private final TimeSeriesIdentifierDescriptor timeSeriesIdDescriptor; + @JsonProperty(required = true) + private final String tsType; + + private AccessToWaterTimeSeriesIdentifier(Builder builder) { + super(builder.officeId); + this.locationId = builder.locationId; + this.timeSeriesIdDescriptor = builder.timeSeriesIdDescriptor; + this.tsType = builder.tsType; + } + + public String getLocationId() { + return locationId; + } + + public TimeSeriesIdentifierDescriptor getTimeSeriesIdDescriptor() { + return timeSeriesIdDescriptor; + } + + public String getTsType() { + return tsType; + } + + public static class Builder { + private String locationId; + private TimeSeriesIdentifierDescriptor timeSeriesIdDescriptor; + private String tsType; + private String officeId; + + public Builder withLocationId(String locationId) { + this.locationId = locationId; + return this; + } + + public Builder withTimeSeriesIdDescriptor(TimeSeriesIdentifierDescriptor timeSeriesIdDescriptor) { + this.timeSeriesIdDescriptor = timeSeriesIdDescriptor; + return this; + } + + public Builder withTsType(String tsType) { + this.tsType = tsType; + return this; + } + + public Builder withOfficeId(String officeId) { + this.officeId = officeId; + return this; + } + + public AccessToWaterTimeSeriesIdentifier build() { + return new AccessToWaterTimeSeriesIdentifier(this); + } + + } +} diff --git a/cwms-data-api/src/test/java/cwms/cda/data/dto/AccessToWaterTimeSeriesIdentifierTest.java b/cwms-data-api/src/test/java/cwms/cda/data/dto/AccessToWaterTimeSeriesIdentifierTest.java new file mode 100644 index 000000000..1084bb9d2 --- /dev/null +++ b/cwms-data-api/src/test/java/cwms/cda/data/dto/AccessToWaterTimeSeriesIdentifierTest.java @@ -0,0 +1,157 @@ +/* + * MIT License + * + * Copyright (c) 2025 Hydrologic Engineering Center + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES, OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ + +package cwms.cda.data.dto; + +import java.time.ZoneId; +import static org.junit.jupiter.api.Assertions.*; + +import cwms.cda.api.errors.FieldException; +import cwms.cda.formatters.ContentType; +import cwms.cda.formatters.Formats; +import cwms.cda.helpers.DTOMatch; +import java.io.InputStream; +import java.nio.charset.StandardCharsets; +import org.apache.commons.io.IOUtils; +import org.junit.jupiter.api.Test; + +final class AccessToWaterTimeSeriesIdentifierTest { + + @Test + void createAccessToWaterTimeSeriesData_allFieldsProvided_success() { + TimeSeriesIdentifierDescriptor tsDescriptor = new TimeSeriesIdentifierDescriptor.Builder() + .withTimeSeriesId("VANL.Stage.Inst.15Minutes.0.Ccp-Rev") + .withOfficeId("SWT") + .withActive(true) + .withIntervalOffsetMinutes(0L) + .withZoneId(ZoneId.of("UTC")) + .build(); + AccessToWaterTimeSeriesIdentifier item = new AccessToWaterTimeSeriesIdentifier.Builder() + .withOfficeId("SWT") + .withLocationId("VANL") + .withTimeSeriesIdDescriptor(tsDescriptor) + .withTsType("STAGE") + .build(); + + assertAll( + () -> assertEquals("VANL", item.getLocationId(), "The location ID does not match the provided value"), + () -> DTOMatch.assertMatch(tsDescriptor, item.getTimeSeriesIdDescriptor()), + () -> assertEquals("STAGE", item.getTsType(), "The time series type does not match the provided value") + ); + } + + @Test + void createAccessToWaterTimeSeriesData_missingField_throwsFieldException() { + TimeSeriesIdentifierDescriptor tsDescriptor = new TimeSeriesIdentifierDescriptor.Builder() + .withTimeSeriesId("VANL.Stage.Inst.15Minutes.0.Ccp-Rev") + .withOfficeId("SWT") + .withActive(true) + .withIntervalOffsetMinutes(0L) + .withZoneId(ZoneId.of("UTC")) + .build(); + assertAll( + () -> assertThrows(FieldException.class, () -> { + AccessToWaterTimeSeriesIdentifier item = new AccessToWaterTimeSeriesIdentifier.Builder() + .withOfficeId("SWT") + .withTimeSeriesIdDescriptor(tsDescriptor) + .withTsType("STAGE") + .build(); + item.validate(); + }, "The validate method should have thrown a FieldException because the location ID is missing"), + + () -> assertThrows(FieldException.class, () -> { + AccessToWaterTimeSeriesIdentifier item = new AccessToWaterTimeSeriesIdentifier.Builder() + .withOfficeId("SWT") + .withLocationId("VANL") + .withTsType("STAGE") + .build(); + item.validate(); + }, "The validate method should have thrown a FieldException because the TimeSeries ID is missing"), + + () -> assertThrows(FieldException.class, () -> { + AccessToWaterTimeSeriesIdentifier item = new AccessToWaterTimeSeriesIdentifier.Builder() + .withOfficeId("SWT") + .withLocationId("VANL") + .withTimeSeriesIdDescriptor(tsDescriptor) + .build(); + item.validate(); + }, "The validate method should have thrown a FieldException because the time series type is missing"), + + () -> assertThrows(FieldException.class, () -> { + AccessToWaterTimeSeriesIdentifier item = new AccessToWaterTimeSeriesIdentifier.Builder() + .withLocationId("VANL") + .withTimeSeriesIdDescriptor(tsDescriptor) + .withTsType("STAGE") + .build(); + item.validate(); + }, "The validate method should have thrown a FieldException because the office id is missing") + ); + } + + @Test + void createAccessToWaterTimeSeriesData_serialize_roundtrip() { + TimeSeriesIdentifierDescriptor tsDescriptor = new TimeSeriesIdentifierDescriptor.Builder() + .withTimeSeriesId("VANL.Stage.Inst.15Minutes.0.Ccp-Rev") + .withOfficeId("SWT") + .withActive(true) + .withIntervalOffsetMinutes(0L) + .withZoneId(ZoneId.of("UTC")) + .build(); + AccessToWaterTimeSeriesIdentifier data = new AccessToWaterTimeSeriesIdentifier.Builder() + .withOfficeId("SWT") + .withLocationId("VANL") + .withTimeSeriesIdDescriptor(tsDescriptor) + .withTsType("STAGE") + .build(); + + ContentType contentType = new ContentType(Formats.JSON); + String json = Formats.format(contentType, data); + AccessToWaterTimeSeriesIdentifier deserialized = Formats.parseContent(contentType, json, AccessToWaterTimeSeriesIdentifier.class); + DTOMatch.assertMatch(data, deserialized); + } + + @Test + void createAccessToWaterTimeSeriesData_deserialize() throws Exception { + TimeSeriesIdentifierDescriptor tsDescriptor = new TimeSeriesIdentifierDescriptor.Builder() + .withTimeSeriesId("VANL.Stage.Inst.15Minutes.0.Ccp-Rev") + .withOfficeId("SWT") + .withActive(true) + .withIntervalOffsetMinutes(0L) + .withZoneId(ZoneId.of("UTC")) + .build(); + AccessToWaterTimeSeriesIdentifier expected = new AccessToWaterTimeSeriesIdentifier.Builder() + .withOfficeId("SWT") + .withLocationId("VANL") + .withTimeSeriesIdDescriptor(tsDescriptor) + .withTsType("STAGE") + .build(); + + InputStream resource = this.getClass().getResourceAsStream("/cwms/cda/data/dto/access_to_water_time_series_data.json"); + assertNotNull(resource); + String json = IOUtils.toString(resource, StandardCharsets.UTF_8); + ContentType contentType = new ContentType(Formats.JSON); + AccessToWaterTimeSeriesIdentifier deserialized = Formats.parseContent(contentType, json, AccessToWaterTimeSeriesIdentifier.class); + DTOMatch.assertMatch(expected, deserialized); + } +} diff --git a/cwms-data-api/src/test/java/cwms/cda/helpers/DTOMatch.java b/cwms-data-api/src/test/java/cwms/cda/helpers/DTOMatch.java index 49baac886..2c2f3784c 100644 --- a/cwms-data-api/src/test/java/cwms/cda/helpers/DTOMatch.java +++ b/cwms-data-api/src/test/java/cwms/cda/helpers/DTOMatch.java @@ -24,9 +24,11 @@ package cwms.cda.helpers; +import cwms.cda.data.dto.AccessToWaterTimeSeriesIdentifier; import cwms.cda.data.dto.CwmsIdTimeExtentsEntry; import cwms.cda.data.dto.TimeExtents; import cwms.cda.data.dto.TimeSeriesExtents; +import cwms.cda.data.dto.TimeSeriesIdentifierDescriptor; import cwms.cda.data.dto.location.kind.Lock; import cwms.cda.data.dto.CwmsDTOBase; import cwms.cda.data.dto.location.kind.GateChange; @@ -601,6 +603,17 @@ public static void assertMatch(CwmsIdTimeExtentsEntry first, CwmsIdTimeExtentsEn ); } + public static void assertMatch(AccessToWaterTimeSeriesIdentifier first, AccessToWaterTimeSeriesIdentifier second) { + assertAll( + () -> assertMatch(first.getTimeSeriesIdDescriptor(), second.getTimeSeriesIdDescriptor()), + () -> assertEquals(first.getTsType(), second.getTsType()), + () -> assertEquals(first.getLocationId(), second.getLocationId()) + ); + } + + public static void assertMatch(TimeSeriesIdentifierDescriptor tsDescriptor, TimeSeriesIdentifierDescriptor timeSeriesIdDescriptor) { + } + @FunctionalInterface public interface AssertMatchMethod{ void assertMatch(T first, T second); diff --git a/cwms-data-api/src/test/resources/cwms/cda/data/dto/access_to_water_time_series_data.json b/cwms-data-api/src/test/resources/cwms/cda/data/dto/access_to_water_time_series_data.json new file mode 100644 index 000000000..2e7b96b3f --- /dev/null +++ b/cwms-data-api/src/test/resources/cwms/cda/data/dto/access_to_water_time_series_data.json @@ -0,0 +1,12 @@ +{ + "office-id":"SWT", + "location-id": "VANL", + "time-series-id-descriptor": { + "office-id": "SWT", + "time-series-id": "VANL.Stage.Inst.15Minutes.0.Ccp-Rev", + "timezone-name": "UTC", + "interval-offset-minutes": 0, + "active": true + }, + "ts-type": "STAGE" +} \ No newline at end of file From e03acf5258c38536128f00c863d055351782985f Mon Sep 17 00:00:00 2001 From: Bryson Spilman Date: Tue, 25 Feb 2025 16:13:47 -0800 Subject: [PATCH 2/7] CWMSVUE-634 - Updated locationId to be a CwmsId, updated DTO match to include TimeSeriesIdentifierDescriptor --- .../AccessToWaterTimeSeriesIdentifier.java | 17 ++++------- ...AccessToWaterTimeSeriesIdentifierTest.java | 29 +++++-------------- .../test/java/cwms/cda/helpers/DTOMatch.java | 8 ++++- .../dto/access_to_water_time_series_data.json | 6 ++-- 4 files changed, 23 insertions(+), 37 deletions(-) diff --git a/cwms-data-api/src/main/java/cwms/cda/data/dto/AccessToWaterTimeSeriesIdentifier.java b/cwms-data-api/src/main/java/cwms/cda/data/dto/AccessToWaterTimeSeriesIdentifier.java index 3434d75e5..6ae857b93 100644 --- a/cwms-data-api/src/main/java/cwms/cda/data/dto/AccessToWaterTimeSeriesIdentifier.java +++ b/cwms-data-api/src/main/java/cwms/cda/data/dto/AccessToWaterTimeSeriesIdentifier.java @@ -36,22 +36,21 @@ @JsonDeserialize(builder = AccessToWaterTimeSeriesIdentifier.Builder.class) @JsonInclude(JsonInclude.Include.NON_NULL) @JsonNaming(PropertyNamingStrategies.KebabCaseStrategy.class) -public final class AccessToWaterTimeSeriesIdentifier extends CwmsDTO { +public final class AccessToWaterTimeSeriesIdentifier extends CwmsDTOBase { @JsonProperty(required = true) - private final String locationId; + private final CwmsId locationId; @JsonProperty(required = true) private final TimeSeriesIdentifierDescriptor timeSeriesIdDescriptor; @JsonProperty(required = true) private final String tsType; private AccessToWaterTimeSeriesIdentifier(Builder builder) { - super(builder.officeId); this.locationId = builder.locationId; this.timeSeriesIdDescriptor = builder.timeSeriesIdDescriptor; this.tsType = builder.tsType; } - public String getLocationId() { + public CwmsId getLocationId() { return locationId; } @@ -64,12 +63,11 @@ public String getTsType() { } public static class Builder { - private String locationId; + private CwmsId locationId; private TimeSeriesIdentifierDescriptor timeSeriesIdDescriptor; private String tsType; - private String officeId; - public Builder withLocationId(String locationId) { + public Builder withLocationId(CwmsId locationId) { this.locationId = locationId; return this; } @@ -84,11 +82,6 @@ public Builder withTsType(String tsType) { return this; } - public Builder withOfficeId(String officeId) { - this.officeId = officeId; - return this; - } - public AccessToWaterTimeSeriesIdentifier build() { return new AccessToWaterTimeSeriesIdentifier(this); } diff --git a/cwms-data-api/src/test/java/cwms/cda/data/dto/AccessToWaterTimeSeriesIdentifierTest.java b/cwms-data-api/src/test/java/cwms/cda/data/dto/AccessToWaterTimeSeriesIdentifierTest.java index 1084bb9d2..823ac15b1 100644 --- a/cwms-data-api/src/test/java/cwms/cda/data/dto/AccessToWaterTimeSeriesIdentifierTest.java +++ b/cwms-data-api/src/test/java/cwms/cda/data/dto/AccessToWaterTimeSeriesIdentifierTest.java @@ -48,14 +48,13 @@ void createAccessToWaterTimeSeriesData_allFieldsProvided_success() { .withZoneId(ZoneId.of("UTC")) .build(); AccessToWaterTimeSeriesIdentifier item = new AccessToWaterTimeSeriesIdentifier.Builder() - .withOfficeId("SWT") - .withLocationId("VANL") + .withLocationId(CwmsId.buildCwmsId("SWT", "VANL")) .withTimeSeriesIdDescriptor(tsDescriptor) .withTsType("STAGE") .build(); assertAll( - () -> assertEquals("VANL", item.getLocationId(), "The location ID does not match the provided value"), + () -> DTOMatch.assertMatch(CwmsId.buildCwmsId("SWT", "VANL"), item.getLocationId(), "The location ID does not match the provided value"), () -> DTOMatch.assertMatch(tsDescriptor, item.getTimeSeriesIdDescriptor()), () -> assertEquals("STAGE", item.getTsType(), "The time series type does not match the provided value") ); @@ -73,7 +72,6 @@ void createAccessToWaterTimeSeriesData_missingField_throwsFieldException() { assertAll( () -> assertThrows(FieldException.class, () -> { AccessToWaterTimeSeriesIdentifier item = new AccessToWaterTimeSeriesIdentifier.Builder() - .withOfficeId("SWT") .withTimeSeriesIdDescriptor(tsDescriptor) .withTsType("STAGE") .build(); @@ -82,8 +80,7 @@ void createAccessToWaterTimeSeriesData_missingField_throwsFieldException() { () -> assertThrows(FieldException.class, () -> { AccessToWaterTimeSeriesIdentifier item = new AccessToWaterTimeSeriesIdentifier.Builder() - .withOfficeId("SWT") - .withLocationId("VANL") + .withLocationId(CwmsId.buildCwmsId("SWT", "VANL")) .withTsType("STAGE") .build(); item.validate(); @@ -91,21 +88,11 @@ void createAccessToWaterTimeSeriesData_missingField_throwsFieldException() { () -> assertThrows(FieldException.class, () -> { AccessToWaterTimeSeriesIdentifier item = new AccessToWaterTimeSeriesIdentifier.Builder() - .withOfficeId("SWT") - .withLocationId("VANL") - .withTimeSeriesIdDescriptor(tsDescriptor) - .build(); - item.validate(); - }, "The validate method should have thrown a FieldException because the time series type is missing"), - - () -> assertThrows(FieldException.class, () -> { - AccessToWaterTimeSeriesIdentifier item = new AccessToWaterTimeSeriesIdentifier.Builder() - .withLocationId("VANL") + .withLocationId(CwmsId.buildCwmsId("SWT", "VANL")) .withTimeSeriesIdDescriptor(tsDescriptor) - .withTsType("STAGE") .build(); item.validate(); - }, "The validate method should have thrown a FieldException because the office id is missing") + }, "The validate method should have thrown a FieldException because the time series type is missing") ); } @@ -119,8 +106,7 @@ void createAccessToWaterTimeSeriesData_serialize_roundtrip() { .withZoneId(ZoneId.of("UTC")) .build(); AccessToWaterTimeSeriesIdentifier data = new AccessToWaterTimeSeriesIdentifier.Builder() - .withOfficeId("SWT") - .withLocationId("VANL") + .withLocationId(CwmsId.buildCwmsId("SWT", "VANL")) .withTimeSeriesIdDescriptor(tsDescriptor) .withTsType("STAGE") .build(); @@ -141,8 +127,7 @@ void createAccessToWaterTimeSeriesData_deserialize() throws Exception { .withZoneId(ZoneId.of("UTC")) .build(); AccessToWaterTimeSeriesIdentifier expected = new AccessToWaterTimeSeriesIdentifier.Builder() - .withOfficeId("SWT") - .withLocationId("VANL") + .withLocationId(CwmsId.buildCwmsId("SWT", "VANL")) .withTimeSeriesIdDescriptor(tsDescriptor) .withTsType("STAGE") .build(); diff --git a/cwms-data-api/src/test/java/cwms/cda/helpers/DTOMatch.java b/cwms-data-api/src/test/java/cwms/cda/helpers/DTOMatch.java index 2c2f3784c..a9c59a4b8 100644 --- a/cwms-data-api/src/test/java/cwms/cda/helpers/DTOMatch.java +++ b/cwms-data-api/src/test/java/cwms/cda/helpers/DTOMatch.java @@ -607,11 +607,17 @@ public static void assertMatch(AccessToWaterTimeSeriesIdentifier first, AccessTo assertAll( () -> assertMatch(first.getTimeSeriesIdDescriptor(), second.getTimeSeriesIdDescriptor()), () -> assertEquals(first.getTsType(), second.getTsType()), - () -> assertEquals(first.getLocationId(), second.getLocationId()) + () -> assertMatch(first.getLocationId(), second.getLocationId()) ); } public static void assertMatch(TimeSeriesIdentifierDescriptor tsDescriptor, TimeSeriesIdentifierDescriptor timeSeriesIdDescriptor) { + assertAll( + () -> assertEquals(tsDescriptor.getIntervalOffsetMinutes(), timeSeriesIdDescriptor.getIntervalOffsetMinutes(), "Identifier does not match"), + () -> assertEquals(tsDescriptor.getTimeSeriesId(), timeSeriesIdDescriptor.getTimeSeriesId(), "Part does not match"), + () -> assertEquals(tsDescriptor.getOfficeId(), timeSeriesIdDescriptor.getOfficeId(), "Time series type does not match"), + () -> assertEquals(tsDescriptor.getTimezoneName(), timeSeriesIdDescriptor.getTimezoneName(), "Office ID does not match") + ); } @FunctionalInterface diff --git a/cwms-data-api/src/test/resources/cwms/cda/data/dto/access_to_water_time_series_data.json b/cwms-data-api/src/test/resources/cwms/cda/data/dto/access_to_water_time_series_data.json index 2e7b96b3f..d03d7ea71 100644 --- a/cwms-data-api/src/test/resources/cwms/cda/data/dto/access_to_water_time_series_data.json +++ b/cwms-data-api/src/test/resources/cwms/cda/data/dto/access_to_water_time_series_data.json @@ -1,6 +1,8 @@ { - "office-id":"SWT", - "location-id": "VANL", + "location-id": { + "name": "VANL", + "office-id":"SWT" + }, "time-series-id-descriptor": { "office-id": "SWT", "time-series-id": "VANL.Stage.Inst.15Minutes.0.Ccp-Rev", From 9fbcb1490c18b0c0237408ac75dc0ade063cefc9 Mon Sep 17 00:00:00 2001 From: Bryson Spilman Date: Wed, 26 Feb 2025 14:15:28 -0800 Subject: [PATCH 3/7] CWMSVUE-634 - Updated name to TypeTimeSeriesIdentifiers. Made dto represent mapping of location id to mapping of ts-type to ts-id. --- ...r.java => TypedTimeSeriesIdentifiers.java} | 46 +++++----- .../dto/TypedTimeSeriesIdentifiersList.java | 88 +++++++++++++++++++ ...ava => TypedTimeSeriesIdentifierTest.java} | 64 ++++++-------- .../test/java/cwms/cda/helpers/DTOMatch.java | 25 ++++-- .../dto/access_to_water_time_series_data.json | 14 --- .../dto/typed_time_series_identifiers.json | 15 ++++ 6 files changed, 168 insertions(+), 84 deletions(-) rename cwms-data-api/src/main/java/cwms/cda/data/dto/{AccessToWaterTimeSeriesIdentifier.java => TypedTimeSeriesIdentifiers.java} (65%) create mode 100644 cwms-data-api/src/main/java/cwms/cda/data/dto/TypedTimeSeriesIdentifiersList.java rename cwms-data-api/src/test/java/cwms/cda/data/dto/{AccessToWaterTimeSeriesIdentifierTest.java => TypedTimeSeriesIdentifierTest.java} (63%) delete mode 100644 cwms-data-api/src/test/resources/cwms/cda/data/dto/access_to_water_time_series_data.json create mode 100644 cwms-data-api/src/test/resources/cwms/cda/data/dto/typed_time_series_identifiers.json diff --git a/cwms-data-api/src/main/java/cwms/cda/data/dto/AccessToWaterTimeSeriesIdentifier.java b/cwms-data-api/src/main/java/cwms/cda/data/dto/TypedTimeSeriesIdentifiers.java similarity index 65% rename from cwms-data-api/src/main/java/cwms/cda/data/dto/AccessToWaterTimeSeriesIdentifier.java rename to cwms-data-api/src/main/java/cwms/cda/data/dto/TypedTimeSeriesIdentifiers.java index 6ae857b93..00a45c0bc 100644 --- a/cwms-data-api/src/main/java/cwms/cda/data/dto/AccessToWaterTimeSeriesIdentifier.java +++ b/cwms-data-api/src/main/java/cwms/cda/data/dto/TypedTimeSeriesIdentifiers.java @@ -23,68 +23,64 @@ */ package cwms.cda.data.dto; +import com.fasterxml.jackson.annotation.JsonIgnore; import com.fasterxml.jackson.annotation.JsonInclude; import com.fasterxml.jackson.annotation.JsonProperty; import com.fasterxml.jackson.databind.PropertyNamingStrategies; import com.fasterxml.jackson.databind.annotation.JsonDeserialize; import com.fasterxml.jackson.databind.annotation.JsonNaming; + import cwms.cda.formatters.Formats; import cwms.cda.formatters.annotations.FormattableWith; import cwms.cda.formatters.json.JsonV1; +import java.util.HashMap; +import java.util.Map; -@FormattableWith(contentType = Formats.JSONV1, formatter = JsonV1.class, aliases = {Formats.JSON, Formats.DEFAULT}) -@JsonDeserialize(builder = AccessToWaterTimeSeriesIdentifier.Builder.class) +@FormattableWith(contentType = Formats.JSONV1, formatter = JsonV1.class, aliases = {Formats.DEFAULT, Formats.JSON}) +@JsonDeserialize(builder = TypedTimeSeriesIdentifiers.Builder.class) @JsonInclude(JsonInclude.Include.NON_NULL) @JsonNaming(PropertyNamingStrategies.KebabCaseStrategy.class) -public final class AccessToWaterTimeSeriesIdentifier extends CwmsDTOBase { +public final class TypedTimeSeriesIdentifiers extends CwmsDTOBase { @JsonProperty(required = true) private final CwmsId locationId; - @JsonProperty(required = true) - private final TimeSeriesIdentifierDescriptor timeSeriesIdDescriptor; - @JsonProperty(required = true) - private final String tsType; - private AccessToWaterTimeSeriesIdentifier(Builder builder) { + private final Map typeToTsIdMap; + + private TypedTimeSeriesIdentifiers(Builder builder) { this.locationId = builder.locationId; - this.timeSeriesIdDescriptor = builder.timeSeriesIdDescriptor; - this.tsType = builder.tsType; + this.typeToTsIdMap = builder.typeToTsIdMap; } public CwmsId getLocationId() { return locationId; } - public TimeSeriesIdentifierDescriptor getTimeSeriesIdDescriptor() { - return timeSeriesIdDescriptor; - } - - public String getTsType() { - return tsType; + public Map getTypeToTsIdMap() { + return typeToTsIdMap; } public static class Builder { private CwmsId locationId; - private TimeSeriesIdentifierDescriptor timeSeriesIdDescriptor; - private String tsType; + private Map typeToTsIdMap = new HashMap<>(); public Builder withLocationId(CwmsId locationId) { this.locationId = locationId; return this; } - public Builder withTimeSeriesIdDescriptor(TimeSeriesIdentifierDescriptor timeSeriesIdDescriptor) { - this.timeSeriesIdDescriptor = timeSeriesIdDescriptor; + public Builder withTypeToTsIdMap(Map typeToTsIdMap) { + this.typeToTsIdMap = typeToTsIdMap; return this; } - public Builder withTsType(String tsType) { - this.tsType = tsType; + @JsonIgnore + public Builder withTypeToTsId(String type, TimeSeriesIdentifierDescriptor tsId) { + this.typeToTsIdMap.put(type, tsId); return this; } - public AccessToWaterTimeSeriesIdentifier build() { - return new AccessToWaterTimeSeriesIdentifier(this); + public TypedTimeSeriesIdentifiers build() { + return new TypedTimeSeriesIdentifiers(this); } - } } diff --git a/cwms-data-api/src/main/java/cwms/cda/data/dto/TypedTimeSeriesIdentifiersList.java b/cwms-data-api/src/main/java/cwms/cda/data/dto/TypedTimeSeriesIdentifiersList.java new file mode 100644 index 000000000..d478df22f --- /dev/null +++ b/cwms-data-api/src/main/java/cwms/cda/data/dto/TypedTimeSeriesIdentifiersList.java @@ -0,0 +1,88 @@ +/* + * MIT License + * + * Copyright (c) 2025 Hydrologic Engineering Center + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ +package cwms.cda.data.dto; + +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.databind.PropertyNamingStrategies; +import com.fasterxml.jackson.databind.annotation.JsonDeserialize; +import com.fasterxml.jackson.databind.annotation.JsonNaming; +import cwms.cda.formatters.Formats; +import cwms.cda.formatters.annotations.FormattableWith; +import cwms.cda.formatters.json.JsonV1; + +import java.util.ArrayList; +import java.util.Collection; +import java.util.Collections; +import java.util.List; + +@JsonDeserialize(builder = TypedTimeSeriesIdentifiersList.Builder.class) +@JsonInclude(JsonInclude.Include.NON_NULL) +@JsonNaming(PropertyNamingStrategies.KebabCaseStrategy.class) +@FormattableWith(contentType = Formats.JSONV1, formatter = JsonV1.class, aliases = {Formats.DEFAULT, Formats.JSON}) +public class TypedTimeSeriesIdentifiersList extends CwmsDTOPaginated { + + private final List typedTimeSeriesIdentifiers; + private final int offset; + + private TypedTimeSeriesIdentifiersList(int offset, int pageSize, Integer total, List identifiersList) { + super(Integer.toString(offset), pageSize, total); + this.typedTimeSeriesIdentifiers = new ArrayList<>(identifiersList); + this.offset = offset; + } + + public List getTypedTimeSeriesIdentifiers() { + return Collections.unmodifiableList(typedTimeSeriesIdentifiers); + } + + public static class Builder { + private final int offset; + private final int pageSize; + private final Integer total; + + private List typedTimeSeriesIdentifiers = new ArrayList<>(); + + public Builder(int offset, int pageSize, Integer total) { + this.offset = offset; + this.pageSize = pageSize; + this.total = total; + } + + public Builder withTypedTimeSeriesIdentifiers(Collection identifiersList) { + this.typedTimeSeriesIdentifiers = new ArrayList<>(identifiersList); + return this; + } + + public TypedTimeSeriesIdentifiersList build() { + TypedTimeSeriesIdentifiersList retval = new TypedTimeSeriesIdentifiersList(offset, pageSize, total, typedTimeSeriesIdentifiers); + + if (this.typedTimeSeriesIdentifiers.size() == this.pageSize) { + String cursor = Integer.toString(retval.offset + retval.typedTimeSeriesIdentifiers.size()); + retval.nextPage = encodeCursor(cursor, retval.pageSize, retval.total); + } else { + retval.nextPage = null; + } + return retval; + } + } +} diff --git a/cwms-data-api/src/test/java/cwms/cda/data/dto/AccessToWaterTimeSeriesIdentifierTest.java b/cwms-data-api/src/test/java/cwms/cda/data/dto/TypedTimeSeriesIdentifierTest.java similarity index 63% rename from cwms-data-api/src/test/java/cwms/cda/data/dto/AccessToWaterTimeSeriesIdentifierTest.java rename to cwms-data-api/src/test/java/cwms/cda/data/dto/TypedTimeSeriesIdentifierTest.java index 823ac15b1..4e7fb4028 100644 --- a/cwms-data-api/src/test/java/cwms/cda/data/dto/AccessToWaterTimeSeriesIdentifierTest.java +++ b/cwms-data-api/src/test/java/cwms/cda/data/dto/TypedTimeSeriesIdentifierTest.java @@ -25,6 +25,8 @@ package cwms.cda.data.dto; import java.time.ZoneId; +import java.util.HashMap; +import java.util.Map; import static org.junit.jupiter.api.Assertions.*; import cwms.cda.api.errors.FieldException; @@ -36,7 +38,7 @@ import org.apache.commons.io.IOUtils; import org.junit.jupiter.api.Test; -final class AccessToWaterTimeSeriesIdentifierTest { +final class TypedTimeSeriesIdentifierTest { @Test void createAccessToWaterTimeSeriesData_allFieldsProvided_success() { @@ -47,16 +49,23 @@ void createAccessToWaterTimeSeriesData_allFieldsProvided_success() { .withIntervalOffsetMinutes(0L) .withZoneId(ZoneId.of("UTC")) .build(); - AccessToWaterTimeSeriesIdentifier item = new AccessToWaterTimeSeriesIdentifier.Builder() + TimeSeriesIdentifierDescriptor tsDescriptor2 = new TimeSeriesIdentifierDescriptor.Builder() + .withTimeSeriesId("VANL.Flow.Inst.15Minutes.0.Ccp-Rev") + .withOfficeId("SWT") + .withActive(true) + .withIntervalOffsetMinutes(0L) + .withZoneId(ZoneId.of("UTC")) + .build(); + TypedTimeSeriesIdentifiers items = new TypedTimeSeriesIdentifiers.Builder() .withLocationId(CwmsId.buildCwmsId("SWT", "VANL")) - .withTimeSeriesIdDescriptor(tsDescriptor) - .withTsType("STAGE") + .withTypeToTsId("STAGE", tsDescriptor) + .withTypeToTsId("OUTFLOW", tsDescriptor2) .build(); assertAll( - () -> DTOMatch.assertMatch(CwmsId.buildCwmsId("SWT", "VANL"), item.getLocationId(), "The location ID does not match the provided value"), - () -> DTOMatch.assertMatch(tsDescriptor, item.getTimeSeriesIdDescriptor()), - () -> assertEquals("STAGE", item.getTsType(), "The time series type does not match the provided value") + () -> DTOMatch.assertMatch(CwmsId.buildCwmsId("SWT", "VANL"), items.getLocationId(), "Location ID"), + () -> DTOMatch.assertMatch(tsDescriptor, items.getTypeToTsIdMap().get("STAGE")), + () -> DTOMatch.assertMatch(tsDescriptor2, items.getTypeToTsIdMap().get("OUTFLOW")) ); } @@ -69,30 +78,15 @@ void createAccessToWaterTimeSeriesData_missingField_throwsFieldException() { .withIntervalOffsetMinutes(0L) .withZoneId(ZoneId.of("UTC")) .build(); + Map typeToTsIdMap = new HashMap<>(); + typeToTsIdMap.put("STAGE", tsDescriptor); assertAll( () -> assertThrows(FieldException.class, () -> { - AccessToWaterTimeSeriesIdentifier item = new AccessToWaterTimeSeriesIdentifier.Builder() - .withTimeSeriesIdDescriptor(tsDescriptor) - .withTsType("STAGE") - .build(); - item.validate(); - }, "The validate method should have thrown a FieldException because the location ID is missing"), - - () -> assertThrows(FieldException.class, () -> { - AccessToWaterTimeSeriesIdentifier item = new AccessToWaterTimeSeriesIdentifier.Builder() - .withLocationId(CwmsId.buildCwmsId("SWT", "VANL")) - .withTsType("STAGE") - .build(); - item.validate(); - }, "The validate method should have thrown a FieldException because the TimeSeries ID is missing"), - - () -> assertThrows(FieldException.class, () -> { - AccessToWaterTimeSeriesIdentifier item = new AccessToWaterTimeSeriesIdentifier.Builder() - .withLocationId(CwmsId.buildCwmsId("SWT", "VANL")) - .withTimeSeriesIdDescriptor(tsDescriptor) + TypedTimeSeriesIdentifiers item = new TypedTimeSeriesIdentifiers.Builder() + .withTypeToTsIdMap(typeToTsIdMap) .build(); item.validate(); - }, "The validate method should have thrown a FieldException because the time series type is missing") + }, "The validate method should have thrown a FieldException because the location ID is missing") ); } @@ -105,15 +99,14 @@ void createAccessToWaterTimeSeriesData_serialize_roundtrip() { .withIntervalOffsetMinutes(0L) .withZoneId(ZoneId.of("UTC")) .build(); - AccessToWaterTimeSeriesIdentifier data = new AccessToWaterTimeSeriesIdentifier.Builder() + TypedTimeSeriesIdentifiers data = new TypedTimeSeriesIdentifiers.Builder() .withLocationId(CwmsId.buildCwmsId("SWT", "VANL")) - .withTimeSeriesIdDescriptor(tsDescriptor) - .withTsType("STAGE") + .withTypeToTsId("STAGE", tsDescriptor) .build(); ContentType contentType = new ContentType(Formats.JSON); String json = Formats.format(contentType, data); - AccessToWaterTimeSeriesIdentifier deserialized = Formats.parseContent(contentType, json, AccessToWaterTimeSeriesIdentifier.class); + TypedTimeSeriesIdentifiers deserialized = Formats.parseContent(contentType, json, TypedTimeSeriesIdentifiers.class); DTOMatch.assertMatch(data, deserialized); } @@ -126,17 +119,16 @@ void createAccessToWaterTimeSeriesData_deserialize() throws Exception { .withIntervalOffsetMinutes(0L) .withZoneId(ZoneId.of("UTC")) .build(); - AccessToWaterTimeSeriesIdentifier expected = new AccessToWaterTimeSeriesIdentifier.Builder() + TypedTimeSeriesIdentifiers expected = new TypedTimeSeriesIdentifiers.Builder() .withLocationId(CwmsId.buildCwmsId("SWT", "VANL")) - .withTimeSeriesIdDescriptor(tsDescriptor) - .withTsType("STAGE") + .withTypeToTsId("STAGE", tsDescriptor) .build(); - InputStream resource = this.getClass().getResourceAsStream("/cwms/cda/data/dto/access_to_water_time_series_data.json"); + InputStream resource = this.getClass().getResourceAsStream("/cwms/cda/data/dto/typed_time_series_identifiers.json"); assertNotNull(resource); String json = IOUtils.toString(resource, StandardCharsets.UTF_8); ContentType contentType = new ContentType(Formats.JSON); - AccessToWaterTimeSeriesIdentifier deserialized = Formats.parseContent(contentType, json, AccessToWaterTimeSeriesIdentifier.class); + TypedTimeSeriesIdentifiers deserialized = Formats.parseContent(contentType, json, TypedTimeSeriesIdentifiers.class); DTOMatch.assertMatch(expected, deserialized); } } diff --git a/cwms-data-api/src/test/java/cwms/cda/helpers/DTOMatch.java b/cwms-data-api/src/test/java/cwms/cda/helpers/DTOMatch.java index a9c59a4b8..c6438c99f 100644 --- a/cwms-data-api/src/test/java/cwms/cda/helpers/DTOMatch.java +++ b/cwms-data-api/src/test/java/cwms/cda/helpers/DTOMatch.java @@ -24,11 +24,12 @@ package cwms.cda.helpers; -import cwms.cda.data.dto.AccessToWaterTimeSeriesIdentifier; import cwms.cda.data.dto.CwmsIdTimeExtentsEntry; import cwms.cda.data.dto.TimeExtents; import cwms.cda.data.dto.TimeSeriesExtents; import cwms.cda.data.dto.TimeSeriesIdentifierDescriptor; +import cwms.cda.data.dto.TypedTimeSeriesIdentifiers; +import cwms.cda.data.dto.TypedTimeSeriesIdentifiersList; import cwms.cda.data.dto.location.kind.Lock; import cwms.cda.data.dto.CwmsDTOBase; import cwms.cda.data.dto.location.kind.GateChange; @@ -603,14 +604,6 @@ public static void assertMatch(CwmsIdTimeExtentsEntry first, CwmsIdTimeExtentsEn ); } - public static void assertMatch(AccessToWaterTimeSeriesIdentifier first, AccessToWaterTimeSeriesIdentifier second) { - assertAll( - () -> assertMatch(first.getTimeSeriesIdDescriptor(), second.getTimeSeriesIdDescriptor()), - () -> assertEquals(first.getTsType(), second.getTsType()), - () -> assertMatch(first.getLocationId(), second.getLocationId()) - ); - } - public static void assertMatch(TimeSeriesIdentifierDescriptor tsDescriptor, TimeSeriesIdentifierDescriptor timeSeriesIdDescriptor) { assertAll( () -> assertEquals(tsDescriptor.getIntervalOffsetMinutes(), timeSeriesIdDescriptor.getIntervalOffsetMinutes(), "Identifier does not match"), @@ -620,6 +613,20 @@ public static void assertMatch(TimeSeriesIdentifierDescriptor tsDescriptor, Time ); } + public static void assertMatch(TypedTimeSeriesIdentifiers first, TypedTimeSeriesIdentifiers second) + { + assertAll( + () -> assertMatch(first.getLocationId(), second.getLocationId()), + () -> assertEquals(first.getTypeToTsIdMap().size(), second.getTypeToTsIdMap().size(), "Type to TS ID map sizes do not match"), + () -> first.getTypeToTsIdMap().forEach((type, tsId) -> { + if (!second.getTypeToTsIdMap().containsKey(type)) { + fail("tsType " + type + " not found in both tsType to tsId maps"); + } + assertMatch(tsId, second.getTypeToTsIdMap().get(type)); + }) + ); + } + @FunctionalInterface public interface AssertMatchMethod{ void assertMatch(T first, T second); diff --git a/cwms-data-api/src/test/resources/cwms/cda/data/dto/access_to_water_time_series_data.json b/cwms-data-api/src/test/resources/cwms/cda/data/dto/access_to_water_time_series_data.json deleted file mode 100644 index d03d7ea71..000000000 --- a/cwms-data-api/src/test/resources/cwms/cda/data/dto/access_to_water_time_series_data.json +++ /dev/null @@ -1,14 +0,0 @@ -{ - "location-id": { - "name": "VANL", - "office-id":"SWT" - }, - "time-series-id-descriptor": { - "office-id": "SWT", - "time-series-id": "VANL.Stage.Inst.15Minutes.0.Ccp-Rev", - "timezone-name": "UTC", - "interval-offset-minutes": 0, - "active": true - }, - "ts-type": "STAGE" -} \ No newline at end of file diff --git a/cwms-data-api/src/test/resources/cwms/cda/data/dto/typed_time_series_identifiers.json b/cwms-data-api/src/test/resources/cwms/cda/data/dto/typed_time_series_identifiers.json new file mode 100644 index 000000000..519b87fc9 --- /dev/null +++ b/cwms-data-api/src/test/resources/cwms/cda/data/dto/typed_time_series_identifiers.json @@ -0,0 +1,15 @@ +{ + "location-id": { + "name": "VANL", + "office-id": "SWT" + }, + "type-to-ts-id-map": { + "STAGE": { + "office-id": "SWT", + "time-series-id": "VANL.Stage.Inst.15Minutes.0.Ccp-Rev", + "timezone-name": "UTC", + "interval-offset-minutes": 0, + "active": true + } + } +} From 4950f401b5cfae823802f8dcfccc9f9a26df099f Mon Sep 17 00:00:00 2001 From: Bryson Spilman Date: Thu, 27 Feb 2025 09:08:16 -0800 Subject: [PATCH 4/7] CWMSVUE-634 - Adding in support for pagination to DTOs --- .../dto/TypedTimeSeriesIdentifiersList.java | 47 ++++++++++++++----- .../test/java/cwms/cda/helpers/DTOMatch.java | 1 - 2 files changed, 35 insertions(+), 13 deletions(-) diff --git a/cwms-data-api/src/main/java/cwms/cda/data/dto/TypedTimeSeriesIdentifiersList.java b/cwms-data-api/src/main/java/cwms/cda/data/dto/TypedTimeSeriesIdentifiersList.java index d478df22f..7dd9f0994 100644 --- a/cwms-data-api/src/main/java/cwms/cda/data/dto/TypedTimeSeriesIdentifiersList.java +++ b/cwms-data-api/src/main/java/cwms/cda/data/dto/TypedTimeSeriesIdentifiersList.java @@ -43,12 +43,33 @@ public class TypedTimeSeriesIdentifiersList extends CwmsDTOPaginated { private final List typedTimeSeriesIdentifiers; - private final int offset; - private TypedTimeSeriesIdentifiersList(int offset, int pageSize, Integer total, List identifiersList) { - super(Integer.toString(offset), pageSize, total); + private TypedTimeSeriesIdentifiersList(String cursor, int pageSize, Integer total, List identifiersList) { + super(cursor, pageSize, total); this.typedTimeSeriesIdentifiers = new ArrayList<>(identifiersList); - this.offset = offset; + } + + public static String getOffice(String cursor) + { + String[] parts = CwmsDTOPaginated.decodeCursor(cursor); + if (parts.length > 1) { + String[] idAndOffice = CwmsDTOPaginated.decodeCursor(parts[0]); + if (idAndOffice.length > 0) { + return idAndOffice[0]; + } + } + return null; + } + + public static String getId(String cursor) { + String[] parts = CwmsDTOPaginated.decodeCursor(cursor); + if (parts.length > 1) { + String[] idAndOffice = CwmsDTOPaginated.decodeCursor(parts[0]); + if (idAndOffice.length > 1) { + return idAndOffice[1]; + } + } + return null; } public List getTypedTimeSeriesIdentifiers() { @@ -56,14 +77,14 @@ public List getTypedTimeSeriesIdentifiers() { } public static class Builder { - private final int offset; + private final String cursor; private final int pageSize; private final Integer total; private List typedTimeSeriesIdentifiers = new ArrayList<>(); - public Builder(int offset, int pageSize, Integer total) { - this.offset = offset; + public Builder(String cursor, int pageSize, Integer total) { + this.cursor = cursor; this.pageSize = pageSize; this.total = total; } @@ -74,15 +95,17 @@ public Builder withTypedTimeSeriesIdentifiers(Collection Date: Wed, 5 Mar 2025 12:03:09 -0800 Subject: [PATCH 5/7] CWMSVUE-634 - Implementing A2W endpoint, DTOs, Dao and Controller. Tests and further updates are needed here, but pausing work for now. --- cwms-data-api/build.gradle | 2 +- .../src/main/java/cwms/cda/ApiServlet.java | 4 + .../cwms/cda/api/AccessToWaterController.java | 164 ++++++++++ .../dao/AccessToWaterRetrievalParameters.java | 44 +++ .../data/dao/AccessToWaterTimeSeriesDao.java | 189 +++++++++++ .../main/java/cwms/cda/data/dto/CwmsId.java | 56 ++++ ....java => TimeSeriesIdentifiersByType.java} | 28 +- ...a => TimeSeriesIdentifiersByTypeList.java} | 57 ++-- .../cwms/cda/data/dto/TimeSeriesMetaData.java | 66 ++++ .../api/AccessToWaterControllerTestIT.java | 293 +++++++++++++++++ .../dao/AccessToWaterTimeSeriesDaoTest.java | 210 ++++++++++++ .../dto/TimeSeriesIdentifiersByTypeTest.java | 299 ++++++++++++++++++ .../dto/TypedTimeSeriesIdentifierTest.java | 134 -------- .../test/java/cwms/cda/helpers/DTOMatch.java | 66 +++- .../fixtures/CwmsDataApiSetupCallback.java | 2 +- .../dto/time_series_identifiers_by_type.json | 126 ++++++++ .../dto/typed_time_series_identifiers.json | 15 - 17 files changed, 1562 insertions(+), 193 deletions(-) create mode 100644 cwms-data-api/src/main/java/cwms/cda/api/AccessToWaterController.java create mode 100644 cwms-data-api/src/main/java/cwms/cda/data/dao/AccessToWaterRetrievalParameters.java create mode 100644 cwms-data-api/src/main/java/cwms/cda/data/dao/AccessToWaterTimeSeriesDao.java rename cwms-data-api/src/main/java/cwms/cda/data/dto/{TypedTimeSeriesIdentifiers.java => TimeSeriesIdentifiersByType.java} (72%) rename cwms-data-api/src/main/java/cwms/cda/data/dto/{TypedTimeSeriesIdentifiersList.java => TimeSeriesIdentifiersByTypeList.java} (60%) create mode 100644 cwms-data-api/src/main/java/cwms/cda/data/dto/TimeSeriesMetaData.java create mode 100644 cwms-data-api/src/test/java/cwms/cda/api/AccessToWaterControllerTestIT.java create mode 100644 cwms-data-api/src/test/java/cwms/cda/data/dao/AccessToWaterTimeSeriesDaoTest.java create mode 100644 cwms-data-api/src/test/java/cwms/cda/data/dto/TimeSeriesIdentifiersByTypeTest.java delete mode 100644 cwms-data-api/src/test/java/cwms/cda/data/dto/TypedTimeSeriesIdentifierTest.java create mode 100644 cwms-data-api/src/test/resources/cwms/cda/data/dto/time_series_identifiers_by_type.json delete mode 100644 cwms-data-api/src/test/resources/cwms/cda/data/dto/typed_time_series_identifiers.json diff --git a/cwms-data-api/build.gradle b/cwms-data-api/build.gradle index 4459c5302..a4334e12b 100644 --- a/cwms-data-api/build.gradle +++ b/cwms-data-api/build.gradle @@ -260,7 +260,7 @@ task run(type: JavaExec) { } task integrationTests(type: Test) { - dependsOn test +// dependsOn test dependsOn generateConfig dependsOn war diff --git a/cwms-data-api/src/main/java/cwms/cda/ApiServlet.java b/cwms-data-api/src/main/java/cwms/cda/ApiServlet.java index d1a410f66..df542220a 100644 --- a/cwms-data-api/src/main/java/cwms/cda/ApiServlet.java +++ b/cwms-data-api/src/main/java/cwms/cda/ApiServlet.java @@ -24,6 +24,7 @@ package cwms.cda; +import cwms.cda.api.AccessToWaterController; import static cwms.cda.api.Controllers.CONTRACT_NAME; import static cwms.cda.api.Controllers.LOCATION_ID; import static cwms.cda.api.Controllers.NAME; @@ -235,6 +236,7 @@ "/stream-locations/*", "/stream-reaches/*", "/measurements/*", + "/access-to-water/*", "/blobs/*", "/clobs/*", "/pools/*", @@ -604,6 +606,8 @@ protected void configureRoutes() { addCacheControl(measTimeExtents, 5, TimeUnit.MINUTES); cdaCrudCache(format(measurements + "{%s}", LOCATION_ID), new cwms.cda.api.MeasurementController(metrics), requiredRoles,5, TimeUnit.MINUTES); + cdaCrudCache(format("/access-to-water/{%s}", LOCATION_ID), + new AccessToWaterController(metrics), requiredRoles,5, TimeUnit.MINUTES); cdaCrudCache("/blobs/{blob-id}", new BlobController(metrics), requiredRoles,5, TimeUnit.MINUTES); cdaCrudCache("/clobs/{clob-id}", diff --git a/cwms-data-api/src/main/java/cwms/cda/api/AccessToWaterController.java b/cwms-data-api/src/main/java/cwms/cda/api/AccessToWaterController.java new file mode 100644 index 000000000..750bceeca --- /dev/null +++ b/cwms-data-api/src/main/java/cwms/cda/api/AccessToWaterController.java @@ -0,0 +1,164 @@ +/* + * MIT License + * + * Copyright (c) 2025 Hydrologic Engineering Center + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ +package cwms.cda.api; + +import com.codahale.metrics.Histogram; +import com.codahale.metrics.MetricRegistry; +import static com.codahale.metrics.MetricRegistry.name; +import com.codahale.metrics.Timer; +import static cwms.cda.api.Controllers.CREATE; +import static cwms.cda.api.Controllers.CURSOR; +import static cwms.cda.api.Controllers.DELETE; +import static cwms.cda.api.Controllers.GET_ALL; +import static cwms.cda.api.Controllers.GET_ONE; +import static cwms.cda.api.Controllers.LOCATION_MASK; +import static cwms.cda.api.Controllers.NOT_SUPPORTED_YET; +import static cwms.cda.api.Controllers.OFFICE_MASK; +import static cwms.cda.api.Controllers.PAGE; +import static cwms.cda.api.Controllers.PAGE_SIZE; +import static cwms.cda.api.Controllers.RESULTS; +import static cwms.cda.api.Controllers.SIZE; +import static cwms.cda.api.Controllers.STATUS_200; +import static cwms.cda.api.Controllers.UPDATE; +import static cwms.cda.api.Controllers.queryParamAsClass; +import cwms.cda.api.errors.CdaError; +import cwms.cda.data.dao.AccessToWaterRetrievalParameters; +import static cwms.cda.data.dao.JooqDao.getDslContext; +import cwms.cda.data.dao.AccessToWaterTimeSeriesDao; +import cwms.cda.data.dto.CwmsDTOPaginated; +import cwms.cda.data.dto.TimeSeriesIdentifiersByTypeList; +import cwms.cda.formatters.ContentType; +import cwms.cda.formatters.Formats; +import io.javalin.apibuilder.CrudHandler; +import io.javalin.core.util.Header; +import io.javalin.http.Context; +import io.javalin.http.HttpCode; +import io.javalin.plugin.openapi.annotations.OpenApi; +import io.javalin.plugin.openapi.annotations.OpenApiContent; +import io.javalin.plugin.openapi.annotations.OpenApiParam; +import io.javalin.plugin.openapi.annotations.OpenApiResponse; +import javax.servlet.http.HttpServletResponse; +import org.jetbrains.annotations.NotNull; +import org.jooq.DSLContext; + +public final class AccessToWaterController implements CrudHandler { + private static final String TAG = "AccessToWater"; + private final MetricRegistry metrics; + private final Histogram requestResultSize; + private static final int DEFAULT_PAGE_SIZE = 20; + + public AccessToWaterController(MetricRegistry metrics) { + this.metrics = metrics; + String className = this.getClass().getName(); + requestResultSize = this.metrics.histogram(name(className, RESULTS, SIZE)); + } + + private Timer.Context markAndTime(String subject) { + return Controllers.markAndTime(metrics, getClass().getName(), subject); + } + + @OpenApi(ignore = true) + @Override + public void create(@NotNull Context context) { + try (final Timer.Context ignored = markAndTime(CREATE)) { + throw new UnsupportedOperationException(NOT_SUPPORTED_YET); + } + } + + @OpenApi(ignore = true) + @Override + public void delete(@NotNull Context context, @NotNull String s) { + try (final Timer.Context ignored = markAndTime(DELETE)) { + throw new UnsupportedOperationException(NOT_SUPPORTED_YET); + } + } + + @OpenApi( + queryParams = { + @OpenApiParam(name = OFFICE_MASK, description = "Office Id used to filter the results."), + @OpenApiParam(name = LOCATION_MASK, description = "Location Id used to filter the results."), + @OpenApiParam(name = PAGE, + description = "This end point can return a lot of data, this " + + "identifies where in the request you are. This is an opaque " + + "value, and can be obtained from the 'next-page' value in " + + "the response."), + @OpenApiParam(name = PAGE_SIZE, + type = Integer.class, + description = "How many entries per page returned. Default " + + DEFAULT_PAGE_SIZE + "."), + }, + responses = { + @OpenApiResponse(status = STATUS_200, content = { + @OpenApiContent(isArray = true, type = Formats.JSONV1, from = TimeSeriesIdentifiersByTypeList.class) + }) + }, + description = "Returns matching time series identifiers for access to water.", + tags = {TAG} + ) + @Override + public void getAll(@NotNull Context ctx) { + String officeIdMask = ctx.queryParam(OFFICE_MASK); + String locationIdMask = ctx.queryParam(LOCATION_MASK); + String cursor = queryParamAsClass(ctx, new String[]{PAGE, CURSOR}, + String.class, "", metrics, name(AccessToWaterController.class.getName(), GET_ALL)); + if (!CwmsDTOPaginated.CURSOR_CHECK.invoke(cursor)) { + ctx.json(new CdaError("cursor or page passed in but failed validation")) + .status(HttpCode.BAD_REQUEST); + return; + } + int pageSize = queryParamAsClass(ctx, new String[]{PAGE_SIZE}, Integer.class, DEFAULT_PAGE_SIZE, metrics, + name(AccessToWaterController.class.getName(), GET_ALL)); + + try (Timer.Context ignored = markAndTime(GET_ALL)) { + DSLContext dsl = getDslContext(ctx); + AccessToWaterTimeSeriesDao dao = new AccessToWaterTimeSeriesDao(dsl); + AccessToWaterRetrievalParameters retrievalParams = new AccessToWaterRetrievalParameters(officeIdMask, locationIdMask); + TimeSeriesIdentifiersByTypeList result = dao.retrieveAccessToWaterTimeSeriesIds(retrievalParams, cursor, pageSize); + + String formatHeader = ctx.header(Header.ACCEPT); + ContentType contentType = Formats.parseHeader(formatHeader, TimeSeriesIdentifiersByTypeList.class); + ctx.contentType(contentType.toString()); + String serialized = Formats.format(contentType, result); + ctx.result(serialized); + ctx.status(HttpServletResponse.SC_OK); + requestResultSize.update(serialized.length()); + } + } + + @OpenApi(ignore = true) + @Override + public void getOne(@NotNull Context context, @NotNull String s) { + try (final Timer.Context ignored = markAndTime(GET_ONE)) { + throw new UnsupportedOperationException(NOT_SUPPORTED_YET); + } + } + + @OpenApi(ignore = true) + @Override + public void update(@NotNull Context context, @NotNull String s) { + try (final Timer.Context ignored = markAndTime(UPDATE)) { + throw new UnsupportedOperationException(NOT_SUPPORTED_YET); + } + } +} diff --git a/cwms-data-api/src/main/java/cwms/cda/data/dao/AccessToWaterRetrievalParameters.java b/cwms-data-api/src/main/java/cwms/cda/data/dao/AccessToWaterRetrievalParameters.java new file mode 100644 index 000000000..286a476bc --- /dev/null +++ b/cwms-data-api/src/main/java/cwms/cda/data/dao/AccessToWaterRetrievalParameters.java @@ -0,0 +1,44 @@ +/* + * MIT License + * + * Copyright (c) 2025 Hydrologic Engineering Center + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ +package cwms.cda.data.dao; +import java.util.Optional; + +public class AccessToWaterRetrievalParameters { + private final String locationIdsMask; + private final String officeIdsMask; + + public AccessToWaterRetrievalParameters(String locationIdsMask, String officeIdsMask) { + this.locationIdsMask = locationIdsMask; + this.officeIdsMask = officeIdsMask; + } + + public Optional getLocationId() { + return Optional.ofNullable(locationIdsMask); + } + + public Optional getOfficeId() { + return Optional.ofNullable(officeIdsMask); + } + +} diff --git a/cwms-data-api/src/main/java/cwms/cda/data/dao/AccessToWaterTimeSeriesDao.java b/cwms-data-api/src/main/java/cwms/cda/data/dao/AccessToWaterTimeSeriesDao.java new file mode 100644 index 000000000..71975b28a --- /dev/null +++ b/cwms-data-api/src/main/java/cwms/cda/data/dao/AccessToWaterTimeSeriesDao.java @@ -0,0 +1,189 @@ +/* + * MIT License + * + * Copyright (c) 2025 Hydrologic Engineering Center + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ +package cwms.cda.data.dao; + +import cwms.cda.data.dto.CwmsDTOPaginated; +import cwms.cda.data.dto.CwmsId; +import cwms.cda.data.dto.TimeSeriesIdentifierDescriptor; +import cwms.cda.data.dto.TimeSeriesIdentifiersByType; +import cwms.cda.data.dto.TimeSeriesIdentifiersByTypeList; +import cwms.cda.data.dto.TimeSeriesMetaData; +import cwms.cda.data.dto.timeseriesprofile.TimeSeriesProfile; +import java.math.BigDecimal; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; +import static java.util.stream.Collectors.toList; +import org.jooq.Condition; +import org.jooq.DSLContext; +import org.jooq.Record1; +import org.jooq.Record5; +import org.jooq.SelectConditionStep; +import static org.jooq.impl.DSL.asterisk; +import static org.jooq.impl.DSL.count; +import static org.jooq.impl.DSL.noCondition; +import static org.jooq.impl.DSL.upper; +import static usace.cwms.db.jooq.codegen.tables.AV_A2W_TS_CODES_BY_LOC2.AV_A2W_TS_CODES_BY_LOC2; +import static usace.cwms.db.jooq.codegen.tables.AV_CWMS_TS_ID.AV_CWMS_TS_ID; + +public final class AccessToWaterTimeSeriesDao extends JooqDao { + + public AccessToWaterTimeSeriesDao(DSLContext dsl) { + super(dsl); + } + + public TimeSeriesIdentifiersByTypeList retrieveAccessToWaterTimeSeriesIds(AccessToWaterRetrievalParameters retrievalParameters, String cursor, int pageSize) + { + int total = 0; + String cursorOffice = null; + String cursorLocId = null; + + Condition officeCondition = retrievalParameters.getOfficeId() + .map(AV_A2W_TS_CODES_BY_LOC2.DB_OFFICE_ID::eq) + .orElse(noCondition()); + + Condition locationCondition = retrievalParameters.getLocationId() + .map(AV_A2W_TS_CODES_BY_LOC2.LOCATION_ID::eq) + .orElse(noCondition()); + + Condition whereClause = locationCondition.and(officeCondition); + + if (cursor == null || cursor.isEmpty()) { + SelectConditionStep> count = dsl.select(count(asterisk())) + .from(AV_A2W_TS_CODES_BY_LOC2) + .leftOuterJoin(AV_CWMS_TS_ID) + .on(AV_A2W_TS_CODES_BY_LOC2.TS_CODE.equal(AV_CWMS_TS_ID.TS_CODE)) + .where(whereClause);// Ensures rows matching cwmsIds are returned + Record1 rec = count.fetchOne(); + if(rec != null) { + total = rec.value1(); + } + } else { + final String[] parts = CwmsDTOPaginated.decodeCursor(cursor, "||"); + + if (parts.length > 1) { + cursorOffice = TimeSeriesIdentifiersByTypeList.getOffice(cursor); + cursorLocId = TimeSeriesIdentifiersByTypeList.getId(cursor); + total = Integer.parseInt(parts[1]); + pageSize = Integer.parseInt(parts[2]); + } + } + int finalizedPageSize = pageSize; + + Condition moreInSameOffice = cursorLocId == null || cursorOffice == null ? noCondition() : + AV_A2W_TS_CODES_BY_LOC2.DB_OFFICE_ID.eq(cursorOffice.toUpperCase()) + .and(upper(AV_A2W_TS_CODES_BY_LOC2.LOCATION_ID).greaterThan(cursorLocId.toUpperCase())); + Condition nextOffices = cursorOffice == null ? noCondition(): + upper(AV_A2W_TS_CODES_BY_LOC2.DB_OFFICE_ID).greaterThan(cursorOffice.toUpperCase()); + Condition pagingCondition = moreInSameOffice.or(nextOffices); + + Map> locationToTsTypeMap = new LinkedHashMap<>(); + + connection(dsl, conn -> + dsl.select(AV_A2W_TS_CODES_BY_LOC2.CWMS_TS_ID, AV_A2W_TS_CODES_BY_LOC2.LOCATION_ID, + AV_A2W_TS_CODES_BY_LOC2.DB_OFFICE_ID, AV_CWMS_TS_ID.TIME_ZONE_ID, + AV_CWMS_TS_ID.INTERVAL_UTC_OFFSET) + .from(AV_A2W_TS_CODES_BY_LOC2) + .leftOuterJoin(AV_CWMS_TS_ID) + .on(AV_A2W_TS_CODES_BY_LOC2.TS_CODE.equal(AV_CWMS_TS_ID.TS_CODE)) + .where(whereClause) + .and(pagingCondition) + .orderBy(AV_A2W_TS_CODES_BY_LOC2.DB_OFFICE_ID, AV_A2W_TS_CODES_BY_LOC2.LOCATION_ID) + .limit(finalizedPageSize) + .fetchStream() + .forEach(row -> { + String locationId = row.get(AV_A2W_TS_CODES_BY_LOC2.LOCATION_ID); + String tsTypeFromRow = row.get(AV_A2W_TS_CODES_BY_LOC2.TS_TYPE); + String officeId = row.get(AV_A2W_TS_CODES_BY_LOC2.DB_OFFICE_ID); + + TimeSeriesMetaData tsId = buildTsId(row); + locationToTsTypeMap.computeIfAbsent(new InsensitiveCwmsId(CwmsId.buildCwmsId(locationId, officeId)), + k -> new LinkedHashMap<>()).put(tsTypeFromRow, tsId); + }) + ); + + List identifiersList = locationToTsTypeMap.entrySet().stream() + .map(entry -> new TimeSeriesIdentifiersByType.Builder() + .withLocationId(entry.getKey().getCwmsId()) + .withTimeSeriesIds(entry.getValue()) + .build()) + .collect(toList()); + + return new TimeSeriesIdentifiersByTypeList.Builder() + .withCursor(cursor) + .withTotal(total) + .withPageSize(pageSize) + .withTimeSeriesIdsForLocations(identifiersList) + .build(); + + } + + private TimeSeriesMetaData buildTsId(Record5 row) + { + String timeSeriesId = row.get(AV_A2W_TS_CODES_BY_LOC2.CWMS_TS_ID); + String officeId = row.get(AV_A2W_TS_CODES_BY_LOC2.DB_OFFICE_ID); + BigDecimal intervalUtcMinuteOffset = row.get(AV_CWMS_TS_ID.INTERVAL_UTC_OFFSET); + String timeZoneId = row.get(AV_CWMS_TS_ID.TIME_ZONE_ID); + return new TimeSeriesMetaData.Builder() + .withTsId(new TimeSeriesIdentifierDescriptor.Builder() + .withOfficeId(officeId) + .withTimezoneName(timeZoneId) + .withTimeSeriesId(timeSeriesId) + .withIntervalOffsetMinutes(intervalUtcMinuteOffset.longValue()) + .build()) + .build(); + } + + //building inner class that wraps CwmsId but implements equals hashcode called InsensitiveCwmsId + private static class InsensitiveCwmsId { + private final CwmsId cwmsId; + + public InsensitiveCwmsId(CwmsId cwmsId) { + this.cwmsId = cwmsId; + } + + private CwmsId getCwmsId() { + return cwmsId; + } + + @Override + public boolean equals(Object o) { + if (this == o) { + return true; + } + if (o == null || getClass() != o.getClass()) { + return false; + } + InsensitiveCwmsId that = (InsensitiveCwmsId) o; + return cwmsId.getName().equalsIgnoreCase(that.cwmsId.getName()) && + cwmsId.getOfficeId().equalsIgnoreCase(that.cwmsId.getOfficeId()); + } + + @Override + public int hashCode() { + return cwmsId.getName().toUpperCase().hashCode() + cwmsId.getOfficeId().toUpperCase().hashCode(); + } + } + +} diff --git a/cwms-data-api/src/main/java/cwms/cda/data/dto/CwmsId.java b/cwms-data-api/src/main/java/cwms/cda/data/dto/CwmsId.java index ef3493047..e1e5a49fb 100644 --- a/cwms-data-api/src/main/java/cwms/cda/data/dto/CwmsId.java +++ b/cwms-data-api/src/main/java/cwms/cda/data/dto/CwmsId.java @@ -24,6 +24,8 @@ package cwms.cda.data.dto; +import com.fasterxml.jackson.annotation.JsonAnySetter; +import com.fasterxml.jackson.annotation.JsonIgnore; import com.fasterxml.jackson.annotation.JsonInclude; import com.fasterxml.jackson.annotation.JsonPropertyOrder; import com.fasterxml.jackson.databind.PropertyNamingStrategies; @@ -32,6 +34,10 @@ import cwms.cda.formatters.Formats; import cwms.cda.formatters.annotations.FormattableWith; import cwms.cda.formatters.json.JsonV1; +import java.util.ArrayList; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; @FormattableWith(contentType = Formats.JSONV1, formatter = JsonV1.class, aliases = {Formats.DEFAULT, Formats.JSON}) @JsonDeserialize(builder = CwmsId.Builder.class) @@ -41,10 +47,15 @@ public final class CwmsId extends CwmsDTO { private final String name; + private final Map properties; + @JsonIgnore + private final List requiredProperties; public CwmsId(Builder builder) { super(builder.officeId); this.name = builder.name; + this.properties = builder.properties; + this.requiredProperties = builder.requiredProperties; } public static CwmsId buildCwmsId(String officeId, String name) { @@ -59,16 +70,49 @@ protected void validateInternal(CwmsDTOValidator validator) { super.validateInternal(validator); validator.required(getOfficeId(), "office-id"); validator.required(getName(), "name"); + for(String requiredProperty : requiredProperties) { + validator.required(properties.get(requiredProperty), requiredProperty); + } } public String getName() { return name; } + public Map getProperties() { + return properties; + } + + @JsonIgnore + public String getProperty(String key) { + return properties.get(key); + } + public static class Builder { + private final Map properties = new LinkedHashMap<>(); + @JsonIgnore + private final List requiredProperties = new ArrayList<>(); private String officeId; private String name; + public Builder withProperties(Map properties) { + this.properties.putAll(properties); + return this; + } + + @JsonIgnore + final Builder withRequiredProperty(String key, String value) { + this.properties.put(key, value); + this.requiredProperties.add(key); + return this; + } + + @JsonAnySetter + public Builder withProperty(String key, String value) { + this.properties.put(key, value); + return this; + } + public Builder withOfficeId(String officeId) { this.officeId = officeId; return this; @@ -79,6 +123,18 @@ public Builder withName(String name) { return this; } + // ------ Known properties can have their builder methods added here to add to properties map------ // + public Builder withKind(String kind) { + this.properties.put("kind", kind); + return this; + } + + public Builder withBoundingOfficeId(String boundingOfficeId) { + this.properties.put("bounding-office-id", boundingOfficeId); + return this; + } + // ------ end of known property builders ------ // + public CwmsId build() { return new CwmsId(this); } diff --git a/cwms-data-api/src/main/java/cwms/cda/data/dto/TypedTimeSeriesIdentifiers.java b/cwms-data-api/src/main/java/cwms/cda/data/dto/TimeSeriesIdentifiersByType.java similarity index 72% rename from cwms-data-api/src/main/java/cwms/cda/data/dto/TypedTimeSeriesIdentifiers.java rename to cwms-data-api/src/main/java/cwms/cda/data/dto/TimeSeriesIdentifiersByType.java index 00a45c0bc..a9b43129d 100644 --- a/cwms-data-api/src/main/java/cwms/cda/data/dto/TypedTimeSeriesIdentifiers.java +++ b/cwms-data-api/src/main/java/cwms/cda/data/dto/TimeSeriesIdentifiersByType.java @@ -37,50 +37,50 @@ import java.util.Map; @FormattableWith(contentType = Formats.JSONV1, formatter = JsonV1.class, aliases = {Formats.DEFAULT, Formats.JSON}) -@JsonDeserialize(builder = TypedTimeSeriesIdentifiers.Builder.class) +@JsonDeserialize(builder = TimeSeriesIdentifiersByType.Builder.class) @JsonInclude(JsonInclude.Include.NON_NULL) @JsonNaming(PropertyNamingStrategies.KebabCaseStrategy.class) -public final class TypedTimeSeriesIdentifiers extends CwmsDTOBase { +public final class TimeSeriesIdentifiersByType extends CwmsDTOBase { @JsonProperty(required = true) private final CwmsId locationId; - private final Map typeToTsIdMap; + private final Map timeSeriesIds; - private TypedTimeSeriesIdentifiers(Builder builder) { + private TimeSeriesIdentifiersByType(Builder builder) { this.locationId = builder.locationId; - this.typeToTsIdMap = builder.typeToTsIdMap; + this.timeSeriesIds = builder.timeSeriesIds; } public CwmsId getLocationId() { return locationId; } - public Map getTypeToTsIdMap() { - return typeToTsIdMap; + public Map getTimeSeriesIds() { + return timeSeriesIds; } public static class Builder { private CwmsId locationId; - private Map typeToTsIdMap = new HashMap<>(); + private Map timeSeriesIds = new HashMap<>(); public Builder withLocationId(CwmsId locationId) { this.locationId = locationId; return this; } - public Builder withTypeToTsIdMap(Map typeToTsIdMap) { - this.typeToTsIdMap = typeToTsIdMap; + public Builder withTimeSeriesIds(Map timeSeriesIds) { + this.timeSeriesIds = timeSeriesIds; return this; } @JsonIgnore - public Builder withTypeToTsId(String type, TimeSeriesIdentifierDescriptor tsId) { - this.typeToTsIdMap.put(type, tsId); + public Builder withTimeSeriesId(String type, TimeSeriesMetaData tsId) { + this.timeSeriesIds.put(type, tsId); return this; } - public TypedTimeSeriesIdentifiers build() { - return new TypedTimeSeriesIdentifiers(this); + public TimeSeriesIdentifiersByType build() { + return new TimeSeriesIdentifiersByType(this); } } } diff --git a/cwms-data-api/src/main/java/cwms/cda/data/dto/TypedTimeSeriesIdentifiersList.java b/cwms-data-api/src/main/java/cwms/cda/data/dto/TimeSeriesIdentifiersByTypeList.java similarity index 60% rename from cwms-data-api/src/main/java/cwms/cda/data/dto/TypedTimeSeriesIdentifiersList.java rename to cwms-data-api/src/main/java/cwms/cda/data/dto/TimeSeriesIdentifiersByTypeList.java index 7dd9f0994..e61bd86d4 100644 --- a/cwms-data-api/src/main/java/cwms/cda/data/dto/TypedTimeSeriesIdentifiersList.java +++ b/cwms-data-api/src/main/java/cwms/cda/data/dto/TimeSeriesIdentifiersByTypeList.java @@ -36,17 +36,17 @@ import java.util.Collections; import java.util.List; -@JsonDeserialize(builder = TypedTimeSeriesIdentifiersList.Builder.class) +@JsonDeserialize(builder = TimeSeriesIdentifiersByTypeList.Builder.class) @JsonInclude(JsonInclude.Include.NON_NULL) @JsonNaming(PropertyNamingStrategies.KebabCaseStrategy.class) @FormattableWith(contentType = Formats.JSONV1, formatter = JsonV1.class, aliases = {Formats.DEFAULT, Formats.JSON}) -public class TypedTimeSeriesIdentifiersList extends CwmsDTOPaginated { +public class TimeSeriesIdentifiersByTypeList extends CwmsDTOPaginated { - private final List typedTimeSeriesIdentifiers; + private final List timeSeriesIdsForLocations; - private TypedTimeSeriesIdentifiersList(String cursor, int pageSize, Integer total, List identifiersList) { - super(cursor, pageSize, total); - this.typedTimeSeriesIdentifiers = new ArrayList<>(identifiersList); + private TimeSeriesIdentifiersByTypeList(Builder builder) { + super(builder.cursor, builder.pageSize, builder.total); + this.timeSeriesIdsForLocations = new ArrayList<>(builder.timeSeriesIdsForLocations); } public static String getOffice(String cursor) @@ -72,35 +72,50 @@ public static String getId(String cursor) { return null; } - public List getTypedTimeSeriesIdentifiers() { - return Collections.unmodifiableList(typedTimeSeriesIdentifiers); + public List getTimeSeriesIdsForLocations() { + return Collections.unmodifiableList(timeSeriesIdsForLocations); } public static class Builder { - private final String cursor; - private final int pageSize; - private final Integer total; + private String cursor; + private int pageSize; + private Integer total; + private String nextPage; - private List typedTimeSeriesIdentifiers = new ArrayList<>(); + private List timeSeriesIdsForLocations = new ArrayList<>(); - public Builder(String cursor, int pageSize, Integer total) { + public Builder withCursor(String cursor) { this.cursor = cursor; + return this; + } + + public Builder withPageSize(int pageSize) { this.pageSize = pageSize; + return this; + } + + public Builder withTotal(Integer total) { this.total = total; + return this; + } + + public Builder withNextPage(String nextPage) { + this.nextPage = nextPage; + return this; } - public Builder withTypedTimeSeriesIdentifiers(Collection identifiersList) { - this.typedTimeSeriesIdentifiers = new ArrayList<>(identifiersList); + public Builder withTimeSeriesIdsForLocations(Collection timeSeriesIdsForLocations) { + this.timeSeriesIdsForLocations = new ArrayList<>(timeSeriesIdsForLocations); return this; } - public TypedTimeSeriesIdentifiersList build() { - TypedTimeSeriesIdentifiersList retval = new TypedTimeSeriesIdentifiersList(cursor, pageSize, total, typedTimeSeriesIdentifiers); + public TimeSeriesIdentifiersByTypeList build() { + TimeSeriesIdentifiersByTypeList retval = new TimeSeriesIdentifiersByTypeList(this); - if (typedTimeSeriesIdentifiers.size() == pageSize && !typedTimeSeriesIdentifiers.isEmpty()) { - TypedTimeSeriesIdentifiers lastTypedTimeSeriesIdentifiers = typedTimeSeriesIdentifiers.get(typedTimeSeriesIdentifiers.size() - 1); - String cursor = encodeCursor(CwmsDTOPaginated.delimiter, lastTypedTimeSeriesIdentifiers.getLocationId().getOfficeId(), - lastTypedTimeSeriesIdentifiers.getLocationId().getName()); + if (timeSeriesIdsForLocations.size() == pageSize && !timeSeriesIdsForLocations.isEmpty()) { + TimeSeriesIdentifiersByType lastTimeSeriesIdentifiersByType = timeSeriesIdsForLocations.get(timeSeriesIdsForLocations.size() - 1); + String cursor = encodeCursor(CwmsDTOPaginated.delimiter, lastTimeSeriesIdentifiersByType.getLocationId().getOfficeId(), + lastTimeSeriesIdentifiersByType.getLocationId().getName()); retval.nextPage = encodeCursor(cursor, pageSize, total); } else { retval.nextPage = null; diff --git a/cwms-data-api/src/main/java/cwms/cda/data/dto/TimeSeriesMetaData.java b/cwms-data-api/src/main/java/cwms/cda/data/dto/TimeSeriesMetaData.java new file mode 100644 index 000000000..64221eaa3 --- /dev/null +++ b/cwms-data-api/src/main/java/cwms/cda/data/dto/TimeSeriesMetaData.java @@ -0,0 +1,66 @@ +package cwms.cda.data.dto; + +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.annotation.JsonProperty; +import com.fasterxml.jackson.databind.PropertyNamingStrategies; +import com.fasterxml.jackson.databind.annotation.JsonDeserialize; +import com.fasterxml.jackson.databind.annotation.JsonNaming; +import cwms.cda.formatters.Formats; +import cwms.cda.formatters.annotations.FormattableWith; +import cwms.cda.formatters.json.JsonV1; +import java.time.Instant; + +@FormattableWith(contentType = Formats.JSONV1, formatter = JsonV1.class, aliases = {Formats.DEFAULT, Formats.JSON}) +@JsonDeserialize(builder = TimeSeriesMetaData.Builder.class) +@JsonInclude(JsonInclude.Include.NON_NULL) +@JsonNaming(PropertyNamingStrategies.KebabCaseStrategy.class) +public final class TimeSeriesMetaData { + @JsonProperty(required = true) + private final TimeSeriesIdentifierDescriptor tsId; + private final Instant dateRefreshed; + private final String notes; + + private TimeSeriesMetaData(Builder builder) { + this.tsId = builder.tsId; + this.dateRefreshed = builder.dateRefreshed; + this.notes = builder.notes; + } + + public TimeSeriesIdentifierDescriptor getTsId() { + return tsId; + } + + public Instant getDateRefreshed() { + return dateRefreshed; + } + + public String getNotes() { + return notes; + } + + public static class Builder { + private TimeSeriesIdentifierDescriptor tsId; + private Instant dateRefreshed; + private String notes; + + public Builder withTsId(TimeSeriesIdentifierDescriptor tsId) { + this.tsId = tsId; + return this; + } + + public Builder withDateRefreshed(Instant dateRefreshed) { + this.dateRefreshed = dateRefreshed; + return this; + } + + public Builder withNotes(String notes) { + this.notes = notes; + return this; + } + + public TimeSeriesMetaData build() { + return new TimeSeriesMetaData(this); + } + } + +} diff --git a/cwms-data-api/src/test/java/cwms/cda/api/AccessToWaterControllerTestIT.java b/cwms-data-api/src/test/java/cwms/cda/api/AccessToWaterControllerTestIT.java new file mode 100644 index 000000000..b190c96dc --- /dev/null +++ b/cwms-data-api/src/test/java/cwms/cda/api/AccessToWaterControllerTestIT.java @@ -0,0 +1,293 @@ +package cwms.cda.api; + +import static cwms.cda.api.Controllers.PAGE; +import static cwms.cda.api.Controllers.PAGE_SIZE; +import static cwms.cda.data.dao.DaoTest.getDslContext; +import cwms.cda.data.dao.TimeSeriesDao; +import cwms.cda.data.dao.TimeSeriesDaoImpl; +import cwms.cda.data.dao.TimeSeriesDeleteOptions; +import cwms.cda.data.dto.TimeSeries; +import cwms.cda.formatters.Formats; +import static cwms.cda.security.KeyAccessManager.AUTH_HEADER; +import fixtures.CwmsDataApiSetupCallback; +import fixtures.TestAccounts; +import hec.heclib.dss.DSSPathname; +import static io.restassured.RestAssured.given; +import io.restassured.filter.log.LogDetail; +import io.restassured.response.ExtractableResponse; +import io.restassured.response.Response; +import java.sql.CallableStatement; +import java.sql.SQLException; +import java.sql.Timestamp; +import java.sql.Types; +import java.time.Duration; +import java.time.ZonedDateTime; +import java.util.ArrayList; +import java.util.Date; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; +import javax.servlet.http.HttpServletResponse; +import mil.army.usace.hec.test.database.CwmsDatabaseContainer; +import static org.hamcrest.Matchers.equalTo; +import static org.hamcrest.Matchers.greaterThan; +import static org.hamcrest.Matchers.is; +import static org.hamcrest.Matchers.notNullValue; +import org.jooq.DSLContext; +import org.junit.jupiter.api.AfterAll; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.Disabled; +import org.junit.jupiter.api.Tag; +import org.junit.jupiter.api.Test; +import static usace.cwms.db.jooq.codegen.tables.AV_CWMS_TS_ID.AV_CWMS_TS_ID; + +@Tag("integration") +@Disabled +public final class AccessToWaterControllerTestIT extends DataApiTestIT { + + private static final TestAccounts.KeyUser user = TestAccounts.KeyUser.SPK_NORMAL; + private static final String OFFICE_ID = "SPK"; + private static final ZonedDateTime start = ZonedDateTime.parse("2021-06-21T08:00:00-07:00[PST8PDT]"); + private static final ZonedDateTime end = ZonedDateTime.parse("2021-06-21T09:00:00-07:00[PST8PDT]"); + private static final Map> LOCATION_TO_TS_ID = new LinkedHashMap<>(); + private static final String STAGE = "STAGE"; + private static final String FLOW = "FLOW"; + private static final Map TS_CODE_MAP = new LinkedHashMap<>(); + + @BeforeAll + public static void beforeAll() throws Exception { + Map aarkParamToTsIdMap = new LinkedHashMap<>(); + aarkParamToTsIdMap.put(STAGE, "AARK.Stage.Inst.15Minutes.0.Ccp-Rev"); + aarkParamToTsIdMap.put(FLOW, "AARK.Flow.Inst.1Hour.0.Ccp-Rev"); + Map addiParamToTsIdMap = new LinkedHashMap<>(); + addiParamToTsIdMap.put(STAGE, "ADDI.Stage.Inst.15Minutes.0.Ccp-Rev"); + LOCATION_TO_TS_ID.put("AARK", aarkParamToTsIdMap); + LOCATION_TO_TS_ID.put("ADDI", addiParamToTsIdMap); + List tsToCreate = new ArrayList<>(); + for(String locationId : LOCATION_TO_TS_ID.keySet()) + { + try { + createLocation(locationId, true, OFFICE_ID, "SITE"); + createLocation(locationId, true, OFFICE_ID, "SITE"); + } catch (Exception e) { + throw new RuntimeException(e); + } + Map tsIds = LOCATION_TO_TS_ID.get(locationId); + for(String tsId : tsIds.values()) + { + DSSPathname path = new DSSPathname(tsId); + int minutes = 15; + if("1Hour".equals(path.getDPart())) + { + minutes = 60; + } + int count = 60 / minutes; + TimeSeries ts = new TimeSeries(null, -1, 0, tsId, OFFICE_ID, start, end, "m", Duration.ofMinutes(minutes)); + + ZonedDateTime next = start; + for(int i = 0; i < count; i++) + { + Timestamp dateTime = Timestamp.valueOf(next.toLocalDateTime()); + ts.addValue(dateTime, (double) i, 0); + next = next.plusMinutes(minutes); + } + tsToCreate.add(ts); + } + } + + CwmsDatabaseContainer databaseLink = CwmsDataApiSetupCallback.getDatabaseLink(); + databaseLink.connection(c -> { + DSLContext context = getDslContext(c, databaseLink.getOfficeId()); + TimeSeriesDao timeSeriesDao = new TimeSeriesDaoImpl(context); + for(TimeSeries ts : tsToCreate) + { + timeSeriesDao.create(ts); + } + + // Retrieve ts_code for each TS ID + for (String locationId : LOCATION_TO_TS_ID.keySet()) { + Map tsIds = LOCATION_TO_TS_ID.get(locationId); + for (String tsId : tsIds.values()) { + Integer tsCode = context.select(AV_CWMS_TS_ID.TS_CODE) + .from(AV_CWMS_TS_ID) + .where(AV_CWMS_TS_ID.CWMS_TS_ID.eq(tsId)) + .fetchOneInto(Integer.class); + + if (tsCode != null) { + TS_CODE_MAP.put(tsId, tsCode); + } else { + throw new RuntimeException("TS Code not found for " + tsId); + } + } + } + + // Call a2w store procedure + for (String locationId : LOCATION_TO_TS_ID.keySet()) { + Map tsIds = LOCATION_TO_TS_ID.get(locationId); + context.connection(conn -> { + try (CallableStatement cs = conn.prepareCall("{ call cwms_cma_pkg.p_load_a2w_by_location(?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?) }")) { + cs.setString(1, OFFICE_ID); + cs.setString(2, locationId); + cs.setString(3, "Y"); // Example display flag + cs.setString(4, "Generated data"); // Example notes + cs.setInt(5, tsIds.values().size()); + cs.setObject(6, tsIds.get(STAGE) == null ? null : TS_CODE_MAP.get(tsIds.get(STAGE))); + cs.setObject(7, tsIds.get(FLOW) == null ? null : TS_CODE_MAP.get(tsIds.get(FLOW))); + cs.setObject(8, null); + cs.setObject(9, null); + cs.setObject(10, null); + cs.setObject(11, null); + cs.setObject(12, null); + cs.setObject(13, null); + cs.setObject(14, null); + cs.setObject(15, null); + cs.setObject(16, null); + cs.setObject(17, null); + cs.setObject(18, null); + cs.setObject(19, null); + cs.setObject(20, null); + cs.setObject(21, null); + cs.setObject(22, null); + cs.setObject(23, null); + cs.setObject(24, null); + cs.setObject(25, null); + cs.setObject(26, null); + cs.setObject(27, null); + cs.setObject(28, null); + cs.setObject(29, null); + cs.setObject(30, null); + cs.setObject(31, null); + cs.setObject(32, null); + cs.setObject(33, null); + cs.registerOutParameter(34, Types.VARCHAR); // p_error_msg as OUT parameter + cs.execute(); + } + }); + } + }, "CWMS_20"); + } + + @AfterAll + public static void afterAll() throws SQLException { + CwmsDatabaseContainer databaseLink = CwmsDataApiSetupCallback.getDatabaseLink(); + databaseLink.connection(c -> { + DSLContext context = getDslContext(c, databaseLink.getOfficeId()); + for (Object tsCode : TS_CODE_MAP.values()) { + context.connection(conn -> { + try (CallableStatement cs = conn.prepareCall("{ call cwms_cma_pkg.p_clear_a2w_ts_code(?) }")) { + cs.setObject(1, tsCode); + cs.execute(); + } + }); + } + TimeSeriesDeleteOptions options = new TimeSeriesDaoImpl.DeleteOptions.Builder() + .withStartTime(Date.from(start.toInstant())).withEndTime(Date.from(end.toInstant())) + .withEndTimeInclusive(true).withStartTimeInclusive(true).withMaxVersion(true) + .withVersionDate(Date.from(start.toInstant())).withOverrideProtection("T").build(); + TimeSeriesDao timeSeriesDao = new TimeSeriesDaoImpl(context); + LOCATION_TO_TS_ID.values().forEach(tsIds -> + tsIds.values().forEach(tsId -> + timeSeriesDao.delete(OFFICE_ID, tsId, options))); + }, CwmsDataApiSetupCallback.getWebUser()); + } + + @Test + void testGetAllNotPaginated() + { + // Create test parameters (mock the filters for officeId, locationId, and tsType) + String officeMask = "SPK"; // Example office ID + String locationMask = "AARK||ADDI"; // Example location IDs + String tsType = "Stage||Flow"; // Example time series types + + // Send a request to the endpoint to retrieve all time series profiles without pagination + given() + .log().ifValidationFails(LogDetail.ALL, true) + .accept(Formats.JSONV1) + .contentType(Formats.JSONV1) + .header(AUTH_HEADER, user.toHeaderValue()) + .queryParam("office-mask", officeMask) + .queryParam("location-mask", locationMask) + .queryParam("ts-type", tsType) + .when() + .redirects().follow(true) + .redirects().max(3) + .get("/access-to-water/") + .then() + .log().ifValidationFails(LogDetail.ALL, true) + .assertThat() + .statusCode(is(HttpServletResponse.SC_OK)) + .body("result.size()", greaterThan(0)) // Ensure there is some data returned + .body("result[0].tsId", notNullValue()) // Check that each time series has a tsId + .body("result[0].locationId", equalTo("AARK")) // Example check for locationId + .body("result[0].tsType", equalTo("Stage")); // Example check for time series type + } + + @Test + void testGetAllPaginated() + { + // Create test parameters (mock the filters for officeId, locationId, and tsType) + String locationMask = "AARK||ADDI"; // Example location IDs + String tsType = "Stage||Flow"; // Example time series types + + // Send a request to the endpoint to retrieve all time series profiles without pagination + given() + .log().ifValidationFails(LogDetail.ALL, true) + .accept(Formats.JSONV1) + .contentType(Formats.JSONV1) + .header(AUTH_HEADER, user.toHeaderValue()) + .queryParam("office-mask", OFFICE_ID) + .queryParam("location-mask", locationMask) + .queryParam("ts-type", tsType) + .when() + .redirects().follow(true) + .redirects().max(3) + .get("/access-to-water/") // Endpoint for time series retrieval + .then() + .log().ifValidationFails(LogDetail.ALL, true) + .assertThat() + .statusCode(is(HttpServletResponse.SC_OK)) + .body("result.size()", greaterThan(0)) // Ensure there is some data returned + .body("result[0].tsId", notNullValue()) // Check that each time series has a tsId + .body("result[0].locationId", equalTo("AARK")) // Example check for locationId + .body("result[0].tsType", equalTo("Stage")); // Example check for time series type + + // Retrieve with pagination, page 1 + ExtractableResponse response = given() + .log().ifValidationFails(LogDetail.ALL, true) + .accept(Formats.JSONV1) + .contentType(Formats.JSONV1) + .header(AUTH_HEADER, user.toHeaderValue()) + .queryParam(PAGE_SIZE, 1) + .when() + .redirects().follow(true) + .redirects().max(3) + .get("/access-to-water/") + .then() + .log().ifValidationFails(LogDetail.ALL, true) + .assertThat() + .statusCode(is(HttpServletResponse.SC_OK)) + .body("typed-time-series-identifiers.size()", is(1)) + .body("next-page", is(notNullValue())).extract(); + + String nextPageCursor = response.path("next-page"); + assert nextPageCursor != null; + + // Retrieve with pagination, page 2 + given() + .log().ifValidationFails(LogDetail.ALL, true) + .accept(Formats.JSONV1) + .contentType(Formats.JSONV1) + .header(AUTH_HEADER, user.toHeaderValue()) + .queryParam(PAGE_SIZE, 1) + .queryParam(PAGE, nextPageCursor) + .when() + .redirects().follow(true) + .redirects().max(3) + .get("/access-to-water/") + .then() + .log().ifValidationFails(LogDetail.ALL, true) + .assertThat() + .statusCode(is(HttpServletResponse.SC_OK)) + .body("typed-time-series-identifiers.size()", is(1)); + } +} diff --git a/cwms-data-api/src/test/java/cwms/cda/data/dao/AccessToWaterTimeSeriesDaoTest.java b/cwms-data-api/src/test/java/cwms/cda/data/dao/AccessToWaterTimeSeriesDaoTest.java new file mode 100644 index 000000000..4aa9d4ffc --- /dev/null +++ b/cwms-data-api/src/test/java/cwms/cda/data/dao/AccessToWaterTimeSeriesDaoTest.java @@ -0,0 +1,210 @@ +/* + * MIT License + * + * Copyright (c) 2025 Hydrologic Engineering Center + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ +package cwms.cda.data.dao; + +import cwms.cda.api.DataApiTestIT; +import static cwms.cda.data.dao.DaoTest.getDslContext; +import cwms.cda.data.dto.TimeSeries; +import fixtures.CwmsDataApiSetupCallback; +import hec.heclib.dss.DSSPathname; +import java.sql.CallableStatement; +import java.sql.SQLException; +import java.sql.Timestamp; +import java.sql.Types; +import java.time.Duration; +import java.time.ZonedDateTime; +import java.util.ArrayList; +import java.util.Date; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; +import mil.army.usace.hec.test.database.CwmsDatabaseContainer; +import org.jooq.DSLContext; +import org.junit.jupiter.api.AfterAll; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.Disabled; +import org.junit.jupiter.api.Tag; +import org.junit.jupiter.api.Test; +import static usace.cwms.db.jooq.codegen.tables.AV_CWMS_TS_ID.AV_CWMS_TS_ID; + +@Tag("integration") +@Disabled +public final class AccessToWaterTimeSeriesDaoTest extends DataApiTestIT { + private static final String OFFICE_ID = "SPK"; + private static final ZonedDateTime start = ZonedDateTime.parse("2021-06-21T08:00:00-07:00[PST8PDT]"); + private static final ZonedDateTime end = ZonedDateTime.parse("2021-06-21T09:00:00-07:00[PST8PDT]"); + private static final Map> LOCATION_TO_TS_ID = new LinkedHashMap<>(); + private static final String STAGE = "STAGE"; + private static final String FLOW = "FLOW"; + private static final Map TS_CODE_MAP = new LinkedHashMap<>(); + + @BeforeAll + public static void beforeAll() throws Exception { + Map aarkParamToTsIdMap = new LinkedHashMap<>(); + aarkParamToTsIdMap.put(STAGE, "AARK.Stage.Inst.15Minutes.0.Ccp-Rev"); + aarkParamToTsIdMap.put(FLOW, "AARK.Flow.Inst.1Hour.0.Ccp-Rev"); + Map addiParamToTsIdMap = new LinkedHashMap<>(); + addiParamToTsIdMap.put(STAGE, "ADDI.Stage.Inst.15Minutes.0.Ccp-Rev"); + LOCATION_TO_TS_ID.put("AARK", aarkParamToTsIdMap); + LOCATION_TO_TS_ID.put("ADDI", addiParamToTsIdMap); + List tsToCreate = new ArrayList<>(); + for(String locationId : LOCATION_TO_TS_ID.keySet()) + { + try { + createLocation(locationId, true, OFFICE_ID, "SITE"); + createLocation(locationId, true, OFFICE_ID, "SITE"); + } catch (Exception e) { + throw new RuntimeException(e); + } + Map tsIds = LOCATION_TO_TS_ID.get(locationId); + for(String tsId : tsIds.values()) + { + DSSPathname path = new DSSPathname(tsId); + int minutes = 15; + if("1Hour".equals(path.getDPart())) + { + minutes = 60; + } + int count = 60 / minutes; + TimeSeries ts = new TimeSeries(null, -1, 0, tsId, OFFICE_ID, start, end, "m", Duration.ofMinutes(minutes)); + + ZonedDateTime next = start; + for(int i = 0; i < count; i++) + { + Timestamp dateTime = Timestamp.valueOf(next.toLocalDateTime()); + ts.addValue(dateTime, (double) i, 0); + next = next.plusMinutes(minutes); + } + tsToCreate.add(ts); + } + } + + CwmsDatabaseContainer databaseLink = CwmsDataApiSetupCallback.getDatabaseLink(); + databaseLink.connection(c -> { + DSLContext context = getDslContext(c, databaseLink.getOfficeId()); + TimeSeriesDao timeSeriesDao = new TimeSeriesDaoImpl(context); + for(TimeSeries ts : tsToCreate) + { + timeSeriesDao.create(ts); + } + + // Retrieve ts_code for each TS ID + for (String locationId : LOCATION_TO_TS_ID.keySet()) { + Map tsIds = LOCATION_TO_TS_ID.get(locationId); + for (String tsId : tsIds.values()) { + Integer tsCode = context.select(AV_CWMS_TS_ID.TS_CODE) + .from(AV_CWMS_TS_ID) + .where(AV_CWMS_TS_ID.CWMS_TS_ID.eq(tsId)) + .fetchOneInto(Integer.class); + + if (tsCode != null) { + TS_CODE_MAP.put(tsId, tsCode); + } else { + throw new RuntimeException("TS Code not found for " + tsId); + } + } + } + }, CwmsDataApiSetupCallback.getWebUser()); + + databaseLink.connection(c -> { + DSLContext context = getDslContext(c, databaseLink.getOfficeId()); + // Call a2w store procedure + for (String locationId : LOCATION_TO_TS_ID.keySet()) { + Map tsIds = LOCATION_TO_TS_ID.get(locationId); + context.connection(conn -> { + try (CallableStatement cs = conn.prepareCall("{ call cwms_cma_pkg.p_load_a2w_by_location(?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?) }")) { + cs.setString(1, OFFICE_ID); + cs.setString(2, locationId); + cs.setString(3, "Y"); // Example display flag + cs.setString(4, "Generated data"); // Example notes + cs.setInt(5, tsIds.values().size()); + cs.setObject(6, tsIds.get(STAGE) == null ? null : TS_CODE_MAP.get(tsIds.get(STAGE))); + cs.setObject(7, tsIds.get(FLOW) == null ? null : TS_CODE_MAP.get(tsIds.get(FLOW))); + cs.setObject(8, null); + cs.setObject(9, null); + cs.setObject(10, null); + cs.setObject(11, null); + cs.setObject(12, null); + cs.setObject(13, null); + cs.setObject(14, null); + cs.setObject(15, null); + cs.setObject(16, null); + cs.setObject(17, null); + cs.setObject(18, null); + cs.setObject(19, null); + cs.setObject(20, null); + cs.setObject(21, null); + cs.setObject(22, null); + cs.setObject(23, null); + cs.setObject(24, null); + cs.setObject(25, null); + cs.setObject(26, null); + cs.setObject(27, null); + cs.setObject(28, null); + cs.setObject(29, null); + cs.setObject(30, null); + cs.setObject(31, null); + cs.setObject(32, null); + cs.setObject(33, null); + cs.registerOutParameter(34, Types.VARCHAR); // p_error_msg as OUT parameter + + cs.execute(); + } + }); + } + }, "CWMS_20"); + } + + @AfterAll + public static void afterAll() throws SQLException { + CwmsDatabaseContainer databaseLink = CwmsDataApiSetupCallback.getDatabaseLink(); + databaseLink.connection(c -> { + DSLContext context = getDslContext(c, databaseLink.getOfficeId()); + for (Object tsCode : TS_CODE_MAP.values()) { + context.connection(conn -> { + try (CallableStatement cs = conn.prepareCall("{ call cwms_cma_pkg.p_clear_a2w_ts_code(?) }")) { + cs.setObject(1, tsCode); + cs.execute(); + } + }); + } + TimeSeriesDeleteOptions options = new TimeSeriesDaoImpl.DeleteOptions.Builder() + .withStartTime(Date.from(start.toInstant())).withEndTime(Date.from(end.toInstant())) + .withEndTimeInclusive(true).withStartTimeInclusive(true).withMaxVersion(true) + .withVersionDate(Date.from(start.toInstant())).withOverrideProtection("T").build(); + TimeSeriesDao timeSeriesDao = new TimeSeriesDaoImpl(context); + LOCATION_TO_TS_ID.values().forEach(tsIds -> + tsIds.values().forEach(tsId -> + timeSeriesDao.delete(OFFICE_ID, tsId, options))); + }, CwmsDataApiSetupCallback.getWebUser()); + } + + @Test + public void testRetrieval() throws Exception + { + System.out.println("TESTING A2W"); + } + + +} diff --git a/cwms-data-api/src/test/java/cwms/cda/data/dto/TimeSeriesIdentifiersByTypeTest.java b/cwms-data-api/src/test/java/cwms/cda/data/dto/TimeSeriesIdentifiersByTypeTest.java new file mode 100644 index 000000000..547c71d3c --- /dev/null +++ b/cwms-data-api/src/test/java/cwms/cda/data/dto/TimeSeriesIdentifiersByTypeTest.java @@ -0,0 +1,299 @@ +/* + * MIT License + * + * Copyright (c) 2025 Hydrologic Engineering Center + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES, OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ + +package cwms.cda.data.dto; + +import java.time.ZoneId; +import java.time.Instant; +import java.util.Arrays; +import java.util.HashMap; +import java.util.Map; +import static org.junit.jupiter.api.Assertions.*; + +import cwms.cda.api.errors.FieldException; +import cwms.cda.formatters.ContentType; +import cwms.cda.formatters.Formats; +import cwms.cda.helpers.DTOMatch; +import java.io.InputStream; +import java.nio.charset.StandardCharsets; +import org.apache.commons.io.IOUtils; +import org.junit.jupiter.api.Test; + +final class TimeSeriesIdentifiersByTypeTest { + + @Test + void createAccessToWaterTimeSeriesData_allFieldsProvided_success() { + TimeSeriesMetaData ts1 = new TimeSeriesMetaData.Builder() + .withTsId(new TimeSeriesIdentifierDescriptor.Builder() + .withTimeSeriesId("AARK.Stage.Inst.15Minutes.0.Ccp-Rev") + .withOfficeId("SWT") + .withActive(true) + .withIntervalOffsetMinutes(0L) + .withZoneId(ZoneId.of("UTC")) + .build()) + .withNotes("This is a test") + .withDateRefreshed(Instant.parse("2025-03-03T12:00:00Z")) // Adding date refreshed + .build(); + + TimeSeriesMetaData ts2 = new TimeSeriesMetaData.Builder() + .withTsId(new TimeSeriesIdentifierDescriptor.Builder() + .withTimeSeriesId("AARK.Flow.Inst.15Minutes.0.Ccp-Rev") + .withOfficeId("SWT") + .withActive(true) + .withIntervalOffsetMinutes(0L) + .withZoneId(ZoneId.of("UTC")) + .build()) + .withNotes("This is another test") + .withDateRefreshed(Instant.parse("2025-03-03T12:00:00Z")) // Adding date refreshed + .build(); + + CwmsId locId = new CwmsId.Builder() + .withName("AARK") + .withOfficeId("SWT") + .withBoundingOfficeId("SWT") + .withKind("SITE") + .build(); + TimeSeriesIdentifiersByType items = new TimeSeriesIdentifiersByType.Builder() + .withLocationId(locId) + .withTimeSeriesId("STAGE", ts1) + .withTimeSeriesId("OUTFLOW", ts2) + .build(); + + assertAll( + () -> DTOMatch.assertMatch(locId, items.getLocationId()), + () -> DTOMatch.assertMatch(ts1, items.getTimeSeriesIds().get("STAGE")), + () -> DTOMatch.assertMatch(ts2, items.getTimeSeriesIds().get("OUTFLOW")) + ); + } + + @Test + void createAccessToWaterTimeSeriesData_missingField_throwsFieldException() { + TimeSeriesMetaData tsDescriptor = new TimeSeriesMetaData.Builder() + .withTsId(new TimeSeriesIdentifierDescriptor.Builder() + .withTimeSeriesId("AARK.Stage.Inst.15Minutes.0.Ccp-Rev") + .withOfficeId("SWT") + .withActive(true) + .withIntervalOffsetMinutes(0L) + .withZoneId(ZoneId.of("UTC")) + .build()) + .withNotes("This is a test") + .withDateRefreshed(Instant.now()) // Adding date refreshed + .build(); + + Map typeToTsIdMap = new HashMap<>(); + typeToTsIdMap.put("STAGE", tsDescriptor); + + assertAll( + () -> assertThrows(FieldException.class, () -> { + TimeSeriesIdentifiersByType item = new TimeSeriesIdentifiersByType.Builder() + .withTimeSeriesIds(typeToTsIdMap) + .build(); + item.validate(); + }, "The validate method should have thrown a FieldException because the location ID is missing") + ); + } + + @Test + void createAccessToWaterTimeSeriesData_serialize_roundtrip() { + TimeSeriesMetaData tsDescriptor = new TimeSeriesMetaData.Builder() + .withTsId(new TimeSeriesIdentifierDescriptor.Builder() + .withTimeSeriesId("AARK.Stage.Inst.15Minutes.0.Ccp-Rev") + .withOfficeId("SWT") + .withActive(true) + .withIntervalOffsetMinutes(0L) + .withZoneId(ZoneId.of("UTC")) + .build()) + .withNotes("This is a test") + .withDateRefreshed(Instant.now()) // Adding date refreshed + .build(); + TimeSeriesIdentifiersByType data = new TimeSeriesIdentifiersByType.Builder() + .withLocationId(new CwmsId.Builder() + .withName("AARK") + .withOfficeId("SWT") + .withBoundingOfficeId("SWT") + .withKind("SITE") + .build()) + .withTimeSeriesId("STAGE", tsDescriptor) + .build(); + + ContentType contentType = new ContentType(Formats.JSON); + String json = Formats.format(contentType, data); + TimeSeriesIdentifiersByType deserialized = Formats.parseContent(contentType, json, TimeSeriesIdentifiersByType.class); + DTOMatch.assertMatch(data, deserialized); + } + + @Test + void createAccessToWaterTimeSeriesData_deserialize_multipleEntries() throws Exception { + // Expected data + TimeSeriesMetaData ts1Stage = new TimeSeriesMetaData.Builder() + .withTsId(new TimeSeriesIdentifierDescriptor.Builder() + .withTimeSeriesId("AARK.Stage.Inst.15Minutes.0.Ccp-Rev") + .withOfficeId("SWT") + .withActive(true) + .withIntervalOffsetMinutes(0L) + .withZoneId(ZoneId.of("UTC")) + .build()) + .withNotes("Stage data for AARK") + .withDateRefreshed(Instant.parse("2025-03-03T12:00:00Z")) + .build(); + + TimeSeriesMetaData ts2Outflow = new TimeSeriesMetaData.Builder() + .withTsId(new TimeSeriesIdentifierDescriptor.Builder() + .withTimeSeriesId("AARK.Flow.Inst.1Hour.0.Ccp-Rev") + .withOfficeId("SWT") + .withActive(true) + .withIntervalOffsetMinutes(0L) + .withZoneId(ZoneId.of("UTC")) + .build()) + .withNotes("Flow data for AARK") + .withDateRefreshed(Instant.parse("2025-03-03T12:00:00Z")) + .build(); + + TimeSeriesMetaData ts3Precip = new TimeSeriesMetaData.Builder() + .withTsId(new TimeSeriesIdentifierDescriptor.Builder() + .withTimeSeriesId("AUGU.Precip-Inc.Total.1Hour.1Hour.Ccp-Rev") + .withOfficeId("SWT") + .withActive(true) + .withIntervalOffsetMinutes(0L) + .withZoneId(ZoneId.of("UTC")) + .build()) + .withNotes("Precipitation data for AUGU") + .withDateRefreshed(Instant.parse("2025-03-03T12:00:00Z")) + .build(); + + TimeSeriesMetaData ts4Stage = new TimeSeriesMetaData.Builder() + .withTsId(new TimeSeriesIdentifierDescriptor.Builder() + .withTimeSeriesId("AUGU.Stage.Inst.1Hour.0.Ccp-Rev") + .withOfficeId("SWT") + .withActive(true) + .withIntervalOffsetMinutes(0L) + .withZoneId(ZoneId.of("UTC")) + .build()) + .withNotes("Stage data for AUGU") + .withDateRefreshed(Instant.parse("2025-03-03T12:00:00Z")) + .build(); + + TimeSeriesMetaData ts5Outflow = new TimeSeriesMetaData.Builder() + .withTsId(new TimeSeriesIdentifierDescriptor.Builder() + .withTimeSeriesId("AUGU.Flow.Inst.1Hour.0.Ccp-Rev") + .withOfficeId("SWT") + .withActive(true) + .withIntervalOffsetMinutes(0L) + .withZoneId(ZoneId.of("UTC")) + .build()) + .withNotes("Flow data for AUGU") + .withDateRefreshed(Instant.parse("2025-03-03T12:00:00Z")) + .build(); + + TimeSeriesMetaData ts6OutflowAlt = new TimeSeriesMetaData.Builder() + .withTsId(new TimeSeriesIdentifierDescriptor.Builder() + .withTimeSeriesId("ALTU.Flow-Res Out.Ave.1Hour.1Hour.Rev-Regi-Flowgroup") + .withOfficeId("SWT") + .withActive(true) + .withIntervalOffsetMinutes(0L) + .withZoneId(ZoneId.of("UTC")) + .build()) + .withNotes("Outflow data for ALTU") + .withDateRefreshed(Instant.parse("2025-03-03T12:00:00Z")) + .build(); + + TimeSeriesMetaData ts7FloodStorage = new TimeSeriesMetaData.Builder() + .withTsId(new TimeSeriesIdentifierDescriptor.Builder() + .withTimeSeriesId("ALTU.Stor-Flood Pool.Inst.1Hour.0.Ccp-Rev") + .withOfficeId("SWT") + .withActive(true) + .withIntervalOffsetMinutes(0L) + .withZoneId(ZoneId.of("UTC")) + .build()) + .withNotes("Flood storage data for ALTU") + .withDateRefreshed(Instant.parse("2025-03-03T12:00:00Z")) + .build(); + + TimeSeriesMetaData ts8ConservationStorage = new TimeSeriesMetaData.Builder() + .withTsId(new TimeSeriesIdentifierDescriptor.Builder() + .withTimeSeriesId("ALTU.Stor-Conservation Pool.Inst.1Hour.0.Ccp-Rev") + .withOfficeId("SWT") + .withActive(true) + .withIntervalOffsetMinutes(0L) + .withZoneId(ZoneId.of("UTC")) + .build()) + .withNotes("Conservation storage data for ALTU") + .withDateRefreshed(Instant.parse("2025-03-03T12:00:00Z")) + .build(); + + // Expected data for locations + TimeSeriesIdentifiersByType expectedAARK = new TimeSeriesIdentifiersByType.Builder() + .withLocationId(new CwmsId.Builder() + .withName("AARK") + .withOfficeId("SWT") + .withBoundingOfficeId("SWT") + .withKind("SITE") + .build()) + .withTimeSeriesId("STAGE", ts1Stage) + .withTimeSeriesId("OUTFLOW", ts2Outflow) + .build(); + + TimeSeriesIdentifiersByType expectedAUGU = new TimeSeriesIdentifiersByType.Builder() + .withLocationId(new CwmsId.Builder() + .withName("AUGU") + .withOfficeId("SWT") + .withBoundingOfficeId("SWT") + .withKind("SITE") + .build()) + .withTimeSeriesId("PRECIP", ts3Precip) + .withTimeSeriesId("STAGE", ts4Stage) + .withTimeSeriesId("OUTFLOW", ts5Outflow) + .build(); + + TimeSeriesIdentifiersByType expectedALTU = new TimeSeriesIdentifiersByType.Builder() + .withLocationId(new CwmsId.Builder() + .withName("ALTU") + .withOfficeId("SWT") + .withBoundingOfficeId("SWT") + .withKind("SITE") + .build()) + .withTimeSeriesId("OUTFLOW", ts6OutflowAlt) + .withTimeSeriesId("FLOOD STORAGE", ts7FloodStorage) + .withTimeSeriesId("CONSERVATION STORAGE", ts8ConservationStorage) + .build(); + + TimeSeriesIdentifiersByTypeList list = new TimeSeriesIdentifiersByTypeList.Builder() + .withCursor("cursor") + .withPageSize(20) + .withTotal(100) + .withTimeSeriesIdsForLocations(Arrays.asList(expectedAARK, expectedAUGU, expectedALTU)) + .build(); + + InputStream resource = this.getClass().getResourceAsStream("/cwms/cda/data/dto/time_series_identifiers_by_type.json"); + assertNotNull(resource); + String json = IOUtils.toString(resource, StandardCharsets.UTF_8); + + ContentType contentType = new ContentType(Formats.JSON); + TimeSeriesIdentifiersByTypeList deserialized = Formats.parseContent(contentType, json, TimeSeriesIdentifiersByTypeList.class); + + // Verify the deserialized data matches the expected data + assertAll(() -> DTOMatch.assertMatch(list, deserialized)); + } + +} diff --git a/cwms-data-api/src/test/java/cwms/cda/data/dto/TypedTimeSeriesIdentifierTest.java b/cwms-data-api/src/test/java/cwms/cda/data/dto/TypedTimeSeriesIdentifierTest.java deleted file mode 100644 index 4e7fb4028..000000000 --- a/cwms-data-api/src/test/java/cwms/cda/data/dto/TypedTimeSeriesIdentifierTest.java +++ /dev/null @@ -1,134 +0,0 @@ -/* - * MIT License - * - * Copyright (c) 2025 Hydrologic Engineering Center - * - * Permission is hereby granted, free of charge, to any person obtaining a copy - * of this software and associated documentation files (the "Software"), to deal - * in the Software without restriction, including without limitation the rights - * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell - * copies of the Software, and to permit persons to whom the Software is - * furnished to do so, subject to the following conditions: - * - * The above copyright notice and this permission notice shall be included in all - * copies or substantial portions of the Software. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE - * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES, OR OTHER - * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, - * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE - * SOFTWARE. - */ - -package cwms.cda.data.dto; - -import java.time.ZoneId; -import java.util.HashMap; -import java.util.Map; -import static org.junit.jupiter.api.Assertions.*; - -import cwms.cda.api.errors.FieldException; -import cwms.cda.formatters.ContentType; -import cwms.cda.formatters.Formats; -import cwms.cda.helpers.DTOMatch; -import java.io.InputStream; -import java.nio.charset.StandardCharsets; -import org.apache.commons.io.IOUtils; -import org.junit.jupiter.api.Test; - -final class TypedTimeSeriesIdentifierTest { - - @Test - void createAccessToWaterTimeSeriesData_allFieldsProvided_success() { - TimeSeriesIdentifierDescriptor tsDescriptor = new TimeSeriesIdentifierDescriptor.Builder() - .withTimeSeriesId("VANL.Stage.Inst.15Minutes.0.Ccp-Rev") - .withOfficeId("SWT") - .withActive(true) - .withIntervalOffsetMinutes(0L) - .withZoneId(ZoneId.of("UTC")) - .build(); - TimeSeriesIdentifierDescriptor tsDescriptor2 = new TimeSeriesIdentifierDescriptor.Builder() - .withTimeSeriesId("VANL.Flow.Inst.15Minutes.0.Ccp-Rev") - .withOfficeId("SWT") - .withActive(true) - .withIntervalOffsetMinutes(0L) - .withZoneId(ZoneId.of("UTC")) - .build(); - TypedTimeSeriesIdentifiers items = new TypedTimeSeriesIdentifiers.Builder() - .withLocationId(CwmsId.buildCwmsId("SWT", "VANL")) - .withTypeToTsId("STAGE", tsDescriptor) - .withTypeToTsId("OUTFLOW", tsDescriptor2) - .build(); - - assertAll( - () -> DTOMatch.assertMatch(CwmsId.buildCwmsId("SWT", "VANL"), items.getLocationId(), "Location ID"), - () -> DTOMatch.assertMatch(tsDescriptor, items.getTypeToTsIdMap().get("STAGE")), - () -> DTOMatch.assertMatch(tsDescriptor2, items.getTypeToTsIdMap().get("OUTFLOW")) - ); - } - - @Test - void createAccessToWaterTimeSeriesData_missingField_throwsFieldException() { - TimeSeriesIdentifierDescriptor tsDescriptor = new TimeSeriesIdentifierDescriptor.Builder() - .withTimeSeriesId("VANL.Stage.Inst.15Minutes.0.Ccp-Rev") - .withOfficeId("SWT") - .withActive(true) - .withIntervalOffsetMinutes(0L) - .withZoneId(ZoneId.of("UTC")) - .build(); - Map typeToTsIdMap = new HashMap<>(); - typeToTsIdMap.put("STAGE", tsDescriptor); - assertAll( - () -> assertThrows(FieldException.class, () -> { - TypedTimeSeriesIdentifiers item = new TypedTimeSeriesIdentifiers.Builder() - .withTypeToTsIdMap(typeToTsIdMap) - .build(); - item.validate(); - }, "The validate method should have thrown a FieldException because the location ID is missing") - ); - } - - @Test - void createAccessToWaterTimeSeriesData_serialize_roundtrip() { - TimeSeriesIdentifierDescriptor tsDescriptor = new TimeSeriesIdentifierDescriptor.Builder() - .withTimeSeriesId("VANL.Stage.Inst.15Minutes.0.Ccp-Rev") - .withOfficeId("SWT") - .withActive(true) - .withIntervalOffsetMinutes(0L) - .withZoneId(ZoneId.of("UTC")) - .build(); - TypedTimeSeriesIdentifiers data = new TypedTimeSeriesIdentifiers.Builder() - .withLocationId(CwmsId.buildCwmsId("SWT", "VANL")) - .withTypeToTsId("STAGE", tsDescriptor) - .build(); - - ContentType contentType = new ContentType(Formats.JSON); - String json = Formats.format(contentType, data); - TypedTimeSeriesIdentifiers deserialized = Formats.parseContent(contentType, json, TypedTimeSeriesIdentifiers.class); - DTOMatch.assertMatch(data, deserialized); - } - - @Test - void createAccessToWaterTimeSeriesData_deserialize() throws Exception { - TimeSeriesIdentifierDescriptor tsDescriptor = new TimeSeriesIdentifierDescriptor.Builder() - .withTimeSeriesId("VANL.Stage.Inst.15Minutes.0.Ccp-Rev") - .withOfficeId("SWT") - .withActive(true) - .withIntervalOffsetMinutes(0L) - .withZoneId(ZoneId.of("UTC")) - .build(); - TypedTimeSeriesIdentifiers expected = new TypedTimeSeriesIdentifiers.Builder() - .withLocationId(CwmsId.buildCwmsId("SWT", "VANL")) - .withTypeToTsId("STAGE", tsDescriptor) - .build(); - - InputStream resource = this.getClass().getResourceAsStream("/cwms/cda/data/dto/typed_time_series_identifiers.json"); - assertNotNull(resource); - String json = IOUtils.toString(resource, StandardCharsets.UTF_8); - ContentType contentType = new ContentType(Formats.JSON); - TypedTimeSeriesIdentifiers deserialized = Formats.parseContent(contentType, json, TypedTimeSeriesIdentifiers.class); - DTOMatch.assertMatch(expected, deserialized); - } -} diff --git a/cwms-data-api/src/test/java/cwms/cda/helpers/DTOMatch.java b/cwms-data-api/src/test/java/cwms/cda/helpers/DTOMatch.java index cb0a49c2e..771f24ff2 100644 --- a/cwms-data-api/src/test/java/cwms/cda/helpers/DTOMatch.java +++ b/cwms-data-api/src/test/java/cwms/cda/helpers/DTOMatch.java @@ -28,7 +28,9 @@ import cwms.cda.data.dto.TimeExtents; import cwms.cda.data.dto.TimeSeriesExtents; import cwms.cda.data.dto.TimeSeriesIdentifierDescriptor; -import cwms.cda.data.dto.TypedTimeSeriesIdentifiers; +import cwms.cda.data.dto.TimeSeriesIdentifiersByType; +import cwms.cda.data.dto.TimeSeriesIdentifiersByTypeList; +import cwms.cda.data.dto.TimeSeriesMetaData; import cwms.cda.data.dto.location.kind.Lock; import cwms.cda.data.dto.CwmsDTOBase; import cwms.cda.data.dto.location.kind.GateChange; @@ -88,7 +90,14 @@ private DTOMatch() { public static void assertMatch(CwmsId first, CwmsId second, String variableName) { assertAll( () -> Assertions.assertEquals(first.getOfficeId(), second.getOfficeId(),variableName + " is not the same. Office ID differs"), - () -> Assertions.assertEquals(first.getName(), second.getName(),variableName + " is not the same. Name differs") + () -> Assertions.assertEquals(first.getName(), second.getName(),variableName + " is not the same. Name differs"), + () -> first.getProperties().forEach((key, value) -> { + if (second.getProperties().containsKey(key)) { + assertEquals(value, second.getProperties().get(key), variableName + " is not the same. Property " + key + " differs"); + } else { + fail(variableName + " is not the same. Property " + key + " is missing"); + } + }) ); } @@ -612,20 +621,63 @@ public static void assertMatch(TimeSeriesIdentifierDescriptor tsDescriptor, Time ); } - public static void assertMatch(TypedTimeSeriesIdentifiers first, TypedTimeSeriesIdentifiers second) + public static void assertMatch(TimeSeriesIdentifiersByType first, TimeSeriesIdentifiersByType second) { assertAll( () -> assertMatch(first.getLocationId(), second.getLocationId()), - () -> assertEquals(first.getTypeToTsIdMap().size(), second.getTypeToTsIdMap().size(), "Type to TS ID map sizes do not match"), - () -> first.getTypeToTsIdMap().forEach((type, tsId) -> { - if (!second.getTypeToTsIdMap().containsKey(type)) { + () -> assertEquals(first.getTimeSeriesIds().size(), second.getTimeSeriesIds().size(), "Type to TS ID map sizes do not match"), + () -> first.getTimeSeriesIds().forEach((type, tsId) -> { + if (!second.getTimeSeriesIds().containsKey(type)) { fail("tsType " + type + " not found in both tsType to tsId maps"); } - assertMatch(tsId, second.getTypeToTsIdMap().get(type)); + assertMatch(tsId, second.getTimeSeriesIds().get(type)); + }) + ); + } + + public static void assertMatch(TimeSeriesMetaData ts1, TimeSeriesMetaData ts2) { + assertAll( + () -> assertMatch(ts1.getTsId(), ts2.getTsId()), + () -> assertEquals(ts1.getDateRefreshed(), ts2.getDateRefreshed(), "Date-Refreshed does not match"), + () -> assertEquals(ts1.getNotes(), ts2.getNotes(), "Notes do not match") + ); + } + + public static void assertMatch(TimeSeriesIdentifiersByTypeList list, TimeSeriesIdentifiersByTypeList list2) { + assertAll( + () -> assertEquals(list.getPage(), list2.getPage(), "Page does not match"), + () -> assertEquals(list.getPageSize(), list2.getPageSize(), "Page size does not match"), + () -> assertEquals(list.getTotal(), list2.getTotal(), "Total does not match"), + () -> assertEquals(list.getNextPage(), list2.getNextPage(), "Next page does not match"), + () -> assertEquals(list.getTimeSeriesIdsForLocations().size(), list2.getTimeSeriesIdsForLocations().size(), "Time series identifiers sizes do not match"), + () -> list.getTimeSeriesIdsForLocations().forEach(tsIdsForLocation -> { + TimeSeriesIdentifiersByType found = list2.getTimeSeriesIdsForLocations().stream() + .filter(tsId1 -> isEqual(tsIdsForLocation.getLocationId(),tsId1.getLocationId())) + .findFirst().orElse(null); + assertNotNull(found, "Time series identifiers were expected but not found for locationId: " + tsIdsForLocation.getLocationId().getName()); + assertMatch(tsIdsForLocation, found); }) ); } + private static boolean isEqual(CwmsId loc1, CwmsId loc2) { + return loc1.getName().equals(loc2.getName()) + && loc1.getOfficeId().equals(loc2.getOfficeId()) + && sameProperties(loc1, loc2); + } + + private static boolean sameProperties(CwmsId loc1, CwmsId loc2) { + boolean retVal = true; + for (String key : loc1.getProperties().keySet()) { + if (!loc2.getProperties().containsKey(key) || !loc1.getProperties().get(key).equals(loc2.getProperties().get(key))) { + retVal = false; + break; + } + } + return retVal; + } + + @FunctionalInterface public interface AssertMatchMethod{ void assertMatch(T first, T second); diff --git a/cwms-data-api/src/test/java/fixtures/CwmsDataApiSetupCallback.java b/cwms-data-api/src/test/java/fixtures/CwmsDataApiSetupCallback.java index a995ebdab..32c986bc4 100644 --- a/cwms-data-api/src/test/java/fixtures/CwmsDataApiSetupCallback.java +++ b/cwms-data-api/src/test/java/fixtures/CwmsDataApiSetupCallback.java @@ -97,7 +97,7 @@ private static int versionInt() { int ret = -999999; String tmp = schemaVersion(); - if (tmp.equalsIgnoreCase("latest-dev")) { + if (tmp.equalsIgnoreCase("latest-dev") || "Bypass".equalsIgnoreCase(tmp)) { ret = 999999; } else if(tmp.toLowerCase().endsWith("staging")) { diff --git a/cwms-data-api/src/test/resources/cwms/cda/data/dto/time_series_identifiers_by_type.json b/cwms-data-api/src/test/resources/cwms/cda/data/dto/time_series_identifiers_by_type.json new file mode 100644 index 000000000..c1709c4f0 --- /dev/null +++ b/cwms-data-api/src/test/resources/cwms/cda/data/dto/time_series_identifiers_by_type.json @@ -0,0 +1,126 @@ +{ + "cursor": "cursor", + "page-size": 20, + "total": 100, + "next-page": "", + "time-series-ids-for-locations": [ + { + "location-id": { + "name": "AARK", + "office-id": "SWT", + "bounding-office-id": "SWT", + "kind": "SITE" + }, + "time-series-ids": { + "STAGE": { + "ts-id": { + "office-id": "SWT", + "time-series-id": "AARK.Stage.Inst.15Minutes.0.Ccp-Rev", + "timezone-name": "UTC", + "interval-offset-minutes": 0, + "active": true + }, + "date-refreshed": "2025-03-03T12:00:00Z", + "notes": "Stage data for AARK" + }, + "OUTFLOW": { + "ts-id": { + "office-id": "SWT", + "time-series-id": "AARK.Flow.Inst.1Hour.0.Ccp-Rev", + "timezone-name": "UTC", + "interval-offset-minutes": 0, + "active": true + }, + "date-refreshed": "2025-03-03T12:00:00Z", + "notes": "Flow data for AARK" + } + } + }, + { + "location-id": { + "name": "AUGU", + "office-id": "SWT", + "bounding-office-id": "SWT", + "kind": "SITE" + }, + "time-series-ids": { + "PRECIP": { + "ts-id": { + "office-id": "SWT", + "time-series-id": "AUGU.Precip-Inc.Total.1Hour.1Hour.Ccp-Rev", + "timezone-name": "UTC", + "interval-offset-minutes": 0, + "active": true + }, + "date-refreshed": "2025-03-03T12:00:00Z", + "notes": "Precipitation data for AUGU" + }, + "STAGE": { + "ts-id": { + "office-id": "SWT", + "time-series-id": "AUGU.Stage.Inst.1Hour.0.Ccp-Rev", + "timezone-name": "UTC", + "interval-offset-minutes": 0, + "active": true + }, + "date-refreshed": "2025-03-03T12:00:00Z", + "notes": "Stage data for AUGU" + }, + "OUTFLOW": { + "ts-id": { + "office-id": "SWT", + "time-series-id": "AUGU.Flow.Inst.1Hour.0.Ccp-Rev", + "timezone-name": "UTC", + "interval-offset-minutes": 0, + "active": true + }, + "date-refreshed": "2025-03-03T12:00:00Z", + "notes": "Flow data for AUGU" + } + } + }, + { + "location-id": { + "name": "ALTU", + "office-id": "SWT", + "bounding-office-id": "SWT", + "kind": "SITE" + }, + "time-series-ids": { + "OUTFLOW": { + "ts-id": { + "office-id": "SWT", + "time-series-id": "ALTU.Flow-Res Out.Ave.1Hour.1Hour.Rev-Regi-Flowgroup", + "timezone-name": "UTC", + "interval-offset-minutes": 0, + "active": true + }, + "date-refreshed": "2025-03-03T12:00:00Z", + "notes": "Outflow data for ALTU" + }, + "FLOOD STORAGE": { + "ts-id": { + "office-id": "SWT", + "time-series-id": "ALTU.Stor-Flood Pool.Inst.1Hour.0.Ccp-Rev", + "timezone-name": "UTC", + "interval-offset-minutes": 0, + "active": true + }, + "date-refreshed": "2025-03-03T12:00:00Z", + "notes": "Flood storage data for ALTU" + }, + "CONSERVATION STORAGE": { + "ts-id": { + "office-id": "SWT", + "time-series-id": "ALTU.Stor-Conservation Pool.Inst.1Hour.0.Ccp-Rev", + "timezone-name": "UTC", + "interval-offset-minutes": 0, + "active": true + }, + "date-refreshed": "2025-03-03T12:00:00Z", + "notes": "Conservation storage data for ALTU" + } + } + } + ] +} diff --git a/cwms-data-api/src/test/resources/cwms/cda/data/dto/typed_time_series_identifiers.json b/cwms-data-api/src/test/resources/cwms/cda/data/dto/typed_time_series_identifiers.json deleted file mode 100644 index 519b87fc9..000000000 --- a/cwms-data-api/src/test/resources/cwms/cda/data/dto/typed_time_series_identifiers.json +++ /dev/null @@ -1,15 +0,0 @@ -{ - "location-id": { - "name": "VANL", - "office-id": "SWT" - }, - "type-to-ts-id-map": { - "STAGE": { - "office-id": "SWT", - "time-series-id": "VANL.Stage.Inst.15Minutes.0.Ccp-Rev", - "timezone-name": "UTC", - "interval-offset-minutes": 0, - "active": true - } - } -} From 8ba438990b352da1998ee02495c25ed59293f58f Mon Sep 17 00:00:00 2001 From: Bryson Spilman Date: Thu, 6 Mar 2025 10:50:21 -0800 Subject: [PATCH 6/7] CWMSVUE-634 -reverted build.gradle --- cwms-data-api/build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cwms-data-api/build.gradle b/cwms-data-api/build.gradle index a4334e12b..4459c5302 100644 --- a/cwms-data-api/build.gradle +++ b/cwms-data-api/build.gradle @@ -260,7 +260,7 @@ task run(type: JavaExec) { } task integrationTests(type: Test) { -// dependsOn test + dependsOn test dependsOn generateConfig dependsOn war From c09c6bc652d3dd206ca900b413ddd0030f92967b Mon Sep 17 00:00:00 2001 From: Bryson Spilman Date: Thu, 6 Mar 2025 10:57:39 -0800 Subject: [PATCH 7/7] CWMSVUE-634 - added notes for when work is picked back up --- .../cwms/cda/data/dao/AccessToWaterTimeSeriesDao.java | 11 ++++++++++- .../cda/data/dao/AccessToWaterTimeSeriesDaoTest.java | 2 ++ 2 files changed, 12 insertions(+), 1 deletion(-) diff --git a/cwms-data-api/src/main/java/cwms/cda/data/dao/AccessToWaterTimeSeriesDao.java b/cwms-data-api/src/main/java/cwms/cda/data/dao/AccessToWaterTimeSeriesDao.java index 71975b28a..69ce5b50f 100644 --- a/cwms-data-api/src/main/java/cwms/cda/data/dao/AccessToWaterTimeSeriesDao.java +++ b/cwms-data-api/src/main/java/cwms/cda/data/dao/AccessToWaterTimeSeriesDao.java @@ -59,6 +59,9 @@ public TimeSeriesIdentifiersByTypeList retrieveAccessToWaterTimeSeriesIds(Access String cursorOffice = null; String cursorLocId = null; + //TODO: Need to update this query to get notes, refresh date from a2w_loc2 view, + // bounding office, kind from location + // and active attribute from ts Condition officeCondition = retrievalParameters.getOfficeId() .map(AV_A2W_TS_CODES_BY_LOC2.DB_OFFICE_ID::eq) .orElse(noCondition()); @@ -118,7 +121,12 @@ public TimeSeriesIdentifiersByTypeList retrieveAccessToWaterTimeSeriesIds(Access String officeId = row.get(AV_A2W_TS_CODES_BY_LOC2.DB_OFFICE_ID); TimeSeriesMetaData tsId = buildTsId(row); - locationToTsTypeMap.computeIfAbsent(new InsensitiveCwmsId(CwmsId.buildCwmsId(locationId, officeId)), + locationToTsTypeMap.computeIfAbsent(new InsensitiveCwmsId(new CwmsId.Builder() + .withOfficeId(officeId) + .withName(locationId) + .withBoundingOfficeId(null) //TODO: Need to update this to get bounding office from location + .withKind(null) //TODO: Need to update this to get kind from location + .build()), k -> new LinkedHashMap<>()).put(tsTypeFromRow, tsId); }) ); @@ -151,6 +159,7 @@ private TimeSeriesMetaData buildTsId(Record5 { DSLContext context = getDslContext(c, databaseLink.getOfficeId()); // Call a2w store procedure @@ -204,6 +205,7 @@ public static void afterAll() throws SQLException { public void testRetrieval() throws Exception { System.out.println("TESTING A2W"); + //TODO: Need to write test for dao here }