From 863f0b2d78bee319147f0e0826a09ca29b420af1 Mon Sep 17 00:00:00 2001 From: Bryson Spilman Date: Thu, 12 Dec 2024 09:29:38 -0800 Subject: [PATCH 1/4] CWMVUE-611 - Adding in endpoint to get time range map for measurments. Updated measurement retrieval to use XML api. Fixed issues with quality. Introduced TimeExtents object. --- .../src/main/java/cwms/cda/ApiServlet.java | 4 + .../MeasurementTimeExtentsGetController.java | 93 +++++++++++++++ .../main/java/cwms/cda/data/dao/JooqDao.java | 12 ++ .../cwms/cda/data/dao/MeasurementDao.java | 103 ++++++++++++---- .../cda/data/dto/CwmsIdTimeExtentsEntry.java | 73 ++++++++++++ .../java/cwms/cda/data/dto/TimeExtents.java | 86 ++++++++++++++ .../cwms/cda/data/dto/TimeSeriesExtents.java | 47 +++++--- .../cda/api/MeasurementControllerTestIT.java | 21 ++++ .../cwms/cda/data/dao/MeasurementDaoTest.java | 2 +- .../cda/data/dao/MeasurementDaoTestIT.java | 78 +++++++++++- .../data/dto/CwmsIdTimeExtentsEntryTest.java | 111 ++++++++++++++++++ .../cwms/cda/data/dto/TimeExtentsTest.java | 89 ++++++++++++++ .../test/java/cwms/cda/helpers/DTOMatch.java | 16 +++ .../fixtures/CwmsDataApiSetupCallback.java | 3 +- .../resources/cwms/cda/api/measurement.json | 2 +- .../resources/cwms/cda/api/measurements.json | 4 +- .../cwms/cda/data/dao/dbMeasurements.xml | 2 +- .../data/dto/cwms_id_time_extents_entry.json | 10 ++ .../cwms/cda/data/dto/time_extents.json | 4 + 19 files changed, 712 insertions(+), 48 deletions(-) create mode 100644 cwms-data-api/src/main/java/cwms/cda/api/MeasurementTimeExtentsGetController.java create mode 100644 cwms-data-api/src/main/java/cwms/cda/data/dto/CwmsIdTimeExtentsEntry.java create mode 100644 cwms-data-api/src/main/java/cwms/cda/data/dto/TimeExtents.java create mode 100644 cwms-data-api/src/test/java/cwms/cda/data/dto/CwmsIdTimeExtentsEntryTest.java create mode 100644 cwms-data-api/src/test/java/cwms/cda/data/dto/TimeExtentsTest.java create mode 100644 cwms-data-api/src/test/resources/cwms/cda/data/dto/cwms_id_time_extents_entry.json create mode 100644 cwms-data-api/src/test/resources/cwms/cda/data/dto/time_extents.json 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 a6f6e70c7..b83729c0b 100644 --- a/cwms-data-api/src/main/java/cwms/cda/ApiServlet.java +++ b/cwms-data-api/src/main/java/cwms/cda/ApiServlet.java @@ -30,6 +30,7 @@ import static cwms.cda.api.Controllers.OFFICE; import static cwms.cda.api.Controllers.PROJECT_ID; import static cwms.cda.api.Controllers.WATER_USER; +import cwms.cda.api.MeasurementTimeExtentsGetController; import static io.javalin.apibuilder.ApiBuilder.crud; import static io.javalin.apibuilder.ApiBuilder.delete; import static io.javalin.apibuilder.ApiBuilder.get; @@ -594,6 +595,9 @@ protected void configureRoutes() { cdaCrudCache(format("/stream-reaches/{%s}", NAME), new StreamReachController(metrics), requiredRoles,1, TimeUnit.DAYS); String measurements = "/measurements/"; + String measTimeExtents = measurements + "time-extents"; + get(measTimeExtents,new MeasurementTimeExtentsGetController(metrics)); + addCacheControl(measTimeExtents, 5, TimeUnit.MINUTES); cdaCrudCache(format(measurements + "{%s}", LOCATION_ID), new cwms.cda.api.MeasurementController(metrics), requiredRoles,5, TimeUnit.MINUTES); cdaCrudCache("/blobs/{blob-id}", diff --git a/cwms-data-api/src/main/java/cwms/cda/api/MeasurementTimeExtentsGetController.java b/cwms-data-api/src/main/java/cwms/cda/api/MeasurementTimeExtentsGetController.java new file mode 100644 index 000000000..acb86ee7c --- /dev/null +++ b/cwms-data-api/src/main/java/cwms/cda/api/MeasurementTimeExtentsGetController.java @@ -0,0 +1,93 @@ +/* + * MIT License + * + * Copyright (c) 2024 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.OFFICE_MASK; +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.data.dao.JooqDao.getDslContext; +import cwms.cda.data.dao.MeasurementDao; +import cwms.cda.data.dto.CwmsIdTimeExtentsEntry; +import cwms.cda.formatters.ContentType; +import cwms.cda.formatters.Formats; +import io.javalin.core.util.Header; +import io.javalin.http.Context; +import io.javalin.http.Handler; +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 java.util.List; +import javax.servlet.http.HttpServletResponse; +import org.jetbrains.annotations.NotNull; +import org.jooq.DSLContext; + +public final class MeasurementTimeExtentsGetController implements Handler { + private final MetricRegistry metrics; + private final Histogram requestResultSize; + + public MeasurementTimeExtentsGetController(MetricRegistry metrics) { + this.metrics = metrics; + String className = this.getClass().getName(); + requestResultSize = this.metrics.histogram(name(className, RESULTS, SIZE)); + } + + private Timer.Context markAndTime() { + return Controllers.markAndTime(metrics, getClass().getName(), Controllers.GET_ALL); + } + + @OpenApi( + queryParams = { + @OpenApiParam(name = OFFICE_MASK, description = "Office Id used to filter the results.") + }, + responses = { + @OpenApiResponse(status = STATUS_200, content = { + @OpenApiContent(isArray = true, type = Formats.JSONV1, from = CwmsIdTimeExtentsEntry.class) + }) + }, + description = "Returns matching downstream stream locations.", + tags = {StreamLocationController.TAG} + ) + public void handle(@NotNull Context ctx) throws Exception { + String officeIdMask = ctx.queryParam(OFFICE_MASK); + try (Timer.Context ignored = markAndTime()) { + DSLContext dsl = getDslContext(ctx); + MeasurementDao dao = new MeasurementDao(dsl); + List timeExtents = dao.retrieveMeasurementTimeExtentsMap(officeIdMask); + + String formatHeader = ctx.header(Header.ACCEPT); + ContentType contentType = Formats.parseHeader(formatHeader, CwmsIdTimeExtentsEntry.class); + ctx.contentType(contentType.toString()); + String serialized = Formats.format(contentType, timeExtents, CwmsIdTimeExtentsEntry.class); + ctx.result(serialized); + ctx.status(HttpServletResponse.SC_OK); + requestResultSize.update(serialized.length()); + } + } +} diff --git a/cwms-data-api/src/main/java/cwms/cda/data/dao/JooqDao.java b/cwms-data-api/src/main/java/cwms/cda/data/dao/JooqDao.java index 622c0ab38..832e5cd5b 100644 --- a/cwms-data-api/src/main/java/cwms/cda/data/dao/JooqDao.java +++ b/cwms-data-api/src/main/java/cwms/cda/data/dao/JooqDao.java @@ -24,6 +24,8 @@ package cwms.cda.data.dao; +import java.sql.Timestamp; +import java.time.Instant; import static org.jooq.SQLDialect.ORACLE; import com.google.common.flogger.FluentLogger; @@ -128,6 +130,16 @@ public static DSLContext getDslContext(Context ctx) { return retVal; } + protected static Timestamp buildTimestamp(Instant date) + { + Timestamp retval = null; + if(date != null) + { + retval = Timestamp.from(date); + } + return retval; + } + public static DSLContext getDslContext(Connection connection, String officeId) { // Because this dsl is constructed with a connection, jOOQ will reuse the provided // connection and not get new connections from a DataSource. See: diff --git a/cwms-data-api/src/main/java/cwms/cda/data/dao/MeasurementDao.java b/cwms-data-api/src/main/java/cwms/cda/data/dao/MeasurementDao.java index 5aaac5932..679f3e6bb 100644 --- a/cwms-data-api/src/main/java/cwms/cda/data/dao/MeasurementDao.java +++ b/cwms-data-api/src/main/java/cwms/cda/data/dao/MeasurementDao.java @@ -46,6 +46,8 @@ import cwms.cda.api.enums.UnitSystem; import cwms.cda.api.errors.NotFoundException; import cwms.cda.data.dto.CwmsId; +import cwms.cda.data.dto.CwmsIdTimeExtentsEntry; +import cwms.cda.data.dto.TimeExtents; import cwms.cda.data.dto.measurement.Measurement; import cwms.cda.data.dto.measurement.StreamflowMeasurement; import cwms.cda.data.dto.measurement.SupplementalStreamflowMeasurement; @@ -56,11 +58,11 @@ import java.sql.Timestamp; import java.time.Instant; import java.time.OffsetDateTime; +import java.time.ZoneId; import java.time.ZonedDateTime; import java.time.format.DateTimeFormatter; import java.util.ArrayList; -import java.util.Date; -import java.util.TimeZone; +import java.util.Optional; import mil.army.usace.hec.metadata.location.LocationTemplate; import org.jooq.DSLContext; @@ -68,12 +70,16 @@ import static java.util.stream.Collectors.toList; import org.jooq.impl.DSL; -import usace.cwms.db.dao.util.OracleTypeMap; +import static org.jooq.impl.DSL.max; +import static org.jooq.impl.DSL.min; import usace.cwms.db.jooq.codegen.packages.CWMS_STREAM_PACKAGE; +import usace.cwms.db.jooq.codegen.tables.AV_STREAMFLOW_MEAS; import usace.cwms.db.jooq.codegen.udt.records.STREAMFLOW_MEAS2_T; -import usace.cwms.db.jooq.codegen.udt.records.STREAMFLOW_MEAS2_TAB_T; public final class MeasurementDao extends JooqDao { + + static final String MIN_DATE = "MIN_DATE"; + static final String MAX_DATE = "MAX_DATE"; static final XmlMapper XML_MAPPER = buildXmlMapper(); public MeasurementDao(DSLContext dsl) { @@ -93,19 +99,16 @@ public List retrieveMeasurements(String officeId, String locationId String agencies, String qualities) { return connectionResult(dsl, conn -> { setOffice(conn, officeId); - Timestamp minTimestamp = OracleTypeMap.buildTimestamp(minDateMask == null ? null : Date.from(minDateMask)); - Timestamp maxTimestamp = OracleTypeMap.buildTimestamp(maxDateMask == null ? null : Date.from(maxDateMask)); - TimeZone timeZone = OracleTypeMap.GMT_TIME_ZONE; - return retrieveMeasurementsJooq(conn, officeId, locationId, unitSystem, minHeight, maxHeight, minFlow, maxFlow, minNum, maxNum, agencies, qualities, minTimestamp, maxTimestamp, timeZone); + Timestamp minTimestamp = buildTimestamp(minDateMask); + Timestamp maxTimestamp = buildTimestamp(maxDateMask); + return retrieveMeasurementsJooq(conn, officeId, locationId, unitSystem, minHeight, maxHeight, minFlow, maxFlow, minNum, maxNum, agencies, qualities, minTimestamp, maxTimestamp); }); } - private static List retrieveMeasurementsJooq(Connection conn, String officeId, String locationId, String unitSystem, Number minHeight, Number maxHeight, Number minFlow, Number maxFlow, String minNum, String maxNum, String agencies, String qualities, Timestamp minTimestamp, Timestamp maxTimestamp, TimeZone timeZone) { - STREAMFLOW_MEAS2_TAB_T retrieved = CWMS_STREAM_PACKAGE.call_RETRIEVE_MEAS_OBJS(DSL.using(conn).configuration(), locationId, unitSystem, minTimestamp, maxTimestamp, - minHeight, maxHeight, minFlow, maxFlow, minNum, maxNum, agencies, qualities, timeZone.getID(), officeId); - List retVal = retrieved.stream() - .map(MeasurementDao::fromJooqMeasurementRecord) - .collect(toList()); + private static List retrieveMeasurementsJooq(Connection conn, String officeId, String locationId, String unitSystem, Number minHeight, Number maxHeight, Number minFlow, Number maxFlow, String minNum, String maxNum, String agencies, String qualities, Timestamp minTimestamp, Timestamp maxTimestamp) throws JsonProcessingException { + String xml = CWMS_STREAM_PACKAGE.call_RETRIEVE_MEAS_XML(DSL.using(conn).configuration(), locationId, unitSystem, minTimestamp, maxTimestamp, + minHeight, maxHeight, minFlow, maxFlow, minNum, maxNum, agencies, qualities, "UTC", officeId); + List retVal = fromDbXml(xml); if(retVal.isEmpty()) { throw new NotFoundException("No measurements found."); } @@ -142,24 +145,55 @@ public void deleteMeasurements(String officeId, String locationId, Instant minDa String maxNum) { connection(dsl, conn -> { setOffice(conn, officeId); - Timestamp minTimestamp = OracleTypeMap.buildTimestamp(minDateMask == null ? null : Date.from(minDateMask)); - Timestamp maxTimestamp = OracleTypeMap.buildTimestamp(maxDateMask == null ? null : Date.from(maxDateMask)); - TimeZone timeZone = OracleTypeMap.GMT_TIME_ZONE; - String timeZoneId = timeZone.getID(); + Timestamp minTimestamp = buildTimestamp(minDateMask); + Timestamp maxTimestamp = buildTimestamp(maxDateMask); + String timeZoneId = "UTC"; verifyMeasurementsExists(conn, officeId, locationId, maxNum, maxNum); CWMS_STREAM_PACKAGE.call_DELETE_STREAMFLOW_MEAS(DSL.using(conn).configuration(), locationId, minNum, minTimestamp, maxTimestamp, null, null, null, null, maxNum, maxNum, null, null, timeZoneId, officeId); }); } - private void verifyMeasurementsExists(Connection conn, String officeId, String locationId, String minNum, String maxNum) { + public List retrieveMeasurementTimeExtentsMap(String officeIdMask) + { + AV_STREAMFLOW_MEAS view = AV_STREAMFLOW_MEAS.AV_STREAMFLOW_MEAS; + return dsl.select( + view.OFFICE_ID, + view.LOCATION_ID, + min(view.DATE_TIME_UTC).as(MIN_DATE).convertFrom(Timestamp::toInstant), + max(view.DATE_TIME_UTC).as(MAX_DATE).convertFrom(Timestamp::toInstant) + ) + .from(view) + .where(Optional.ofNullable(officeIdMask) + .map(view.OFFICE_ID::eq) + .orElse(DSL.trueCondition())) + .groupBy(view.OFFICE_ID, view.LOCATION_ID) + .fetch() + .map(record -> new CwmsIdTimeExtentsEntry.Builder() + .withId( new CwmsId.Builder() + .withOfficeId(record.get(view.OFFICE_ID)) + .withName(record.get(view.LOCATION_ID)) + .build()) + .withTimeExtents(new TimeExtents.Builder() + .withEarliestTime(record.value3().atZone(ZoneId.of("UTC"))) + .withLatestTime(record.value4().atZone(ZoneId.of("UTC"))) + .build()) + .build()); + } + + private void verifyMeasurementsExists(Connection conn, String officeId, String locationId, String minNum, String maxNum) throws JsonProcessingException { List measurements = retrieveMeasurementsJooq(conn, officeId, locationId, UnitSystem.EN.toString(), - null, null, null, null, minNum, maxNum, null, null, null, null, OracleTypeMap.GMT_TIME_ZONE); + null, null, null, null, minNum, maxNum, null, null, null, null); if (measurements.isEmpty()) { throw new NotFoundException("Could not find measurements for " + locationId + " in office " + officeId + "."); } } + static List fromDbXml(String xml) throws JsonProcessingException { + MeasurementsXmlDto xmlDto = XML_MAPPER.readValue(xml, MeasurementsXmlDto.class); + return convertMeasurementsFromXmlDto(xmlDto); + } + static String toDbXml(List measurements) throws JsonProcessingException { MeasurementsXmlDto xmlDto = convertMeasurementsToXmlDto(measurements); return XML_MAPPER.writeValueAsString(xmlDto); @@ -249,6 +283,35 @@ static MeasurementXmlDto convertMeasurementToXmlDto(Measurement meas) .build(); } + private static List convertMeasurementsFromXmlDto(MeasurementsXmlDto xmlDto) { + return xmlDto.getMeasurements().stream() + .map(MeasurementDao::convertMeasurementFromXmlDto) + .collect(toList()); + } + + static Measurement convertMeasurementFromXmlDto(MeasurementXmlDto dto) { + return new Measurement.Builder() + .withAgency(dto.getAgency()) + .withAreaUnit(dto.getAreaUnit()) + .withFlowUnit(dto.getFlowUnit()) + .withHeightUnit(dto.getHeightUnit()) + .withInstant(dto.getInstant()) + .withId(new CwmsId.Builder() + .withName(dto.getLocationId()) + .withOfficeId(dto.getOfficeId()) + .build()) + .withNumber(dto.getNumber()) + .withParty(dto.getParty()) + .withTempUnit(dto.getTempUnit()) + .withUsed(dto.isUsed()) + .withVelocityUnit(dto.getVelocityUnit()) + .withStreamflowMeasurement(dto.getStreamflowMeasurement()) + .withSupplementalStreamflowMeasurement(dto.getSupplementalStreamflowMeasurement()) + .withUsgsMeasurement(dto.getUsgsMeasurement()) + .withWmComments(dto.getWmComments()) + .build(); + } + private static XmlMapper buildXmlMapper() { XmlMapper retVal = new XmlMapper(); retVal.registerModule(new JavaTimeModule()); diff --git a/cwms-data-api/src/main/java/cwms/cda/data/dto/CwmsIdTimeExtentsEntry.java b/cwms-data-api/src/main/java/cwms/cda/data/dto/CwmsIdTimeExtentsEntry.java new file mode 100644 index 000000000..90296c8a2 --- /dev/null +++ b/cwms-data-api/src/main/java/cwms/cda/data/dto/CwmsIdTimeExtentsEntry.java @@ -0,0 +1,73 @@ +/* + * MIT License + * + * Copyright (c) 2024 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; + +@FormattableWith(contentType = Formats.JSONV1, formatter = JsonV1.class, aliases = {Formats.DEFAULT, Formats.JSON}) +@JsonDeserialize(builder = CwmsIdTimeExtentsEntry.Builder.class) +@JsonInclude(JsonInclude.Include.NON_NULL) +@JsonNaming(PropertyNamingStrategies.KebabCaseStrategy.class) +public final class CwmsIdTimeExtentsEntry extends CwmsDTOBase{ + private final CwmsId id; + private final TimeExtents timeExtents; + + public CwmsIdTimeExtentsEntry(Builder builder) { + this.id = builder.id; + this.timeExtents = builder.timeExtents; + } + + public CwmsId getId() { + return id; + } + + public TimeExtents getTimeExtents() { + return timeExtents; + } + + public static final class Builder { + private CwmsId id; + private TimeExtents timeExtents; + + public Builder withId(CwmsId id) { + this.id = id; + return this; + } + + public Builder withTimeExtents(TimeExtents timeExtents) { + this.timeExtents = timeExtents; + return this; + } + + public CwmsIdTimeExtentsEntry build() { + return new CwmsIdTimeExtentsEntry(this); + } + } +} diff --git a/cwms-data-api/src/main/java/cwms/cda/data/dto/TimeExtents.java b/cwms-data-api/src/main/java/cwms/cda/data/dto/TimeExtents.java new file mode 100644 index 000000000..e3be79734 --- /dev/null +++ b/cwms-data-api/src/main/java/cwms/cda/data/dto/TimeExtents.java @@ -0,0 +1,86 @@ +/* + * MIT License + * + * Copyright (c) 2024 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.JsonFormat; +import com.fasterxml.jackson.annotation.JsonFormat.Shape; +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 io.swagger.v3.oas.annotations.media.Schema; + +import java.time.ZonedDateTime; + +@FormattableWith(contentType = Formats.JSONV1, formatter = JsonV1.class, aliases = {Formats.DEFAULT, Formats.JSON}) +@JsonInclude(JsonInclude.Include.NON_NULL) +@JsonDeserialize(builder = TimeExtents.Builder.class) +@Schema(description = "Represents the start and end times of an extent") +@JsonNaming(PropertyNamingStrategies.KebabCaseStrategy.class) +public class TimeExtents extends CwmsDTOBase { + + @Schema(description = "Earliest value in the timeseries") + @JsonFormat(shape = Shape.STRING) + private final ZonedDateTime earliestTime; + + @Schema(description = "Latest value in the timeseries") + @JsonFormat(shape = Shape.STRING) + private final ZonedDateTime latestTime; + + public TimeExtents(Builder builder) { + this.earliestTime = builder.earliestTime; + this.latestTime = builder.latestTime; + } + + public ZonedDateTime getEarliestTime() { + return this.earliestTime; + } + + public ZonedDateTime getLatestTime() { + return this.latestTime; + } + + public static class Builder { + private ZonedDateTime earliestTime; + private ZonedDateTime latestTime; + + public TimeExtents.Builder withEarliestTime(ZonedDateTime start) { + this.earliestTime = start; + return this; + } + + public TimeExtents.Builder withLatestTime(ZonedDateTime end) { + this.latestTime = end; + return this; + } + + public TimeExtents build() { + return new TimeExtents(this); + } + } +} + diff --git a/cwms-data-api/src/main/java/cwms/cda/data/dto/TimeSeriesExtents.java b/cwms-data-api/src/main/java/cwms/cda/data/dto/TimeSeriesExtents.java index fffdecae2..7de38d579 100644 --- a/cwms-data-api/src/main/java/cwms/cda/data/dto/TimeSeriesExtents.java +++ b/cwms-data-api/src/main/java/cwms/cda/data/dto/TimeSeriesExtents.java @@ -1,3 +1,26 @@ +/* + * MIT License + * + * Copyright (c) 2024 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.JsonFormat; @@ -16,33 +39,27 @@ @Schema(description = "TimeSeries extent information") @JsonPropertyOrder(alphabetic = true) @JsonNaming(PropertyNamingStrategies.KebabCaseStrategy.class) -public class TimeSeriesExtents { +public class TimeSeriesExtents extends TimeExtents { @Schema(description = "TimeSeries version to which this extent information applies") @JsonFormat(shape = Shape.STRING) ZonedDateTime versionTime; - @Schema(description = "Earliest value in the timeseries") - @JsonFormat(shape = Shape.STRING) - ZonedDateTime earliestTime; - - @Schema(description = "Latest value in the timeseries") - @JsonFormat(shape = Shape.STRING) - ZonedDateTime latestTime; - @Schema(description = "Last update in the timeseries") @JsonFormat(shape = Shape.STRING) ZonedDateTime lastUpdate; @SuppressWarnings("unused") // required so JAXB can initialize and marshal private TimeSeriesExtents() { + super(new Builder()); } public TimeSeriesExtents(final ZonedDateTime versionTime, final ZonedDateTime earliestTime, final ZonedDateTime latestTime, final ZonedDateTime lastUpdateTime) { + super(new TimeExtents.Builder() + .withEarliestTime(earliestTime) + .withLatestTime(latestTime)); this.versionTime = versionTime; - this.earliestTime = earliestTime; - this.latestTime = latestTime; this.lastUpdate = lastUpdateTime; } @@ -63,14 +80,6 @@ public ZonedDateTime getVersionTime() { return this.versionTime; } - public ZonedDateTime getEarliestTime() { - return this.earliestTime; - } - - public ZonedDateTime getLatestTime() { - return this.latestTime; - } - public ZonedDateTime getLastUpdate() { return this.lastUpdate; } diff --git a/cwms-data-api/src/test/java/cwms/cda/api/MeasurementControllerTestIT.java b/cwms-data-api/src/test/java/cwms/cda/api/MeasurementControllerTestIT.java index aab51f262..a38df8058 100644 --- a/cwms-data-api/src/test/java/cwms/cda/api/MeasurementControllerTestIT.java +++ b/cwms-data-api/src/test/java/cwms/cda/api/MeasurementControllerTestIT.java @@ -28,6 +28,7 @@ import static cwms.cda.data.dao.DaoTest.getDslContext; import cwms.cda.data.dao.DeleteRule; import cwms.cda.data.dao.MeasurementDao; +import cwms.cda.data.dao.MeasurementDaoTestIT; import static cwms.cda.data.dao.MeasurementDaoTestIT.MINIMUM_SCHEMA; import cwms.cda.data.dao.StreamDao; import cwms.cda.data.dto.CwmsId; @@ -73,6 +74,7 @@ public static void setup() throws SQLException { createLocation(testLoc, true, OFFICE_ID, "STREAM_LOCATION"); TEST_STREAM_LOC_IDS.add(testLoc); createAndStoreTestStream("ImOnThisStream2"); + MeasurementDaoTestIT.copyAtDisplayUnitsWithUpdatedOfficeCode(OFFICE_ID); } static void createAndStoreTestStream(String testLoc) throws SQLException { @@ -377,6 +379,25 @@ void test_create_retrieve_delete_measurement_multiple() throws IOException { .body("[1].usgs-measurement.air-temp", equalTo(measurement2.getUsgsMeasurement().getAirTemp().floatValue())) .body("[1].usgs-measurement.water-temp", equalTo(measurement2.getUsgsMeasurement().getWaterTemp().floatValue())); + // Retrieve the Measurements Extents and assert that they exists + given() + .log().ifValidationFails(LogDetail.ALL, true) + .accept(Formats.JSON) + .queryParam(Controllers.OFFICE_MASK, measurement1.getId().getOfficeId()) + .when() + .redirects().follow(true) + .redirects().max(3) + .get("/measurements/time-extents") + .then() + .log().ifValidationFails(LogDetail.ALL, true) + .assertThat() + .statusCode(is(HttpServletResponse.SC_OK)) + //measurement1 and measurement2 share the same location id, so the time-extents should cover measurement1 and measurement2 + .body("find { it.id.name == '" + measurement1.getLocationId() + "' }.id.name", equalTo(measurement1.getLocationId())) + .body("find { it.id.name == '" + measurement1.getLocationId() + "' }.id.office-id", equalTo(measurement1.getOfficeId())) + .body("find { it.id.name == '" + measurement1.getLocationId() + "' }.time-extents.earliest-time", equalTo(measurement1.getInstant().toString())) + .body("find { it.id.name == '" + measurement1.getLocationId() + "' }.time-extents.latest-time", equalTo(measurement2.getInstant().toString())); + // Delete the Measurements given() .log().ifValidationFails(LogDetail.ALL, true) diff --git a/cwms-data-api/src/test/java/cwms/cda/data/dao/MeasurementDaoTest.java b/cwms-data-api/src/test/java/cwms/cda/data/dao/MeasurementDaoTest.java index 0007a3b0b..1cadd0e79 100644 --- a/cwms-data-api/src/test/java/cwms/cda/data/dao/MeasurementDaoTest.java +++ b/cwms-data-api/src/test/java/cwms/cda/data/dao/MeasurementDaoTest.java @@ -220,7 +220,7 @@ private Measurement buildMeasurement2() { .withStreamflowMeasurement(new StreamflowMeasurement.Builder() .withFlow(200.0) .withGageHeight(2.4) - .withQuality("G") + .withQuality("Good") .build()) .withUsgsMeasurement(new UsgsMeasurement.Builder() .withAirTemp(35.0) diff --git a/cwms-data-api/src/test/java/cwms/cda/data/dao/MeasurementDaoTestIT.java b/cwms-data-api/src/test/java/cwms/cda/data/dao/MeasurementDaoTestIT.java index 30331c1c7..ffd404b56 100644 --- a/cwms-data-api/src/test/java/cwms/cda/data/dao/MeasurementDaoTestIT.java +++ b/cwms-data-api/src/test/java/cwms/cda/data/dao/MeasurementDaoTestIT.java @@ -5,6 +5,7 @@ import cwms.cda.api.errors.NotFoundException; import static cwms.cda.data.dao.DaoTest.getDslContext; import cwms.cda.data.dto.CwmsId; +import cwms.cda.data.dto.CwmsIdTimeExtentsEntry; import cwms.cda.data.dto.measurement.Measurement; import cwms.cda.data.dto.measurement.StreamflowMeasurement; import cwms.cda.data.dto.measurement.SupplementalStreamflowMeasurement; @@ -23,8 +24,16 @@ import java.util.stream.Collectors; import mil.army.usace.hec.test.database.CwmsDatabaseContainer; import org.jooq.DSLContext; +import org.jooq.Field; +import org.jooq.Record; +import org.jooq.Record4; +import org.jooq.SelectConditionStep; +import org.jooq.Table; +import org.jooq.impl.DSL; +import static org.jooq.impl.DSL.inline; import org.junit.jupiter.api.AfterAll; import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; import static org.junit.jupiter.api.Assertions.assertNotNull; import static org.junit.jupiter.api.Assertions.assertThrows; import org.junit.jupiter.api.BeforeAll; @@ -55,6 +64,61 @@ public static void setup() { //ignore if already exists } } + copyAtDisplayUnitsWithUpdatedOfficeCode(OFFICE_ID); + } + + public static void copyAtDisplayUnitsWithUpdatedOfficeCode(String officeId) { + try { + CwmsDatabaseContainer db = CwmsDataApiSetupCallback.getDatabaseLink(); + db.connection((c) -> { + DSLContext dsl = DSL.using(c); // Create the JOOQ DSLContext from the connection + + // Define the table and fields for AT_DISPLAY_UNITS + Table AT_DISPLAY_UNITS = DSL.table("AT_DISPLAY_UNITS"); + Field OFFICE_CODE = DSL.field("office_code", Integer.class); + Field DB_OFFICE_CODE = DSL.field("db_office_code", Integer.class); + Field PARAMETER_CODE = DSL.field("parameter_code", Integer.class); + Field UNIT_SYSTEM = DSL.field("unit_system", String.class); + Field DISPLAY_UNIT_CODE = DSL.field("display_unit_code", Integer.class); + + // Fetch the db_office_code for the officeId + Integer cwmsDbOfficeCode = dsl.select(OFFICE_CODE) + .from(DSL.table("CWMS_OFFICE")) + .where(DSL.field("office_id", String.class).eq(officeId)) + .fetchOne(OFFICE_CODE); + + if (cwmsDbOfficeCode == null) { + throw new IllegalArgumentException("No db_office_code found for office_id: " + officeId); + } + + // Check if the db_office_code already exists in AT_DISPLAY_UNITS + boolean codeExists = dsl.fetchExists( + dsl.selectOne() + .from(AT_DISPLAY_UNITS) + .where(DB_OFFICE_CODE.eq(cwmsDbOfficeCode)) + ); + + if (!codeExists) { + // Construct a new SELECT query with the updated db_office_code + SelectConditionStep> selectQuery = dsl.selectDistinct( + inline(cwmsDbOfficeCode).as(DB_OFFICE_CODE.getName()), + PARAMETER_CODE, + UNIT_SYSTEM, + DISPLAY_UNIT_CODE + ) + .from(AT_DISPLAY_UNITS) + .where(DB_OFFICE_CODE.ne(cwmsDbOfficeCode)); // Exclude rows already with the target db_office_code + + // Insert rows into AT_DISPLAY_UNITS with updated db_office_code + dsl.insertInto(AT_DISPLAY_UNITS) + .columns(DB_OFFICE_CODE, PARAMETER_CODE, UNIT_SYSTEM, DISPLAY_UNIT_CODE) + .select(selectQuery) + .execute(); + } + }, "cwms_20"); + } catch (SQLException ex) { + throw new RuntimeException(ex); + } } @AfterAll @@ -151,6 +215,16 @@ void testRoundTripStore() throws Exception { assertNotNull(retrievedMeas1B); DTOMatch.assertMatch(meas1B, retrievedMeas1B); + List timeExtents = measurementDao.retrieveMeasurementTimeExtentsMap(OFFICE_ID); + assertFalse(timeExtents.isEmpty()); + CwmsIdTimeExtentsEntry extentsFound = timeExtents.stream() + .filter(te -> te.getId().getName().equals(meas1.getId().getName()) && te.getId().getOfficeId().equals(meas1.getId().getOfficeId())) + .findFirst() + .orElse(null); + assertNotNull(extentsFound); + assertEquals(meas1.getInstant(), extentsFound.getTimeExtents().getEarliestTime().toInstant()); + assertEquals(meas1B.getInstant(), extentsFound.getTimeExtents().getLatestTime().toInstant()); + //delete measurements measurementDao.deleteMeasurements(meas1.getId().getOfficeId(), meas1.getId().getName(), null, null, null, null); measurementDao.deleteMeasurements(meas2.getId().getOfficeId(), meas2.getId().getName(), null, null, null, null); @@ -201,7 +275,7 @@ private Measurement buildMeasurement1(String streamLocId, double flow) { .withStreamflowMeasurement(new StreamflowMeasurement.Builder() .withFlow(flow) .withGageHeight(2.0) - .withQuality("G") + .withQuality("Good") .build()) .withUsgsMeasurement(new UsgsMeasurement.Builder() .withAirTemp(11.0) @@ -258,7 +332,7 @@ private Measurement buildMeasurement2(String streamLocId, double flow) { .withStreamflowMeasurement(new StreamflowMeasurement.Builder() .withFlow(flow) .withGageHeight(4.0) - .withQuality("G") + .withQuality("Good") .build()) .withUsgsMeasurement(new UsgsMeasurement.Builder() .withAirTemp(26.0) diff --git a/cwms-data-api/src/test/java/cwms/cda/data/dto/CwmsIdTimeExtentsEntryTest.java b/cwms-data-api/src/test/java/cwms/cda/data/dto/CwmsIdTimeExtentsEntryTest.java new file mode 100644 index 000000000..73698d617 --- /dev/null +++ b/cwms-data-api/src/test/java/cwms/cda/data/dto/CwmsIdTimeExtentsEntryTest.java @@ -0,0 +1,111 @@ +/* + * MIT License + * + * Copyright (c) 2024 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 cwms.cda.formatters.ContentType; +import cwms.cda.formatters.Formats; +import cwms.cda.helpers.DTOMatch; +import org.apache.commons.io.IOUtils; +import org.junit.jupiter.api.Test; + +import java.io.InputStream; +import java.nio.charset.StandardCharsets; +import java.time.ZonedDateTime; + +import static org.junit.jupiter.api.Assertions.assertAll; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNotNull; + +final class CwmsIdTimeExtentsEntryTest { + + @Test + void createCwmsIdTimeExtentsEntry_allFieldsProvided_success() { + CwmsId id = new CwmsId.Builder() + .withOfficeId("SWT") + .withName("ARBU").build(); + ZonedDateTime start = ZonedDateTime.parse("2024-12-06T00:00:00Z"); + ZonedDateTime end = ZonedDateTime.parse("2024-12-06T12:00:00Z"); + TimeExtents timeExtents = new TimeExtents.Builder() + .withEarliestTime(start) + .withLatestTime(end) + .build(); + CwmsIdTimeExtentsEntry entry = new CwmsIdTimeExtentsEntry.Builder() + .withId(id) + .withTimeExtents(timeExtents) + .build(); + + assertAll( + () -> assertEquals(id, entry.getId(), "CwmsId"), + () -> assertEquals(timeExtents, entry.getTimeExtents(), "TimeExtents") + ); + } + + @Test + void createCwmsIdTimeExtentsEntry_serialize_roundtrip() { + CwmsId id = new CwmsId.Builder() + .withOfficeId("SWT") + .withName("ARBU").build(); + ZonedDateTime start = ZonedDateTime.parse("2024-12-06T00:00:00Z"); + ZonedDateTime end = ZonedDateTime.parse("2024-12-06T12:00:00Z"); + TimeExtents timeExtents = new TimeExtents.Builder() + .withEarliestTime(start) + .withLatestTime(end) + .build(); + CwmsIdTimeExtentsEntry entry = new CwmsIdTimeExtentsEntry.Builder() + .withId(id) + .withTimeExtents(timeExtents) + .build(); + + ContentType contentType = new ContentType(Formats.JSON); + String json = Formats.format(contentType, entry); + CwmsIdTimeExtentsEntry deserialized = Formats.parseContent(contentType, json, CwmsIdTimeExtentsEntry.class); + + DTOMatch.assertMatch(entry, deserialized); + } + + @Test + void createCwmsIdTimeExtentsEntry_deserialize() throws Exception { + InputStream resource = this.getClass().getResourceAsStream("/cwms/cda/data/dto/cwms_id_time_extents_entry.json"); + assertNotNull(resource); + String json = IOUtils.toString(resource, StandardCharsets.UTF_8); + ContentType contentType = new ContentType(Formats.JSON); + CwmsIdTimeExtentsEntry deserialized = Formats.parseContent(contentType, json, CwmsIdTimeExtentsEntry.class); + + CwmsId id = new CwmsId.Builder() + .withOfficeId("SWT") + .withName("ARBU").build(); + ZonedDateTime start = ZonedDateTime.parse("2024-12-06T00:00:00Z"); + ZonedDateTime end = ZonedDateTime.parse("2024-12-06T12:00:00Z"); + TimeExtents timeExtents = new TimeExtents.Builder() + .withEarliestTime(start) + .withLatestTime(end) + .build(); + CwmsIdTimeExtentsEntry expectedEntry = new CwmsIdTimeExtentsEntry.Builder() + .withId(id) + .withTimeExtents(timeExtents) + .build(); + + DTOMatch.assertMatch(expectedEntry, deserialized); + } +} diff --git a/cwms-data-api/src/test/java/cwms/cda/data/dto/TimeExtentsTest.java b/cwms-data-api/src/test/java/cwms/cda/data/dto/TimeExtentsTest.java new file mode 100644 index 000000000..f4617bc51 --- /dev/null +++ b/cwms-data-api/src/test/java/cwms/cda/data/dto/TimeExtentsTest.java @@ -0,0 +1,89 @@ +/* + * MIT License + * + * Copyright (c) 2024 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 cwms.cda.formatters.ContentType; +import cwms.cda.formatters.Formats; +import cwms.cda.helpers.DTOMatch; +import org.apache.commons.io.IOUtils; +import org.junit.jupiter.api.Test; + +import java.io.InputStream; +import java.nio.charset.StandardCharsets; +import java.time.ZonedDateTime; + +import static org.junit.jupiter.api.Assertions.assertAll; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNotNull; + +final class TimeExtentsTest { + + @Test + void createTimeExtents_allFieldsProvided_success() { + ZonedDateTime start = ZonedDateTime.parse("2024-12-06T00:00:00Z"); + ZonedDateTime end = ZonedDateTime.parse("2024-12-06T12:00:00Z"); + TimeExtents extents = new TimeExtents.Builder() + .withEarliestTime(start) + .withLatestTime(end) + .build(); + + assertAll( + () -> assertEquals(start, extents.getEarliestTime(), "Start Instant"), + () -> assertEquals(end, extents.getLatestTime(), "End Instant") + ); + } + + @Test + void createTimeExtents_serialize_roundtrip() { + ZonedDateTime start = ZonedDateTime.parse("2024-12-06T00:00:00Z"); + ZonedDateTime end = ZonedDateTime.parse("2024-12-06T12:00:00Z"); + TimeExtents extents = new TimeExtents.Builder() + .withEarliestTime(start) + .withLatestTime(end) + .build(); + + ContentType contentType = new ContentType(Formats.JSON); + String json = Formats.format(contentType, extents); + TimeExtents deserialized = Formats.parseContent(contentType, json, TimeExtents.class); + + DTOMatch.assertMatch(extents, deserialized); + } + + @Test + void createTimeExtents_deserialize() throws Exception { + InputStream resource = this.getClass().getResourceAsStream("/cwms/cda/data/dto/time_extents.json"); + assertNotNull(resource); + String json = IOUtils.toString(resource, StandardCharsets.UTF_8); + ContentType contentType = new ContentType(Formats.JSON); + TimeExtents deserialized = Formats.parseContent(contentType, json, TimeExtents.class); + ZonedDateTime start = ZonedDateTime.parse("2024-12-06T00:00:00Z"); + ZonedDateTime end = ZonedDateTime.parse("2024-12-06T12:00:00Z"); + TimeExtents expectedExtents = new TimeExtents.Builder() + .withEarliestTime(start) + .withLatestTime(end) + .build(); + + DTOMatch.assertMatch(expectedExtents, 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 f6e167604..16d588a8e 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,6 +24,8 @@ package cwms.cda.helpers; +import cwms.cda.data.dto.CwmsIdTimeExtentsEntry; +import cwms.cda.data.dto.TimeExtents; import cwms.cda.data.dto.location.kind.Lock; import cwms.cda.data.dto.CwmsDTOBase; import cwms.cda.data.dto.location.kind.GateChange; @@ -575,6 +577,20 @@ public static void assertMatch(LockLocationLevelRef first, LockLocationLevelRef ); } + public static void assertMatch(TimeExtents first, TimeExtents second) { + assertAll( + () -> assertEquals(first.getEarliestTime(), second.getEarliestTime(), "Start time does not match"), + () -> assertEquals(first.getLatestTime(), second.getLatestTime(), "End time does not match") + ); + } + + public static void assertMatch(CwmsIdTimeExtentsEntry first, CwmsIdTimeExtentsEntry second) { + assertAll( + () -> assertMatch(first.getId(), second.getId()), + () -> assertMatch(first.getTimeExtents(), second.getTimeExtents()) + ); + } + @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 101560c7e..81f88ecbf 100644 --- a/cwms-data-api/src/test/java/fixtures/CwmsDataApiSetupCallback.java +++ b/cwms-data-api/src/test/java/fixtures/CwmsDataApiSetupCallback.java @@ -13,7 +13,6 @@ import org.apache.commons.io.IOUtils; import mil.army.usace.hec.test.database.CwmsDatabaseContainer; -import mil.army.usace.hec.test.database.CwmsDatabaseContainers; import mil.army.usace.hec.test.database.TeamCityUtilities; import org.junit.jupiter.api.extension.AfterAllCallback; @@ -55,7 +54,7 @@ public class CwmsDataApiSetupCallback implements BeforeAllCallback,AfterAllCallb ); static final String CWMS_DB_IMAGE = System.getProperty("CDA.cwms.database.image", - "registry.hecdev.net/cwms/schema_installer:99.99.99.9-CDA_STAGING" + "registry.hecdev.net/cwms/schema_installer:99.99.99.11-CDA_STAGING" ); diff --git a/cwms-data-api/src/test/resources/cwms/cda/api/measurement.json b/cwms-data-api/src/test/resources/cwms/cda/api/measurement.json index 0c23d00c6..0a73c6838 100644 --- a/cwms-data-api/src/test/resources/cwms/cda/api/measurement.json +++ b/cwms-data-api/src/test/resources/cwms/cda/api/measurement.json @@ -18,7 +18,7 @@ "streamflow-measurement": { "gage-height": 5.5, "flow": 250.0, - "quality": "G" + "quality": "Good" }, "supplemental-streamflow-measurement": { "channel-flow": 300.0, diff --git a/cwms-data-api/src/test/resources/cwms/cda/api/measurements.json b/cwms-data-api/src/test/resources/cwms/cda/api/measurements.json index 3db3ce30b..7e238c1af 100644 --- a/cwms-data-api/src/test/resources/cwms/cda/api/measurements.json +++ b/cwms-data-api/src/test/resources/cwms/cda/api/measurements.json @@ -18,7 +18,7 @@ "streamflow-measurement": { "gage-height": 5.5, "flow": 250.0, - "quality": "G" + "quality": "Good" }, "supplemental-streamflow-measurement": { "channel-flow": 300.0, @@ -67,7 +67,7 @@ "streamflow-measurement": { "gage-height": 6.0, "flow": 275.0, - "quality": "F" + "quality": "Fair" }, "supplemental-streamflow-measurement": { "channel-flow": 320.0, diff --git a/cwms-data-api/src/test/resources/cwms/cda/data/dao/dbMeasurements.xml b/cwms-data-api/src/test/resources/cwms/cda/data/dao/dbMeasurements.xml index 3bd498e73..09c2f22eb 100644 --- a/cwms-data-api/src/test/resources/cwms/cda/data/dao/dbMeasurements.xml +++ b/cwms-data-api/src/test/resources/cwms/cda/data/dao/dbMeasurements.xml @@ -49,7 +49,7 @@ 2.4 200.0 - G + Good Some remarks2 diff --git a/cwms-data-api/src/test/resources/cwms/cda/data/dto/cwms_id_time_extents_entry.json b/cwms-data-api/src/test/resources/cwms/cda/data/dto/cwms_id_time_extents_entry.json new file mode 100644 index 000000000..dce84003b --- /dev/null +++ b/cwms-data-api/src/test/resources/cwms/cda/data/dto/cwms_id_time_extents_entry.json @@ -0,0 +1,10 @@ +{ + "id": { + "office-id": "SWT", + "name": "ARBU" + }, + "time-extents": { + "earliest-time": "2024-12-06T00:00:00Z", + "latest-time": "2024-12-06T12:00:00Z" + } +} diff --git a/cwms-data-api/src/test/resources/cwms/cda/data/dto/time_extents.json b/cwms-data-api/src/test/resources/cwms/cda/data/dto/time_extents.json new file mode 100644 index 000000000..bc5db12a4 --- /dev/null +++ b/cwms-data-api/src/test/resources/cwms/cda/data/dto/time_extents.json @@ -0,0 +1,4 @@ +{ + "earliest-time": "2024-12-06T00:00:00Z", + "latest-time": "2024-12-06T12:00:00Z" +} From 29d59ef20abe4acbf0cfcdb617d6e19332b06ccb Mon Sep 17 00:00:00 2001 From: Bryson Spilman Date: Fri, 13 Dec 2024 10:23:26 -0800 Subject: [PATCH 2/4] CWMVUE-611 - Updated TimeSeriesExtents to use builder to not confuse apis in extending TimeExtents. Code refactor. --- .../main/java/cwms/cda/data/dao/JooqDao.java | 10 +-- .../cwms/cda/data/dao/TimeSeriesDaoImpl.java | 13 ++-- .../java/cwms/cda/data/dto/TimeExtents.java | 8 +- .../cwms/cda/data/dto/TimeSeriesExtents.java | 75 +++++++++++-------- .../main/java/cwms/cda/helpers/DateUtils.java | 9 +++ .../cda/data/dto/TimeSeriesExtentsTest.java | 24 +++++- .../catalog/TimeseriesCatalogEntryTest.java | 9 ++- .../test/java/cwms/cda/helpers/DTOMatch.java | 10 +++ 8 files changed, 102 insertions(+), 56 deletions(-) diff --git a/cwms-data-api/src/main/java/cwms/cda/data/dao/JooqDao.java b/cwms-data-api/src/main/java/cwms/cda/data/dao/JooqDao.java index 832e5cd5b..31a74c56b 100644 --- a/cwms-data-api/src/main/java/cwms/cda/data/dao/JooqDao.java +++ b/cwms-data-api/src/main/java/cwms/cda/data/dao/JooqDao.java @@ -130,14 +130,8 @@ public static DSLContext getDslContext(Context ctx) { return retVal; } - protected static Timestamp buildTimestamp(Instant date) - { - Timestamp retval = null; - if(date != null) - { - retval = Timestamp.from(date); - } - return retval; +protected static Timestamp buildTimestamp(Instant date) { + return date != null ? Timestamp.from(date) : null; } public static DSLContext getDslContext(Connection connection, String officeId) { diff --git a/cwms-data-api/src/main/java/cwms/cda/data/dao/TimeSeriesDaoImpl.java b/cwms-data-api/src/main/java/cwms/cda/data/dao/TimeSeriesDaoImpl.java index 1a261cb5c..30adf193c 100644 --- a/cwms-data-api/src/main/java/cwms/cda/data/dao/TimeSeriesDaoImpl.java +++ b/cwms-data-api/src/main/java/cwms/cda/data/dao/TimeSeriesDaoImpl.java @@ -1,5 +1,6 @@ package cwms.cda.data.dao; +import cwms.cda.helpers.DateUtils; import static org.jooq.impl.DSL.asterisk; import static org.jooq.impl.DSL.countDistinct; import static org.jooq.impl.DSL.field; @@ -557,12 +558,12 @@ public Catalog getTimeSeriesCatalog(String page, int pageSize, CatalogRequestPar } if (params.isIncludeExtents()) { - TimeSeriesExtents extents = - new TimeSeriesExtents(row.get(AV_TS_EXTENTS_UTC.VERSION_TIME), - row.get(AV_TS_EXTENTS_UTC.EARLIEST_TIME), - row.get(AV_TS_EXTENTS_UTC.LATEST_TIME), - row.get(AV_TS_EXTENTS_UTC.LAST_UPDATE) - ); + TimeSeriesExtents extents = new TimeSeriesExtents.Builder() + .withEarliestTime(DateUtils.toZdt(row.get(AV_TS_EXTENTS_UTC.EARLIEST_TIME))) + .withLatestTime(DateUtils.toZdt(row.get(AV_TS_EXTENTS_UTC.LATEST_TIME))) + .withLastUpdate(DateUtils.toZdt(row.get(AV_TS_EXTENTS_UTC.LAST_UPDATE))) + .withVersionTime(DateUtils.toZdt(row.get(AV_TS_EXTENTS_UTC.VERSION_TIME))) + .build(); tsIdExtentMap.get(officeTsId).withExtent(extents); } }); diff --git a/cwms-data-api/src/main/java/cwms/cda/data/dto/TimeExtents.java b/cwms-data-api/src/main/java/cwms/cda/data/dto/TimeExtents.java index e3be79734..077b520c4 100644 --- a/cwms-data-api/src/main/java/cwms/cda/data/dto/TimeExtents.java +++ b/cwms-data-api/src/main/java/cwms/cda/data/dto/TimeExtents.java @@ -25,7 +25,9 @@ import com.fasterxml.jackson.annotation.JsonFormat; import com.fasterxml.jackson.annotation.JsonFormat.Shape; +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; @@ -51,7 +53,7 @@ public class TimeExtents extends CwmsDTOBase { @JsonFormat(shape = Shape.STRING) private final ZonedDateTime latestTime; - public TimeExtents(Builder builder) { + TimeExtents(Builder builder) { this.earliestTime = builder.earliestTime; this.latestTime = builder.latestTime; } @@ -68,12 +70,12 @@ public static class Builder { private ZonedDateTime earliestTime; private ZonedDateTime latestTime; - public TimeExtents.Builder withEarliestTime(ZonedDateTime start) { + public Builder withEarliestTime(ZonedDateTime start) { this.earliestTime = start; return this; } - public TimeExtents.Builder withLatestTime(ZonedDateTime end) { + public Builder withLatestTime(ZonedDateTime end) { this.latestTime = end; return this; } diff --git a/cwms-data-api/src/main/java/cwms/cda/data/dto/TimeSeriesExtents.java b/cwms-data-api/src/main/java/cwms/cda/data/dto/TimeSeriesExtents.java index 7de38d579..940ff65f1 100644 --- a/cwms-data-api/src/main/java/cwms/cda/data/dto/TimeSeriesExtents.java +++ b/cwms-data-api/src/main/java/cwms/cda/data/dto/TimeSeriesExtents.java @@ -25,9 +25,9 @@ import com.fasterxml.jackson.annotation.JsonFormat; import com.fasterxml.jackson.annotation.JsonFormat.Shape; -import com.fasterxml.jackson.annotation.JsonPropertyOrder; -import com.fasterxml.jackson.annotation.JsonRootName; +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 io.swagger.v3.oas.annotations.media.Schema; @@ -35,53 +35,64 @@ import java.time.ZoneId; import java.time.ZonedDateTime; -@JsonRootName("extents") -@Schema(description = "TimeSeries extent information") -@JsonPropertyOrder(alphabetic = true) +@JsonInclude(JsonInclude.Include.NON_NULL) +@JsonDeserialize(builder = TimeSeriesExtents.Builder.class) @JsonNaming(PropertyNamingStrategies.KebabCaseStrategy.class) +@Schema(description = "TimeSeries extent information") public class TimeSeriesExtents extends TimeExtents { @Schema(description = "TimeSeries version to which this extent information applies") @JsonFormat(shape = Shape.STRING) - ZonedDateTime versionTime; + private final ZonedDateTime versionTime; @Schema(description = "Last update in the timeseries") @JsonFormat(shape = Shape.STRING) - ZonedDateTime lastUpdate; + private final ZonedDateTime lastUpdate; - @SuppressWarnings("unused") // required so JAXB can initialize and marshal - private TimeSeriesExtents() { - super(new Builder()); + private TimeSeriesExtents(Builder builder) { + super(builder); + this.versionTime = builder.versionTime; + this.lastUpdate = builder.lastUpdate; } - public TimeSeriesExtents(final ZonedDateTime versionTime, final ZonedDateTime earliestTime, - final ZonedDateTime latestTime, final ZonedDateTime lastUpdateTime) { - super(new TimeExtents.Builder() - .withEarliestTime(earliestTime) - .withLatestTime(latestTime)); - this.versionTime = versionTime; - this.lastUpdate = lastUpdateTime; + public ZonedDateTime getVersionTime() { + return versionTime; } - public TimeSeriesExtents(final Timestamp versionTime, final Timestamp earliestTime, - final Timestamp latestTime, final Timestamp lastUpdateTime) { - this(toZdt(versionTime), toZdt(earliestTime), toZdt(latestTime), toZdt(lastUpdateTime)); + public ZonedDateTime getLastUpdate() { + return lastUpdate; } - private static ZonedDateTime toZdt(final Timestamp time) { - if (time != null) { - return ZonedDateTime.ofInstant(time.toInstant(), ZoneId.of("UTC")); - } else { - return null; - } + public static Builder builder() { + return new Builder(); } - public ZonedDateTime getVersionTime() { - return this.versionTime; - } + public static class Builder extends TimeExtents.Builder { + private ZonedDateTime versionTime; + private ZonedDateTime lastUpdate; - public ZonedDateTime getLastUpdate() { - return this.lastUpdate; - } + @Override + public Builder withLatestTime(ZonedDateTime end) { + return (Builder) super.withLatestTime(end); + } + + @Override + public Builder withEarliestTime(ZonedDateTime start) { + return (Builder) super.withEarliestTime(start); + } + + public Builder withVersionTime(ZonedDateTime versionTime) { + this.versionTime = versionTime; + return this; + } + public Builder withLastUpdate(ZonedDateTime lastUpdate) { + this.lastUpdate = lastUpdate; + return this; + } + + public TimeSeriesExtents build() { + return new TimeSeriesExtents(this); + } + } } diff --git a/cwms-data-api/src/main/java/cwms/cda/helpers/DateUtils.java b/cwms-data-api/src/main/java/cwms/cda/helpers/DateUtils.java index 1178fba72..9c2799431 100644 --- a/cwms-data-api/src/main/java/cwms/cda/helpers/DateUtils.java +++ b/cwms-data-api/src/main/java/cwms/cda/helpers/DateUtils.java @@ -1,6 +1,7 @@ package cwms.cda.helpers; import com.google.common.flogger.FluentLogger; +import java.sql.Timestamp; import java.time.Duration; import java.time.Instant; import java.time.LocalDate; @@ -171,4 +172,12 @@ private static ZonedDateTime parseUserDuration(String text, ZonedDateTime now) { Duration duration = Duration.parse(text); return now.plus(duration); } + + public static ZonedDateTime toZdt(final Timestamp time) { + if (time != null) { + return ZonedDateTime.ofInstant(time.toInstant(), ZoneId.of("UTC")); + } else { + return null; + } + } } diff --git a/cwms-data-api/src/test/java/cwms/cda/data/dto/TimeSeriesExtentsTest.java b/cwms-data-api/src/test/java/cwms/cda/data/dto/TimeSeriesExtentsTest.java index 2b8549f37..0c7984ba4 100644 --- a/cwms-data-api/src/test/java/cwms/cda/data/dto/TimeSeriesExtentsTest.java +++ b/cwms-data-api/src/test/java/cwms/cda/data/dto/TimeSeriesExtentsTest.java @@ -1,5 +1,6 @@ package cwms.cda.data.dto; +import cwms.cda.helpers.DTOMatch; import static org.junit.jupiter.api.Assertions.assertNotNull; import static org.junit.jupiter.api.Assertions.assertTrue; @@ -24,6 +25,19 @@ void test_serialize_json() throws JsonProcessingException { assertTrue(result.contains("last-update"), "should contain kebab-case last-update"); } + @Test + void test_roundtrip_serialization() throws JsonProcessingException { + TimeSeriesExtents extents = buildTimeSeriesExtents(); + + ObjectMapper om = JsonV2.buildObjectMapper(); + + String result = om.writeValueAsString(extents); + assertNotNull(result); + + TimeSeriesExtents deserialized = om.readValue(result, TimeSeriesExtents.class); + DTOMatch.assertMatch(extents, deserialized); + } + private TimeSeriesExtents buildTimeSeriesExtents() { ZonedDateTime version = ZonedDateTime.parse("2019-01-01T00:00:00Z"); @@ -31,8 +45,12 @@ private TimeSeriesExtents buildTimeSeriesExtents() { ZonedDateTime latest = ZonedDateTime.parse("2021-01-01T00:00:00Z"); ZonedDateTime updated = ZonedDateTime.parse("2022-01-01T00:00:00Z"); - TimeSeriesExtents retval = new TimeSeriesExtents(version, earliest, latest, updated); - - return retval; + return new TimeSeriesExtents.Builder() + .withEarliestTime(earliest) + .withEarliestTime(earliest) + .withLatestTime(latest) + .withVersionTime(version) + .withLastUpdate(updated) + .build(); } } diff --git a/cwms-data-api/src/test/java/cwms/cda/data/dto/catalog/TimeseriesCatalogEntryTest.java b/cwms-data-api/src/test/java/cwms/cda/data/dto/catalog/TimeseriesCatalogEntryTest.java index 4f265e47c..03298ffca 100644 --- a/cwms-data-api/src/test/java/cwms/cda/data/dto/catalog/TimeseriesCatalogEntryTest.java +++ b/cwms-data-api/src/test/java/cwms/cda/data/dto/catalog/TimeseriesCatalogEntryTest.java @@ -109,10 +109,11 @@ private TimeseriesCatalogEntry buildEntry() .units("m") .interval("0").intervalOffset(-2147483648L) .timeZone("US/Central") - .withExtent(new TimeSeriesExtents(null, - ZonedDateTime.parse("2017-07-27T05:00:00Z"), - ZonedDateTime.parse("2017-11-24T22:30:00Z"), - ZonedDateTime.parse("2017-11-24T22:30:00Z"))); + .withExtent(new TimeSeriesExtents.Builder() + .withEarliestTime(ZonedDateTime.parse("2017-07-27T05:00:00Z")) + .withLatestTime(ZonedDateTime.parse("2017-11-24T22:30:00Z")) + .withLastUpdate(ZonedDateTime.parse("2017-11-24T22:30:00Z")) + .build()); return builder .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 16d588a8e..49baac886 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 @@ -26,6 +26,7 @@ import cwms.cda.data.dto.CwmsIdTimeExtentsEntry; import cwms.cda.data.dto.TimeExtents; +import cwms.cda.data.dto.TimeSeriesExtents; import cwms.cda.data.dto.location.kind.Lock; import cwms.cda.data.dto.CwmsDTOBase; import cwms.cda.data.dto.location.kind.GateChange; @@ -577,6 +578,15 @@ public static void assertMatch(LockLocationLevelRef first, LockLocationLevelRef ); } + public static void assertMatch(TimeSeriesExtents first, TimeSeriesExtents second) { + assertAll( + () -> assertEquals(first.getLastUpdate(), second.getLastUpdate(), "Last Update time does not match"), + () -> assertEquals(first.getVersionTime(), second.getVersionTime(), "Version time does not match"), + () -> assertEquals(first.getEarliestTime(), second.getEarliestTime(), "Earliest time does not match"), + () -> assertEquals(first.getLatestTime(), second.getLatestTime(), "Latest time does not match") + ); + } + public static void assertMatch(TimeExtents first, TimeExtents second) { assertAll( () -> assertEquals(first.getEarliestTime(), second.getEarliestTime(), "Start time does not match"), From 9f7984abb86b8b1725a75be9eb4a20e46e032759 Mon Sep 17 00:00:00 2001 From: Bryson Spilman <87150647+rma-bryson@users.noreply.github.com> Date: Fri, 13 Dec 2024 13:04:01 -0800 Subject: [PATCH 3/4] Fix Indent Co-authored-by: Mike Neilson --- cwms-data-api/src/main/java/cwms/cda/data/dao/JooqDao.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cwms-data-api/src/main/java/cwms/cda/data/dao/JooqDao.java b/cwms-data-api/src/main/java/cwms/cda/data/dao/JooqDao.java index 31a74c56b..f5a316381 100644 --- a/cwms-data-api/src/main/java/cwms/cda/data/dao/JooqDao.java +++ b/cwms-data-api/src/main/java/cwms/cda/data/dao/JooqDao.java @@ -130,7 +130,7 @@ public static DSLContext getDslContext(Context ctx) { return retVal; } -protected static Timestamp buildTimestamp(Instant date) { + protected static Timestamp buildTimestamp(Instant date) { return date != null ? Timestamp.from(date) : null; } From e940cf3e8aab9dd8e598aec06b010ef6a1dbe855 Mon Sep 17 00:00:00 2001 From: Bryson Spilman Date: Fri, 13 Dec 2024 13:10:20 -0800 Subject: [PATCH 4/4] CWMVUE-611 - Fixed missing import --- .../src/test/java/fixtures/CwmsDataApiSetupCallback.java | 1 + 1 file changed, 1 insertion(+) diff --git a/cwms-data-api/src/test/java/fixtures/CwmsDataApiSetupCallback.java b/cwms-data-api/src/test/java/fixtures/CwmsDataApiSetupCallback.java index 81f88ecbf..452895a1a 100644 --- a/cwms-data-api/src/test/java/fixtures/CwmsDataApiSetupCallback.java +++ b/cwms-data-api/src/test/java/fixtures/CwmsDataApiSetupCallback.java @@ -13,6 +13,7 @@ import org.apache.commons.io.IOUtils; import mil.army.usace.hec.test.database.CwmsDatabaseContainer; +import mil.army.usace.hec.test.database.CwmsDatabaseContainers; import mil.army.usace.hec.test.database.TeamCityUtilities; import org.junit.jupiter.api.extension.AfterAllCallback;