From 8f916dd82c5e0da0707597493b2ad9d46caca228 Mon Sep 17 00:00:00 2001 From: Bryson Spilman Date: Fri, 24 Jul 2026 15:07:56 -0700 Subject: [PATCH] CDA-106 - Adds UUID support for measurements --- .../main/java/cwms/cda/api/Controllers.java | 1 + .../cwms/cda/api/MeasurementController.java | 27 ++-- .../cwms/cda/data/dao/MeasurementDao.java | 60 ++++---- .../cwms/cda/data/dao/TimeSeriesDaoImpl.java | 32 ++--- .../TimeSeriesIdentifierDescriptorDao.java | 4 +- .../cda/data/dto/measurement/Measurement.java | 47 +++++-- .../dto/measurement/MeasurementLegacy.java | 42 ++++++ .../java/cwms/cda/formatters/json/JsonV1.java | 12 +- .../java/cwms/cda/formatters/json/JsonV2.java | 1 + .../cda/api/MeasurementControllerTestIT.java | 131 ++++++++++++++++-- .../cwms/cda/data/dao/MeasurementDaoTest.java | 8 +- .../cda/data/dao/MeasurementDaoTestIT.java | 24 ++-- .../cwms/cda/data/dto/MeasurementTest.java | 107 ++++++++++++-- .../test/java/cwms/cda/helpers/DTOMatch.java | 2 +- .../cda/api/measurements_with_meas_id.json | 100 +++++++++++++ .../data/dto/measurement_with_meas_id.json | 49 +++++++ ...ment.json => measurement_with_number.json} | 0 ...nts.json => measurements_with_number.json} | 0 gradle/libs.versions.toml | 2 +- 19 files changed, 539 insertions(+), 110 deletions(-) create mode 100644 cwms-data-api/src/main/java/cwms/cda/data/dto/measurement/MeasurementLegacy.java create mode 100644 cwms-data-api/src/test/resources/cwms/cda/api/measurements_with_meas_id.json create mode 100644 cwms-data-api/src/test/resources/cwms/cda/data/dto/measurement_with_meas_id.json rename cwms-data-api/src/test/resources/cwms/cda/data/dto/{measurement.json => measurement_with_number.json} (100%) rename cwms-data-api/src/test/resources/cwms/cda/data/dto/{measurements.json => measurements_with_number.json} (100%) diff --git a/cwms-data-api/src/main/java/cwms/cda/api/Controllers.java b/cwms-data-api/src/main/java/cwms/cda/api/Controllers.java index ed05a37267..daf0948656 100644 --- a/cwms-data-api/src/main/java/cwms/cda/api/Controllers.java +++ b/cwms-data-api/src/main/java/cwms/cda/api/Controllers.java @@ -160,6 +160,7 @@ public final class Controllers { public static final String INCLUDE_ALIASES = "include-aliases"; public static final String MIN_NUMBER = "min-number"; public static final String MAX_NUMBER = "max-number"; + public static final String MEASUREMENT_ID = "measurement-id"; public static final String MIN_HEIGHT = "min-height"; public static final String MAX_HEIGHT = "max-height"; public static final String MIN_FLOW = "min-flow"; diff --git a/cwms-data-api/src/main/java/cwms/cda/api/MeasurementController.java b/cwms-data-api/src/main/java/cwms/cda/api/MeasurementController.java index 5e8254a247..b5d8116dcf 100644 --- a/cwms-data-api/src/main/java/cwms/cda/api/MeasurementController.java +++ b/cwms-data-api/src/main/java/cwms/cda/api/MeasurementController.java @@ -37,6 +37,7 @@ import static cwms.cda.api.Controllers.MAX_FLOW; import static cwms.cda.api.Controllers.MAX_HEIGHT; import static cwms.cda.api.Controllers.MAX_NUMBER; +import static cwms.cda.api.Controllers.MEASUREMENT_ID; import static cwms.cda.api.Controllers.MIN_FLOW; import static cwms.cda.api.Controllers.MIN_HEIGHT; import static cwms.cda.api.Controllers.MIN_NUMBER; @@ -48,6 +49,7 @@ import static cwms.cda.api.Controllers.TIMEZONE; import static cwms.cda.api.Controllers.TIME_FORMAT_DESC; import static cwms.cda.api.Controllers.UNIT_SYSTEM; +import static cwms.cda.api.Controllers.queryParamAsClass; import static cwms.cda.api.Controllers.queryParamAsDouble; import static cwms.cda.api.Controllers.queryParamAsInstant; import static cwms.cda.api.Controllers.requiredParam; @@ -63,6 +65,7 @@ import cwms.cda.data.dao.MeasurementDao; import cwms.cda.data.dto.StatusResponse; import cwms.cda.data.dto.measurement.Measurement; +import cwms.cda.data.dto.measurement.MeasurementLegacy; import cwms.cda.formatters.ContentType; import cwms.cda.formatters.Formats; import io.javalin.apibuilder.CrudHandler; @@ -103,8 +106,9 @@ private Timer.Context markAndTime(String subject) { queryParams = { @OpenApiParam(name = OFFICE_MASK, description = "Office id mask for filtering measurements. Use null to retrieve measurements for all offices."), @OpenApiParam(name = ID_MASK, description = "Location id mask for filtering measurements. Use null to retrieve measurements for all locations."), - @OpenApiParam(name = MIN_NUMBER, description = "Minimum measurement number-id for filtering measurements."), - @OpenApiParam(name = MAX_NUMBER, description = "Maximum measurement number-id for filtering measurements."), + @OpenApiParam(name = MEASUREMENT_ID, description = "Measurement ID filter. Supports UUIDs (and optionally integer IDs). This filter has precedence over min-number and max-number filters."), + @OpenApiParam(name = MIN_NUMBER, type = Integer.class, description = "Minimum measurement number-id for filtering measurements. Only applies to integer measurement IDs."), + @OpenApiParam(name = MAX_NUMBER, type = Integer.class, description = "Maximum measurement number-id for filtering measurements. Only applies to integer measurement IDs."), @OpenApiParam(name = BEGIN, description = "The start of the time window to delete. " + TIME_FORMAT_DESC + " A null value is treated as an unbounded start."), @OpenApiParam(name = END, description = "The end of the time window to delete." + @@ -128,8 +132,9 @@ private Timer.Context markAndTime(String subject) { }, responses = { @OpenApiResponse(status = "200", content = { - @OpenApiContent(isArray = true, type = Formats.JSONV1, from = Measurement.class), - @OpenApiContent(isArray = true, type = Formats.JSON, from = Measurement.class) + @OpenApiContent(isArray = true, type = Formats.JSONV2, from = Measurement.class), + @OpenApiContent(isArray = true, type = Formats.JSONV1, from = MeasurementLegacy.class), + @OpenApiContent(isArray = true, type = Formats.JSON, from = MeasurementLegacy.class) }) }, description = "Returns matching measurement data.", @@ -144,6 +149,7 @@ public void getAll(@NotNull Context ctx) { Instant maxDate = queryParamAsInstant(ctx, END); String minNum = ctx.queryParam(MIN_NUMBER); String maxNum = ctx.queryParam(MAX_NUMBER); + String measurementId = ctx.queryParam(MEASUREMENT_ID); Number minHeight = queryParamAsDouble(ctx, MIN_HEIGHT); Number maxHeight = queryParamAsDouble(ctx, MAX_HEIGHT); Number minFlow = queryParamAsDouble(ctx, MIN_FLOW); @@ -154,7 +160,7 @@ public void getAll(@NotNull Context ctx) { DSLContext dsl = getDslContext(ctx); MeasurementDao dao = new MeasurementDao(dsl); List measurements = dao.retrieveMeasurements(officeId, locationId, minDate, maxDate, unitSystem, - minHeight, maxHeight, minFlow, maxFlow, minNum, maxNum, agency, quality); + minHeight, maxHeight, minFlow, maxFlow, minNum, maxNum, agency, quality, measurementId); String formatHeader = ctx.header(Header.ACCEPT); ContentType contentType = Formats.parseHeader(formatHeader, Measurement.class); ctx.contentType(contentType.toString()); @@ -182,8 +188,9 @@ public void getOne(@NotNull Context ctx, @NotNull String locationId) { @OpenApi( requestBody = @OpenApiRequestBody( content = { - @OpenApiContent(isArray = true, from = Measurement.class, type = Formats.JSONV1), - @OpenApiContent(isArray = true, from = Measurement.class, type = Formats.JSON) + @OpenApiContent(isArray = true, from = Measurement.class, type = Formats.JSONV2), + @OpenApiContent(isArray = true, from = MeasurementLegacy.class, type = Formats.JSONV1), + @OpenApiContent(isArray = true, from = MeasurementLegacy.class, type = Formats.JSON) }, required = true), queryParams = { @@ -234,8 +241,8 @@ public void update(@NotNull Context ctx, @NotNull String locationId) { + "to be used if the format of the " + BEGIN + "and " + END + " parameters do not include offset or time zone information. " + "Defaults to UTC."), - @OpenApiParam(name = MIN_NUMBER, description = "Specifies the min number-id of the measurement to delete."), - @OpenApiParam(name = MAX_NUMBER, description = "Specifies the max number-id of the measurement to delete."), + @OpenApiParam(name = MIN_NUMBER, type = Integer.class, description = "Specifies the min number-id of the measurement to delete. Only applies to integer measurement IDs."), + @OpenApiParam(name = MAX_NUMBER, type = Integer.class, description = "Specifies the max number-id of the measurement to delete. Only applies to integer measurement IDs."), }, description = "Delete an existing measurement.", method = HttpMethod.DELETE, @@ -255,7 +262,7 @@ public void delete(@NotNull Context ctx, @NotNull String locationId) { try (Timer.Context ignored = markAndTime(DELETE)) { DSLContext dsl = getDslContext(ctx); MeasurementDao dao = new MeasurementDao(dsl); - dao.deleteMeasurements(officeId, locationId, minDate, maxDate,minNum, maxNum); + dao.deleteMeasurements(officeId, locationId, minDate, maxDate, minNum, maxNum, null); StatusResponse re = new StatusResponse(officeId, "Measurement successfully deleted for specified location-id.", locationId); ctx.status(HttpServletResponse.SC_OK).json(re); } 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 679f3e6bb3..60b8926a0e 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 @@ -44,7 +44,9 @@ import com.fasterxml.jackson.dataformat.xml.annotation.JacksonXmlRootElement; import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule; import cwms.cda.api.enums.UnitSystem; +import cwms.cda.api.errors.FieldException; import cwms.cda.api.errors.NotFoundException; +import cwms.cda.data.dto.CwmsDTOPaginated; import cwms.cda.data.dto.CwmsId; import cwms.cda.data.dto.CwmsIdTimeExtentsEntry; import cwms.cda.data.dto.TimeExtents; @@ -72,6 +74,7 @@ import org.jooq.impl.DSL; 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; @@ -86,33 +89,31 @@ public MeasurementDao(DSLContext dsl) { super(dsl); } - /** - * Retrieve a list of measurements - * - * @param officeId - the office id - * @param locationId - the location id for filtering - * @param unitSystem - the unit system to use for the returned data - * @return a list of measurements - */ public List retrieveMeasurements(String officeId, String locationId, Instant minDateMask, Instant maxDateMask, String unitSystem, Number minHeight, Number maxHeight, Number minFlow, Number maxFlow, String minNum, String maxNum, - String agencies, String qualities) { + String agencies, String qualities, String measurementId) { return connectionResult(dsl, conn -> { setOffice(conn, officeId); Timestamp minTimestamp = buildTimestamp(minDateMask); Timestamp maxTimestamp = buildTimestamp(maxDateMask); - return retrieveMeasurementsJooq(conn, officeId, locationId, unitSystem, minHeight, maxHeight, minFlow, maxFlow, minNum, maxNum, agencies, qualities, minTimestamp, maxTimestamp); + return retrieveMeasurementsJooq(conn, officeId, locationId, unitSystem, minHeight, maxHeight, minFlow, maxFlow, minNum, maxNum, agencies, qualities, minTimestamp, maxTimestamp, measurementId); }); } - 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()) { + 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, String measurementId) throws JsonProcessingException { + String xml; + if (measurementId == null) { + 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); + } else { + xml = CWMS_STREAM_PACKAGE.call_RETRIEVE_STREAMFLOW_MEAS_XML_BY_ID(DSL.using(conn).configuration(), locationId, measurementId, unitSystem, minTimestamp, maxTimestamp, + minHeight, maxHeight, minFlow, maxFlow, agencies, qualities, "UTC", officeId); + } + List measurements = fromDbXml(xml); + if (measurements.isEmpty()) { throw new NotFoundException("No measurements found."); } - return retVal; + return measurements; } /** @@ -142,15 +143,21 @@ private void storeMeasurementsJooq(Connection conn, List measuremen * @param minNum */ public void deleteMeasurements(String officeId, String locationId, Instant minDateMask, Instant maxDateMask, String minNum, - String maxNum) { + String maxNum, String measurementId) { connection(dsl, conn -> { setOffice(conn, officeId); 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); + verifyMeasurementsExists(conn, officeId, locationId, minNum, maxNum, measurementId); + String minNumF = minNum; + String maxNumF = maxNum; + if(measurementId != null) { + minNumF = null; + maxNumF = null; + } + CWMS_STREAM_PACKAGE.call_DELETE_STREAMFLOW_MEAS(DSL.using(conn).configuration(), locationId, "EN", minTimestamp, maxTimestamp, + null, null, null, null, minNumF, maxNumF, null, null, timeZoneId, officeId); }); } @@ -181,12 +188,9 @@ public List retrieveMeasurementTimeExtentsMap(String off .build()); } - private void verifyMeasurementsExists(Connection conn, String officeId, String locationId, String minNum, String maxNum) throws JsonProcessingException { + private void verifyMeasurementsExists(Connection conn, String officeId, String locationId, String minNum, String maxNum, String measurementId) throws JsonProcessingException { List measurements = retrieveMeasurementsJooq(conn, officeId, locationId, UnitSystem.EN.toString(), - 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 + "."); - } + null, null, null, null, minNum, maxNum, null, null, null, null, measurementId); } static List fromDbXml(String xml) throws JsonProcessingException { @@ -207,7 +211,7 @@ static Measurement fromJooqMeasurementRecord(STREAMFLOW_MEAS2_T record) { .withName(locationTemplate.getLocationId()) .withOfficeId(locationTemplate.getOfficeId()) .build()) - .withNumber(record.getMEAS_NUMBER()) + .withMeasurementId(record.getMEAS_NUMBER()) .withAgency(record.getAGENCY_ID()) .withParty(record.getPARTY()) .withUsed(parseBool(record.getUSED())) @@ -270,7 +274,7 @@ static MeasurementXmlDto convertMeasurementToXmlDto(Measurement meas) .withHeightUnit(meas.getHeightUnit()) .withInstant(meas.getInstant()) .withLocationId(meas.getLocationId()) - .withNumber(meas.getNumber()) + .withNumber(meas.getMeasurementId()) .withOfficeId(meas.getOfficeId()) .withParty(meas.getParty()) .withTempUnit(meas.getTempUnit()) @@ -300,7 +304,7 @@ static Measurement convertMeasurementFromXmlDto(MeasurementXmlDto dto) { .withName(dto.getLocationId()) .withOfficeId(dto.getOfficeId()) .build()) - .withNumber(dto.getNumber()) + .withMeasurementId(dto.getNumber()) .withParty(dto.getParty()) .withTempUnit(dto.getTempUnit()) .withUsed(dto.isUsed()) 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 0a34c2b219..43174d531e 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 @@ -246,11 +246,11 @@ public TimeSeriesVersions getTimeSeriesVersions(String cursor, int pageSize, Str throw new NotFoundException("Could not find time series for identifier: " + name); } - BigDecimal tsCode = tsRecord.get(AV_CWMS_TS_ID2.TS_CODE); + Long tsCode = tsRecord.get(AV_CWMS_TS_ID2.TS_CODE); String officeId = tsRecord.get(AV_CWMS_TS_ID2.DB_OFFICE_ID); String tsId = tsRecord.get(AV_CWMS_TS_ID2.CWMS_TS_ID); - Condition extentsCondition = AV_TS_EXTENTS_UTC.TS_CODE.coerce(BigDecimal.class).eq(tsCode); + Condition extentsCondition = AV_TS_EXTENTS_UTC.TS_CODE.coerce(Long.class).eq(tsCode); if (begin != null) { extentsCondition = extentsCondition.and(AV_TS_EXTENTS_UTC.VERSION_TIME.ge(Timestamp.from(begin))); } @@ -687,7 +687,7 @@ protected TimeSeries getRequestedTimeSeriesLegacy(String page, int pageSize, AV_CWMS_TS_ID2.DB_OFFICE_ID.eq(valid.field("office_id", String.class)) .and(AV_CWMS_TS_ID2.TS_CODE.eq(valid.field("tscode", - BigDecimal.class))) + Long.class))) .and(AV_CWMS_TS_ID2.ALIASED_ITEM.isNull()) ); @@ -1045,7 +1045,7 @@ private DirectReadMetadata fetchRequestedTimeSeriesMetadataRecord(DSLContext met .from(valid) .leftOuterJoin(tsIdView) .on(tsIdView.DB_OFFICE_ID.eq(valid.field("office_id", String.class)) - .and(tsIdView.TS_CODE.eq(valid.field("tscode", BigDecimal.class)))); + .and(tsIdView.TS_CODE.eq(valid.field("tscode", Long.class)))); logger.atFine().log("%s", lazy(() -> metadataQuery.getSQL(ParamType.INLINED))); @@ -1620,7 +1620,7 @@ public Catalog getTimeSeriesCatalog(String page, int pageSize, CatalogRequestPar List whereConditions = buildWhereConditions(params); List pagingConditions = buildPagingConditions(cwmsTsIdFields, cursorOffice, cursorTsId); CommonTableExpression limiter = buildWithClause(cwmsTsIdFields, params, whereConditions, pagingConditions, pageSize, false); - Field limiterCode = limiter.field(cwmsTsIdFields.getTsCode()); + Field limiterCode = limiter.field(cwmsTsIdFields.getTsCode()); SelectOnConditionStep tmpQuery = dsl.with(limiter) .select(pageEntryFields) .from(limiter) @@ -2086,7 +2086,7 @@ public List findMostRecentsInRange(String office, List tsId // helper subquery for SELECT ts_code FROM base_ids - Select> tsCodeSubquery = select(baseIds.field(AV_CWMS_TS_ID2.TS_CODE)) + Select> tsCodeSubquery = select(baseIds.field(AV_CWMS_TS_ID2.TS_CODE)) .from(baseIds); // references to appropriate year tables, current year and past year @@ -2096,12 +2096,12 @@ public List findMostRecentsInRange(String office, List tsId Select prevYearSelect = select(asterisk()) .from(AT_TSV_PREV_YEAR_TABLE) .where(field(name(AT_TSV_PREV_YEAR_TABLE.getName(), DATE_TIME), java.sql.Date.class).between(startDate, endDate)) - .and(field(name(AT_TSV_PREV_YEAR_TABLE.getName(), TS_CODE), BigDecimal.class).in(tsCodeSubquery)); + .and(field(name(AT_TSV_PREV_YEAR_TABLE.getName(), TS_CODE), Long.class).in(tsCodeSubquery)); Select currYearSelect = select(asterisk()) .from(AT_TSV_CURR_YEAR_TABLE) .where(field(name(AT_TSV_CURR_YEAR_TABLE.getName(), DATE_TIME), java.sql.Date.class).between(startDate, endDate)) - .and(field(name(AT_TSV_CURR_YEAR_TABLE.getName(), TS_CODE), BigDecimal.class).in(tsCodeSubquery)); + .and(field(name(AT_TSV_CURR_YEAR_TABLE.getName(), TS_CODE), Long.class).in(tsCodeSubquery)); // union tables if start and end date are not in the same year Select combinedSelect = (year1 == year2) @@ -2237,7 +2237,7 @@ public List findRecentsInRange(String office, String categoryId, St // helper subquery for SELECT ts_code FROM base_ids - Select> tsCodeSubquery = select(baseIds.field(AV_CWMS_TS_ID2.TS_CODE)) + Select> tsCodeSubquery = select(baseIds.field(AV_CWMS_TS_ID2.TS_CODE)) .from(baseIds); // references to appropriate year tables, current year and past year @@ -2247,12 +2247,12 @@ public List findRecentsInRange(String office, String categoryId, St Select prevYearSelect = select(asterisk()) .from(AT_TSV_PREV_YEAR_TABLE) .where(field(name(AT_TSV_PREV_YEAR_TABLE.getName(), DATE_TIME), java.sql.Date.class).between(startDate, endDate)) - .and(field(name(AT_TSV_PREV_YEAR_TABLE.getName(), TS_CODE), BigDecimal.class).in(tsCodeSubquery)); + .and(field(name(AT_TSV_PREV_YEAR_TABLE.getName(), TS_CODE), Long.class).in(tsCodeSubquery)); Select currYearSelect = select(asterisk()) .from(AT_TSV_CURR_YEAR_TABLE) .where(field(name(AT_TSV_CURR_YEAR_TABLE.getName(), DATE_TIME), java.sql.Date.class).between(startDate, endDate)) - .and(field(name(AT_TSV_CURR_YEAR_TABLE.getName(), TS_CODE), BigDecimal.class).in(tsCodeSubquery)); + .and(field(name(AT_TSV_CURR_YEAR_TABLE.getName(), TS_CODE), Long.class).in(tsCodeSubquery)); // union tables if start and end date are not in the same year Select combinedSelect = (year1 == year2) @@ -2291,7 +2291,7 @@ public List findRecentsInRange(String office, String categoryId, St ) .from(tsvLimited) .join(baseIds).on(field(name(baseIds.getName(), TS_CODE), BigDecimal.class).equal(tsvLimitedTsCode)) - .join(AV_CWMS_TS_ID2).on(field(name(baseIds.getName(), TS_CODE), BigDecimal.class).equal(AV_CWMS_TS_ID2.TS_CODE)) + .join(AV_CWMS_TS_ID2).on(field(name(baseIds.getName(), TS_CODE), Long.class).equal(AV_CWMS_TS_ID2.TS_CODE)) .join(AT_TS_EXTENTS_TABLE).on( AT_TS_EXTENTS_TS_CODE.equal(tsvLimitedTsCode) .and(AT_TS_EXTENTS_VERSION_TIME @@ -2477,7 +2477,7 @@ private void store(DSLContext dslContext, String officeId, String tsId, String u formatBool(createAsLrts)); } - protected BigDecimal retrieveTsCode(String tsId) { + protected Long retrieveTsCode(String tsId) { return dsl.select(AV_CWMS_TS_ID2.TS_CODE) .from(AV_CWMS_TS_ID2) @@ -2679,7 +2679,7 @@ private DirectReadMetadata(long tsCode, String tsId, String officeId, String uni } private interface FieldMapping { - Field getTsCode(); + Field getTsCode(); Field getLocationCode(); Field getDbOfficeId(); Field getCwmsTsId(); @@ -2693,7 +2693,7 @@ private interface FieldMapping { private static class CwmsTsIdFieldMapping implements FieldMapping { @Override - public Field getTsCode() { + public Field getTsCode() { return AV_CWMS_TS_ID.AV_CWMS_TS_ID.TS_CODE; } @@ -2745,7 +2745,7 @@ public Field getVerionFlag() { private static class CwmsTsId2FieldMapping implements FieldMapping { @Override - public Field getTsCode() { + public Field getTsCode() { return AV_CWMS_TS_ID2.TS_CODE; } diff --git a/cwms-data-api/src/main/java/cwms/cda/data/dao/TimeSeriesIdentifierDescriptorDao.java b/cwms-data-api/src/main/java/cwms/cda/data/dao/TimeSeriesIdentifierDescriptorDao.java index 73d0c4123f..bcf9028aa6 100644 --- a/cwms-data-api/src/main/java/cwms/cda/data/dao/TimeSeriesIdentifierDescriptorDao.java +++ b/cwms-data-api/src/main/java/cwms/cda/data/dao/TimeSeriesIdentifierDescriptorDao.java @@ -126,7 +126,7 @@ public TimeSeriesIdentifierDescriptors getTimeSeriesIdentifiers(String cursor, i } else { Table innerTable = AV_CWMS_TS_ID2.AV_CWMS_TS_ID2.as("alias_table"); Field tsId = innerTable.field("CWMS_TS_ID", String.class); - Field innerTsCode = innerTable.field("TS_CODE", BigDecimal.class); + Field innerTsCode = innerTable.field("TS_CODE", Long.class); Field aliasedItem = innerTable.field("ALIASED_ITEM", String.class); retval = dsl .select(AV_CWMS_TS_ID2.AV_CWMS_TS_ID2.DB_OFFICE_ID, @@ -226,7 +226,7 @@ public Optional getTimeSeriesIdentifier(String o AV_CWMS_TS_ID2 view = AV_CWMS_TS_ID2.AV_CWMS_TS_ID2; Table innerTable = AV_CWMS_TS_ID2.AV_CWMS_TS_ID2.as("alias_table"); Field tsId = innerTable.field("CWMS_TS_ID", String.class); - Field innerTsCode = innerTable.field("TS_CODE", BigDecimal.class); + Field innerTsCode = innerTable.field("TS_CODE", Long.class); Field aliasedItem = innerTable.field("ALIASED_ITEM", String.class); return dsl .select(view.DB_OFFICE_ID, diff --git a/cwms-data-api/src/main/java/cwms/cda/data/dto/measurement/Measurement.java b/cwms-data-api/src/main/java/cwms/cda/data/dto/measurement/Measurement.java index 491b035b9f..16b1ac37f5 100644 --- a/cwms-data-api/src/main/java/cwms/cda/data/dto/measurement/Measurement.java +++ b/cwms-data-api/src/main/java/cwms/cda/data/dto/measurement/Measurement.java @@ -23,6 +23,7 @@ */ package cwms.cda.data.dto.measurement; +import com.fasterxml.jackson.annotation.JsonAlias; import com.fasterxml.jackson.annotation.JsonFormat; import com.fasterxml.jackson.annotation.JsonIgnore; import com.fasterxml.jackson.annotation.JsonInclude; @@ -35,13 +36,16 @@ import cwms.cda.formatters.Formats; import cwms.cda.formatters.annotations.FormattableWith; import cwms.cda.formatters.json.JsonV1; +import cwms.cda.formatters.json.JsonV2; + import java.time.Instant; @FormattableWith(contentType = Formats.JSONV1, formatter = JsonV1.class, aliases = {Formats.DEFAULT, Formats.JSON}) +@FormattableWith(contentType = Formats.JSONV2, formatter = JsonV2.class) @JsonDeserialize(builder = Measurement.Builder.class) @JsonInclude(JsonInclude.Include.NON_NULL) @JsonNaming(PropertyNamingStrategies.KebabCaseStrategy.class) -public final class Measurement extends CwmsDTOBase { +public class Measurement extends CwmsDTOBase { private final String heightUnit; private final String flowUnit; private final String tempUnit; @@ -55,13 +59,20 @@ public final class Measurement extends CwmsDTOBase { private final Instant instant; @JsonProperty(required = true) private final CwmsId id; - @JsonProperty(required = true) - private final String number; + private final String measurementId; + + @Override + protected void validateInternal(cwms.cda.data.dto.CwmsDTOValidator validator) { + super.validateInternal(validator); + validator.required(id, "id"); + validator.required(measurementId, "measurementId"); + validator.required(instant, "instant"); + } private final StreamflowMeasurement streamflowMeasurement; private final SupplementalStreamflowMeasurement supplementalStreamflowMeasurement; private final UsgsMeasurement usgsMeasurement; - private Measurement(Builder builder) { + Measurement(Builder builder) { this.heightUnit = builder.heightUnit; this.flowUnit = builder.flowUnit; this.tempUnit = builder.tempUnit; @@ -75,7 +86,7 @@ private Measurement(Builder builder) { this.supplementalStreamflowMeasurement = builder.supplementalStreamflowMeasurement; this.usgsMeasurement = builder.usgsMeasurement; this.id = builder.id; - this.number = builder.number; + this.measurementId = builder.measurementId; this.instant = builder.instant; } @@ -134,8 +145,8 @@ public Instant getInstant() { return instant; } - public String getNumber() { - return number; + public String getMeasurementId() { + return measurementId; } public StreamflowMeasurement getStreamflowMeasurement() { @@ -150,7 +161,7 @@ public UsgsMeasurement getUsgsMeasurement() { return usgsMeasurement; } - public static final class Builder { + public static class Builder { private String heightUnit; private String flowUnit; private String tempUnit; @@ -164,7 +175,7 @@ public static final class Builder { private SupplementalStreamflowMeasurement supplementalStreamflowMeasurement; private UsgsMeasurement usgsMeasurement; private Instant instant; - private String number; + private String measurementId; private CwmsId id; public Builder withHeightUnit(String heightUnit) { @@ -232,8 +243,9 @@ public Builder withId(CwmsId id) { return this; } - public Builder withNumber(String number) { - this.number = number; + @JsonAlias("number") + public Builder withMeasurementId(String measurementId) { + this.measurementId = measurementId; return this; } @@ -246,4 +258,17 @@ public Measurement build() { return new Measurement(this); } } + + public abstract static class MeasurementV1Mixin { + @JsonProperty("number") + @JsonAlias("measurement-id") + abstract String getMeasurementId(); + + public abstract static class MeasurementBuilderV1Mixin { + @JsonProperty("number") + @JsonAlias("measurement-id") + abstract Measurement.Builder withMeasurementId(String measurementId); + } + } + } diff --git a/cwms-data-api/src/main/java/cwms/cda/data/dto/measurement/MeasurementLegacy.java b/cwms-data-api/src/main/java/cwms/cda/data/dto/measurement/MeasurementLegacy.java new file mode 100644 index 0000000000..64d95c1915 --- /dev/null +++ b/cwms-data-api/src/main/java/cwms/cda/data/dto/measurement/MeasurementLegacy.java @@ -0,0 +1,42 @@ +package cwms.cda.data.dto.measurement; + +import com.fasterxml.jackson.annotation.JsonAlias; +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.DEFAULT, Formats.JSON}) +@JsonDeserialize(builder = MeasurementLegacy.Builder.class) +@JsonInclude(JsonInclude.Include.NON_NULL) +@JsonNaming(PropertyNamingStrategies.KebabCaseStrategy.class) +public class MeasurementLegacy extends Measurement { + + public MeasurementLegacy(Builder builder) { + super(builder); + } + + @JsonProperty("number") + @Override + public String getMeasurementId() { + return super.getMeasurementId(); + } + + public static class Builder extends Measurement.Builder { + @Override + public MeasurementLegacy build() { + return new MeasurementLegacy(this); + } + + @JsonProperty("number") + @JsonAlias("measurement-id") + @Override + public MeasurementLegacy.Builder withMeasurementId(String measurementId) { + return (MeasurementLegacy.Builder) super.withMeasurementId(measurementId); + } + } +} diff --git a/cwms-data-api/src/main/java/cwms/cda/formatters/json/JsonV1.java b/cwms-data-api/src/main/java/cwms/cda/formatters/json/JsonV1.java index 53a2df2e08..22e966ca5a 100644 --- a/cwms-data-api/src/main/java/cwms/cda/formatters/json/JsonV1.java +++ b/cwms-data-api/src/main/java/cwms/cda/formatters/json/JsonV1.java @@ -11,6 +11,7 @@ import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule; import cwms.cda.data.dto.CwmsDTOBase; import cwms.cda.data.dto.Office; +import cwms.cda.data.dto.measurement.Measurement; import cwms.cda.formatters.Formats; import cwms.cda.formatters.FormattingException; import cwms.cda.formatters.OfficeFormatV1; @@ -44,7 +45,7 @@ public JsonV1() { * Create a V1 instance using the project ObjectMapper. * @param om ObjectMapper with its own settings. */ - public JsonV1(ObjectMapper om) { + private JsonV1(ObjectMapper om) { this.om = om; } @@ -68,12 +69,19 @@ public static ObjectMapper buildObjectMapper() { module.addDeserializer(Instant.class, new FlexibleInstantDeserializer()); retVal.registerModule(module); + registerMixIns(retVal); + return retVal; } + public static void registerMixIns(ObjectMapper retVal) { + retVal.addMixIn(Measurement.class, Measurement.MeasurementV1Mixin.class) + .addMixIn(Measurement.Builder.class, Measurement.MeasurementV1Mixin.MeasurementBuilderV1Mixin.class); + } + @Override public String getContentType() { - return Formats.JSON; + return Formats.JSONV1; } @Override diff --git a/cwms-data-api/src/main/java/cwms/cda/formatters/json/JsonV2.java b/cwms-data-api/src/main/java/cwms/cda/formatters/json/JsonV2.java index b941a08a96..f1dc63a055 100644 --- a/cwms-data-api/src/main/java/cwms/cda/formatters/json/JsonV2.java +++ b/cwms-data-api/src/main/java/cwms/cda/formatters/json/JsonV2.java @@ -34,6 +34,7 @@ import com.fasterxml.jackson.datatype.jdk8.Jdk8Module; import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule; import cwms.cda.data.dto.CwmsDTOBase; +import cwms.cda.data.dto.measurement.Measurement; import cwms.cda.formatters.Formats; import cwms.cda.formatters.FormattingException; import cwms.cda.formatters.OutputFormatter; 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 cd16221244..f29278c8d8 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 @@ -127,7 +127,7 @@ static void tearDown() { db.connection(c -> { MeasurementDao measDao = new MeasurementDao(getDslContext(c, OFFICE_ID)); try { - measDao.deleteMeasurements(OFFICE_ID, measLoc, null, null, null, null); + measDao.deleteMeasurements(OFFICE_ID, measLoc, null, null, null, null, null); } catch (Exception e) { LOGGER.atInfo().log("Failed to delete measurements for: " + measLoc + ". Measurement(s) likely already deleted"); } @@ -150,6 +150,17 @@ void test_create_retrieve_delete_measurement(String format) throws IOException { Measurement measurement = measurements.get(0); TestAccounts.KeyUser user = TestAccounts.KeyUser.SPK_NORMAL; + given() + .log().ifValidationFails(LogDetail.ALL,true) + .queryParam(Controllers.OFFICE, user.getOperatingOffice()) + .header(AUTH_HEADER, user.toHeaderValue()) + .when() + .redirects().follow(true) + .redirects().max(3) + .delete("measurements/StreamLoc321") + .then() + .log().ifValidationFails(LogDetail.ALL,true); + // Create the Measurement given() .log().ifValidationFails(LogDetail.ALL, true) @@ -170,7 +181,8 @@ void test_create_retrieve_delete_measurement(String format) throws IOException { .body(IDENTIFIER, isEmptyString()); String locationId = measurement.getLocationId(); - String number = measurement.getNumber(); + String number = measurement.getMeasurementId(); + Instant instant = measurement.getInstant(); // Retrieve the Measurement and assert that it exists given() @@ -201,7 +213,7 @@ void test_create_retrieve_delete_measurement(String format) throws IOException { .body("[0].party", equalTo(measurement.getParty())) .body("[0].wm-comments", equalTo(measurement.getWmComments())) .body("[0].instant", equalTo(measurement.getInstant().toString())) - .body("[0].number", equalTo(measurement.getNumber())) + .body("[0].number", equalTo(measurement.getMeasurementId())) .body("[0].id.name", equalTo(measurement.getLocationId())) .body("[0].id.office-id", equalTo(measurement.getOfficeId())) .body("[0].streamflow-measurement.gage-height", equalTo(measurement.getStreamflowMeasurement().getGageHeight().floatValue())) @@ -237,8 +249,8 @@ void test_create_retrieve_delete_measurement(String format) throws IOException { .accept(format) .header(AUTH_HEADER, user.toHeaderValue()) .queryParam(Controllers.OFFICE, measurement.getId().getOfficeId()) - .queryParam(Controllers.MIN_NUMBER, number) - .queryParam(Controllers.MAX_NUMBER, number) + .queryParam(Controllers.BEGIN, instant.toString()) + .queryParam(Controllers.END, instant.toString()) .when() .redirects().follow(true) .redirects().max(3) @@ -255,10 +267,10 @@ void test_create_retrieve_delete_measurement(String format) throws IOException { given() .log().ifValidationFails(LogDetail.ALL, true) .accept(format) - .queryParam(Controllers.OFFICE, measurement.getId().getOfficeId()) + .queryParam(Controllers.OFFICE_MASK, measurement.getId().getOfficeId()) .queryParam(Controllers.ID_MASK, measurement.getLocationId()) - .queryParam(Controllers.MIN_NUMBER, number) - .queryParam(Controllers.MAX_NUMBER, number) + .queryParam(Controllers.BEGIN, instant.toString()) + .queryParam(Controllers.END, instant.toString()) .queryParam(Controllers.UNIT_SYSTEM, UnitSystem.EN.getValue()) .when() .redirects().follow(true) @@ -284,6 +296,17 @@ void test_create_retrieve_delete_measurement_multiple(String format) throws IOEx Measurement measurement2 = measurements.get(1); TestAccounts.KeyUser user = TestAccounts.KeyUser.SPK_NORMAL; + given() + .log().ifValidationFails(LogDetail.ALL,true) + .queryParam(Controllers.OFFICE, user.getOperatingOffice()) + .header(AUTH_HEADER, user.toHeaderValue()) + .when() + .redirects().follow(true) + .redirects().max(3) + .delete("measurements/StreamLoc321") + .then() + .log().ifValidationFails(LogDetail.ALL,true); + // Create the Measurements given() .log().ifValidationFails(LogDetail.ALL, true) @@ -328,7 +351,7 @@ void test_create_retrieve_delete_measurement_multiple(String format) throws IOEx .body("[0].party", equalTo(measurement1.getParty())) .body("[0].wm-comments", equalTo(measurement1.getWmComments())) .body("[0].instant", equalTo(measurement1.getInstant().toString())) - .body("[0].number", equalTo(measurement1.getNumber())) + .body("[0].number", equalTo(measurement1.getMeasurementId())) .body("[0].id.name", equalTo(measurement1.getLocationId())) .body("[0].id.office-id", equalTo(measurement1.getOfficeId())) .body("[0].streamflow-measurement.gage-height", equalTo(measurement1.getStreamflowMeasurement().getGageHeight().floatValue())) @@ -367,7 +390,7 @@ void test_create_retrieve_delete_measurement_multiple(String format) throws IOEx .body("[1].party", equalTo(measurement2.getParty())) .body("[1].wm-comments", equalTo(measurement2.getWmComments())) .body("[1].instant", equalTo(measurement2.getInstant().toString())) - .body("[1].number", equalTo(measurement2.getNumber())) + .body("[1].number", equalTo(measurement2.getMeasurementId())) .body("[1].id.name", equalTo(measurement2.getLocationId())) .body("[1].id.office-id", equalTo(measurement2.getOfficeId())) .body("[1].streamflow-measurement.gage-height", equalTo(measurement2.getStreamflowMeasurement().getGageHeight().floatValue())) @@ -470,4 +493,92 @@ void test_delete_does_not_exist() { .statusCode(is(HttpServletResponse.SC_NOT_FOUND)); } + @ParameterizedTest + @ValueSource(strings = {Formats.JSONV1, Formats.JSONV2}) + @MinimumSchema(LATEST_SCHEMA) + void test_measurement_id_filter(String format) throws Exception { + InputStream resource = this.getClass().getResourceAsStream("/cwms/cda/api/measurements_with_meas_id.json"); + assertNotNull(resource); + String json = IOUtils.toString(resource, StandardCharsets.UTF_8); + assertNotNull(json); + List measurements = Formats.parseContentList(new ContentType(format), json, Measurement.class); + TestAccounts.KeyUser user = TestAccounts.KeyUser.SPK_NORMAL; + + given() + .log().ifValidationFails(LogDetail.ALL,true) + .queryParam(Controllers.OFFICE, user.getOperatingOffice()) + .header(AUTH_HEADER, user.toHeaderValue()) + .when() + .redirects().follow(true) + .redirects().max(3) + .delete("measurements/StreamLoc321") + .then() + .log().ifValidationFails(LogDetail.ALL,true); + + given() + .log().ifValidationFails(LogDetail.ALL, true) + .contentType(format) + .header(AUTH_HEADER, user.toHeaderValue()) + .body(Formats.format(Formats.parseHeader(format, Measurement.class), measurements, Measurement.class)) + .when() + .redirects().follow(true) + .redirects().max(3) + .post("/measurements") + .then() + .log().ifValidationFails(LogDetail.ALL, true) + .assertThat() + .statusCode(is(HttpServletResponse.SC_CREATED)); + + // Filter by measurement ID + boolean isV2 = format.equals(Formats.JSONV2); + String measurementsPath = isV2 ? "measurements" : ""; + String numberField = isV2 ? "measurement-id" : "number"; + given() + .log().ifValidationFails(LogDetail.ALL, true) + .accept(format) + .queryParam(Controllers.OFFICE_MASK, OFFICE_ID) + .queryParam(Controllers.ID_MASK, "StreamLoc321") + .queryParam(Controllers.MEASUREMENT_ID, "123456") + .when() + .redirects().follow(true) + .redirects().max(3) + .get("/measurements") + .then() + .log().ifValidationFails(LogDetail.ALL, true) + .assertThat() + .statusCode(is(HttpServletResponse.SC_OK)) + .body(measurementsPath + (measurementsPath.isEmpty() ? "" : ".") + "size()", is(1)) + .body(measurementsPath + (measurementsPath.isEmpty() ? "" : ".") + "[0]." + numberField, is("123456")); + + given() + .log().ifValidationFails(LogDetail.ALL, true) + .accept(format) + .queryParam(Controllers.OFFICE_MASK, OFFICE_ID) + .queryParam(Controllers.ID_MASK, "StreamLoc321") + .queryParam(Controllers.MEASUREMENT_ID, "550e8400-e29b-41d4-a716-446655440000") + .when() + .redirects().follow(true) + .redirects().max(3) + .get("/measurements") + .then() + .log().ifValidationFails(LogDetail.ALL, true) + .assertThat() + .statusCode(is(HttpServletResponse.SC_OK)) + .body(measurementsPath + (measurementsPath.isEmpty() ? "" : ".") + "size()", is(1)) + .body(measurementsPath + (measurementsPath.isEmpty() ? "" : ".") + "[0]." + numberField, is("550e8400-e29b-41d4-a716-446655440000")); + + given() + .log().ifValidationFails(LogDetail.ALL,true) + .queryParam(Controllers.OFFICE, user.getOperatingOffice()) + .header(AUTH_HEADER, user.toHeaderValue()) + .when() + .redirects().follow(true) + .redirects().max(3) + .delete("measurements/StreamLoc321") + .then() + .log().ifValidationFails(LogDetail.ALL,true) + .assertThat() + .statusCode(is(HttpServletResponse.SC_OK)); + } + } 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 8b683ba62d..6f5eac9c5e 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 @@ -60,7 +60,7 @@ void testFromJooqMeasurementRecord() { Measurement measurement = MeasurementDao.fromJooqMeasurementRecord(record); assertNotNull(measurement); - assertEquals("12345", measurement.getNumber()); + assertEquals("12345", measurement.getMeasurementId()); assertEquals("USGS", measurement.getAgency()); assertEquals("SomeParty", measurement.getParty()); assertEquals(instant, measurement.getInstant()); @@ -150,7 +150,7 @@ void testToDbXmlMultiple() throws Exception { private Measurement buildTestMeasurement() { return new Measurement.Builder() - .withNumber("12345") + .withMeasurementId("12345") .withAgency("USGS") .withParty("SomeParty") .withInstant(Instant.parse("2024-01-01T00:00:00Z")) @@ -202,7 +202,7 @@ private Measurement buildTestMeasurement() { private Measurement buildMeasurement2() { return new Measurement.Builder() - .withNumber("123456") + .withMeasurementId("123456") .withAgency("USGS") .withParty("SomeParty2") .withInstant(Instant.parse("2024-02-01T00:00:00Z")) @@ -279,7 +279,7 @@ private SUPP_STREAMFLOW_MEAS_T mockSupplementalStreamflowMeasurement() { } private static void assertMatch(Measurement meas, MeasurementDao.MeasurementXmlDto xmlDto) { - assertEquals(meas.getNumber(), xmlDto.getNumber()); + assertEquals(meas.getMeasurementId(), xmlDto.getNumber()); assertEquals(meas.getAgency(), xmlDto.getAgency()); assertEquals(meas.getParty(), xmlDto.getParty()); assertEquals(meas.getInstant(), xmlDto.getInstant()); 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 d550376c84..ea66056479 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 @@ -176,14 +176,14 @@ void testRoundTripStore() throws Exception { measurementDao.storeMeasurements(measurements, false); List retrievedMeasurements = measurementDao.retrieveMeasurements(OFFICE_ID, streamLocId, null, null, UnitSystem.EN.getValue(), - null, null, null, null, null, null, null, null); + null, null, null, null, null, null, null, null, null); assertEquals(2, retrievedMeasurements.size()); DTOMatch.assertMatch(meas1, retrievedMeasurements.get(0)); DTOMatch.assertMatch(meas1B, retrievedMeasurements.get(1)); List measurementsAll = measurementDao.retrieveMeasurements(OFFICE_ID, null, null, null, UnitSystem.EN.getValue(), - null, null, null, null, null, null, null, null); + null, null, null, null, null, null, null, null, null); List meas1List = measurementsAll.stream() .filter(m -> m.getLocationId().equals(streamLocId)) .collect(Collectors.toList()); @@ -199,18 +199,18 @@ void testRoundTripStore() throws Exception { DTOMatch.assertMatch(meas2, meas2Found); retrievedMeasurements = measurementDao.retrieveMeasurements(OFFICE_ID, streamLocId, null, null, UnitSystem.EN.getValue(), - null, null, null, null, null, null, null, null); + null, null, null, null, null, null, null, null, null); Measurement finalMeas1 = meas1; Measurement finalMeas1B = meas1B; Measurement retrievedMeas1 = retrievedMeasurements.stream() - .filter(m -> m.getNumber().equals(finalMeas1.getNumber())) + .filter(m -> m.getMeasurementId().equals(finalMeas1.getMeasurementId())) .findFirst() .orElse(null); assertNotNull(retrievedMeas1); DTOMatch.assertMatch(meas1, retrievedMeas1); Measurement retrievedMeas1B = retrievedMeasurements.stream() - .filter(m -> m.getNumber().equals(finalMeas1B.getNumber())) + .filter(m -> m.getMeasurementId().equals(finalMeas1B.getMeasurementId())) .findFirst() .orElse(null); assertNotNull(retrievedMeas1B); @@ -227,15 +227,15 @@ void testRoundTripStore() throws Exception { 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); + measurementDao.deleteMeasurements(meas1.getId().getOfficeId(), meas1.getId().getName(), null, null, null, null, null); + measurementDao.deleteMeasurements(meas2.getId().getOfficeId(), meas2.getId().getName(), null, null, null, null, null); final Measurement meas1F = meas1; final Measurement meas2F = meas2; assertThrows(NotFoundException.class, () -> measurementDao.retrieveMeasurements(meas1F.getId().getOfficeId(), meas1F.getId().getName(), - null, null, UnitSystem.EN.getValue(), null, null, null, null, null, null, null, null)); + null, null, UnitSystem.EN.getValue(), null, null, null, null, null, null, null, null, null)); assertThrows(NotFoundException.class, () -> measurementDao.retrieveMeasurements(meas2F.getId().getOfficeId(), meas2F.getId().getName(), - null, null, UnitSystem.EN.getValue(), null, null, null, null, null, null, null, null)); + null, null, UnitSystem.EN.getValue(), null, null, null, null, null, null, null, null, null)); } finally { //delete stream locations streamLocationDao.deleteStreamLocation( @@ -258,7 +258,7 @@ private Measurement buildMeasurement1(String streamLocId) { private Measurement buildMeasurement1(String streamLocId, double flow) { return new Measurement.Builder() - .withNumber("12345") + .withMeasurementId("12345") .withAgency("USGS") .withParty("SomeParty") .withInstant(Instant.parse("2024-01-01T00:00:00Z")) @@ -315,7 +315,7 @@ private Measurement buildMeasurement2(String streamLocId) { private Measurement buildMeasurement2(String streamLocId, double flow) { //same as buildMeasurement but with different values (same office) return new Measurement.Builder() - .withNumber("54321") + .withMeasurementId("54321") .withAgency("USGS") .withParty("SomeParty2") .withInstant(Instant.parse("2024-02-01T00:00:00Z")) @@ -367,7 +367,7 @@ private Measurement buildMeasurement2(String streamLocId, double flow) { private Measurement buildMeasurementDoesntExist(String streamLocId) { return new Measurement.Builder() - .withNumber("0981273") + .withMeasurementId("0981273") .withAgency("USGS") .withParty("SomeParty") .withInstant(Instant.parse("2024-01-01T00:00:00Z")) diff --git a/cwms-data-api/src/test/java/cwms/cda/data/dto/MeasurementTest.java b/cwms-data-api/src/test/java/cwms/cda/data/dto/MeasurementTest.java index d37f5616c6..e7b28ed24c 100644 --- a/cwms-data-api/src/test/java/cwms/cda/data/dto/MeasurementTest.java +++ b/cwms-data-api/src/test/java/cwms/cda/data/dto/MeasurementTest.java @@ -39,6 +39,7 @@ import org.apache.commons.io.IOUtils; import static org.junit.jupiter.api.Assertions.assertAll; import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; import static org.junit.jupiter.api.Assertions.assertNotNull; import static org.junit.jupiter.api.Assertions.assertThrows; import static org.junit.jupiter.api.Assertions.assertTrue; @@ -99,7 +100,7 @@ void createMeasurement_allFieldsProvided_success() { .withParty("Survey Party") .withWmComments("Measurement made during normal flow conditions.") .withInstant(Instant.parse("2024-09-16T00:00:00Z")) - .withNumber("123456") + .withMeasurementId("123456") .withStreamflowMeasurement(sfm) .withSupplementalStreamflowMeasurement(supplementalStreamflowMeas) .withUsgsMeasurement(usgsMeas) @@ -115,7 +116,7 @@ void createMeasurement_allFieldsProvided_success() { () -> assertEquals("Survey Party", item.getParty(), "Party"), () -> assertEquals("Measurement made during normal flow conditions.", item.getWmComments(), "Comments"), () -> assertNotNull(item.getInstant(), "Instant"), - () -> assertEquals("123456", item.getNumber(), "Measurement number"), + () -> assertEquals("123456", item.getMeasurementId(), "Measurement number"), () -> DTOMatch.assertMatch(cwmsId, item.getId()), () -> DTOMatch.assertMatch(sfm, item.getStreamflowMeasurement()), () -> DTOMatch.assertMatch(supplementalStreamflowMeas, item.getSupplementalStreamflowMeasurement()), @@ -129,7 +130,7 @@ void createMeasurement_missingField_throwsFieldException() { Measurement item = new Measurement.Builder() .withHeightUnit("ft") .withFlowUnit("cfs") - .withNumber("123456") + .withMeasurementId("123456") .withInstant(Instant.parse("2024-09-16T00:00:00Z")) .build(); @@ -202,7 +203,7 @@ void createMeasurement_serialize_roundtrip() { .withParty("Survey Party") .withWmComments("Measurement made during normal flow conditions.") .withInstant(Instant.parse("2024-09-16T00:00:00Z")) - .withNumber("123456") + .withMeasurementId("123456") .withStreamflowMeasurement(sfm) .withSupplementalStreamflowMeasurement(supplementalStreamflowMeas) .withUsgsMeasurement(usgsMeas) @@ -269,13 +270,13 @@ void createMeasurement_deserialize() throws Exception { .withParty("Survey Party") .withWmComments("Measurement made during normal flow conditions.") .withInstant(Instant.parse("2024-09-16T00:00:00Z")) - .withNumber("123456") + .withMeasurementId("123456") .withStreamflowMeasurement(sfm) .withSupplementalStreamflowMeasurement(supplementalStreamflowMeas) .withUsgsMeasurement(usgsMeas) .build(); - InputStream resource = this.getClass().getResourceAsStream("/cwms/cda/data/dto/measurement.json"); + InputStream resource = this.getClass().getResourceAsStream("/cwms/cda/data/dto/measurement_with_number.json"); assertNotNull(resource); String json = IOUtils.toString(resource, StandardCharsets.UTF_8); ContentType contentType = new ContentType(Formats.JSON); @@ -284,6 +285,75 @@ void createMeasurement_deserialize() throws Exception { DTOMatch.assertMatch(expectedMeasurement, deserialized); } + @Test + void createMeasurement_deserialize_meas_id() throws Exception { + CwmsId cwmsId = new CwmsId.Builder() + .withName("Buckhorn") + .withOfficeId("SPK") + .build(); + + StreamflowMeasurement sfm = new StreamflowMeasurement.Builder() + .withGageHeight(5.5) + .withFlow(250.0) + .withQuality("Good") + .build(); + + SupplementalStreamflowMeasurement supplementalStreamflowMeas = new SupplementalStreamflowMeasurement.Builder() + .withChannelFlow(300.0) + .withOverbankFlow(50.0) + .withOverbankMaxDepth(5.0) + .withChannelMaxDepth(10.0) + .withAvgVelocity(1.5) + .withSurfaceVelocity(2.0) + .withMaxVelocity(3.0) + .withEffectiveFlowArea(200.0) + .withCrossSectionalArea(250.0) + .withMeanGage(20.0) + .withTopWidth(30.0) + .withMainChannelArea(150.0) + .withOverbankArea(80.0) + .build(); + + UsgsMeasurement usgsMeas = new UsgsMeasurement.Builder() + .withRemarks("Remarks") + .withCurrentRating("Rating") + .withControlCondition("Condition") + .withFlowAdjustment("Adjustment") + .withShiftUsed(0.1) + .withPercentDifference(5.0) + .withDeltaHeight(0.05) + .withDeltaTime(10.0) + .withAirTemp(20.0) + .withWaterTemp(15.0) + .build(); + + Measurement expectedMeasurement = new Measurement.Builder() + .withId(cwmsId) + .withHeightUnit("ft") + .withFlowUnit("cfs") + .withTempUnit("F") + .withVelocityUnit("ft/s") + .withAreaUnit("sq ft") + .withUsed(true) + .withAgency("USGS") + .withParty("Survey Party") + .withWmComments("Measurement made during normal flow conditions.") + .withInstant(Instant.parse("2024-09-16T00:00:00Z")) + .withMeasurementId("123456") + .withStreamflowMeasurement(sfm) + .withSupplementalStreamflowMeasurement(supplementalStreamflowMeas) + .withUsgsMeasurement(usgsMeas) + .build(); + + InputStream resource = this.getClass().getResourceAsStream("/cwms/cda/data/dto/measurement_with_meas_id.json"); + assertNotNull(resource); + String json = IOUtils.toString(resource, StandardCharsets.UTF_8); + ContentType contentType = new ContentType(Formats.JSONV2); + Measurement deserialized = Formats.parseContent(contentType, json, Measurement.class); + + DTOMatch.assertMatch(expectedMeasurement, deserialized); + } + @Test void createMeasurement_deserialize_multiple() throws Exception { CwmsId cwmsId1 = new CwmsId.Builder() @@ -338,7 +408,7 @@ void createMeasurement_deserialize_multiple() throws Exception { .withParty("Survey Party") .withWmComments("Measurement made during normal flow conditions.") .withInstant(Instant.parse("2024-09-16T00:00:00Z")) - .withNumber("123456") + .withMeasurementId("123456") .withStreamflowMeasurement(sfm1) .withSupplementalStreamflowMeasurement(supplementalStreamflowMeas1) .withUsgsMeasurement(usgsMeas1) @@ -397,14 +467,14 @@ void createMeasurement_deserialize_multiple() throws Exception { .withParty("Second Survey Party") .withWmComments("Measurement made after recent rainfall.") .withInstant(Instant.parse("2024-09-17T12:00:00Z")) - .withNumber("654321") + .withMeasurementId("654321") .withStreamflowMeasurement(sfm2) .withSupplementalStreamflowMeasurement(supplementalStreamflowMeas2) .withUsgsMeasurement(usgsMeas2) .build(); // Reading and Deserializing the JSON - InputStream resource = this.getClass().getResourceAsStream("/cwms/cda/data/dto/measurements.json"); + InputStream resource = this.getClass().getResourceAsStream("/cwms/cda/data/dto/measurements_with_number.json"); assertNotNull(resource); String json = IOUtils.toString(resource, StandardCharsets.UTF_8); ContentType contentType = new ContentType(Formats.JSON); @@ -417,7 +487,7 @@ void createMeasurement_deserialize_multiple() throws Exception { } @Test - void testRoundTripMultiple() + void testRoundTripMultiple_v1_and_v2() { CwmsId cwmsId1 = new CwmsId.Builder() .withName("Buckhorn") @@ -471,7 +541,7 @@ void testRoundTripMultiple() .withParty("Survey Party") .withWmComments("Measurement made during normal flow conditions.") .withInstant(Instant.parse("2024-09-16T00:00:00Z")) - .withNumber("123456") + .withMeasurementId("123456") .withStreamflowMeasurement(sfm1) .withSupplementalStreamflowMeasurement(supplementalStreamflowMeas1) .withUsgsMeasurement(usgsMeas1) @@ -530,7 +600,7 @@ void testRoundTripMultiple() .withParty("Second Survey Party") .withWmComments("Measurement made after recent rainfall.") .withInstant(Instant.parse("2024-09-17T12:00:00Z")) - .withNumber("654321") + .withMeasurementId("654321") .withStreamflowMeasurement(sfm2) .withSupplementalStreamflowMeasurement(supplementalStreamflowMeas2) .withUsgsMeasurement(usgsMeas2) @@ -540,12 +610,23 @@ void testRoundTripMultiple() measurements.add(expectedMeasurement1); measurements.add(expectedMeasurement2); - ContentType contentType = new ContentType(Formats.JSON); + ContentType contentType = new ContentType(Formats.JSONV1); String serialized = Formats.format(contentType, measurements, Measurement.class); + assertTrue(serialized.contains("number")); List deserialized = Formats.parseContentList(contentType, serialized, Measurement.class); assertEquals(2, deserialized.size(), "Expected 2 measurements"); DTOMatch.assertMatch(expectedMeasurement1, deserialized.get(0)); DTOMatch.assertMatch(expectedMeasurement2, deserialized.get(1)); + + contentType = new ContentType(Formats.JSONV2); + serialized = Formats.format(contentType, measurements, Measurement.class); + assertFalse(serialized.contains("number")); + assertTrue(serialized.contains("measurement-id")); + deserialized = Formats.parseContentList(contentType, serialized, Measurement.class); + + assertEquals(2, deserialized.size(), "Expected 2 measurements"); + DTOMatch.assertMatch(expectedMeasurement1, deserialized.get(0)); + DTOMatch.assertMatch(expectedMeasurement2, deserialized.get(1)); } } 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 415f7b7f12..edaa8561a1 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 @@ -523,7 +523,7 @@ public static void assertMatch(Measurement first, Measurement second) { () -> assertEquals(first.getParty(), second.getParty(), "Party does not match"), () -> assertEquals(first.getWmComments(), second.getWmComments(), "WM Comments do not match"), () -> assertEquals(first.getInstant(), second.getInstant(), "Instant does not match"), - () -> assertEquals(first.getNumber(), second.getNumber(), "Number does not match"), + () -> assertEquals(first.getMeasurementId(), second.getMeasurementId(), "Number does not match"), () -> assertMatch(first.getStreamflowMeasurement(), second.getStreamflowMeasurement()), () -> assertMatch(first.getSupplementalStreamflowMeasurement(), second.getSupplementalStreamflowMeasurement()), () -> assertMatch(first.getUsgsMeasurement(), second.getUsgsMeasurement()) diff --git a/cwms-data-api/src/test/resources/cwms/cda/api/measurements_with_meas_id.json b/cwms-data-api/src/test/resources/cwms/cda/api/measurements_with_meas_id.json new file mode 100644 index 0000000000..01eabec932 --- /dev/null +++ b/cwms-data-api/src/test/resources/cwms/cda/api/measurements_with_meas_id.json @@ -0,0 +1,100 @@ +[ + { + "height-unit": "ft", + "flow-unit": "cfs", + "temp-unit": "F", + "velocity-unit": "fps", + "area-unit": "ft2", + "used": true, + "agency": "USGS", + "party": "SPK", + "wm-comments": "Measurement made during normal flow conditions.", + "instant": "2024-09-16T00:00:00Z", + "measurement-id": "123456", + "id": { + "name": "StreamLoc321", + "office-id": "SPK" + }, + "streamflow-measurement": { + "gage-height": 5.5, + "flow": 250.0, + "quality": "Good" + }, + "supplemental-streamflow-measurement": { + "channel-flow": 300.0, + "overbank-flow": 50.0, + "overbank-max-depth": 5.0, + "channel-max-depth": 10.0, + "avg-velocity": 1.5, + "surface-velocity": 2.0, + "max-velocity": 3.0, + "effective-flow-area": 200.0, + "cross-sectional-area": 250.0, + "mean-gage": 20.0, + "top-width": 30.0, + "main-channel-area": 150.0, + "overbank-area": 80.0 + }, + "usgs-measurement": { + "remarks": "Remarks", + "current-rating": "1", + "control-condition": "FILL", + "flow-adjustment": "OTHR", + "shift-used": 0.1, + "percent-difference": 5.0, + "delta-height": 0.05, + "delta-time": 10.0, + "air-temp": 20.0, + "water-temp": 15.0 + } + }, + { + "height-unit": "ft", + "flow-unit": "cfs", + "temp-unit": "F", + "velocity-unit": "fps", + "area-unit": "ft2", + "used": true, + "agency": "USGS", + "party": "SPK", + "wm-comments": "Measurement made after recent rainfall.", + "instant": "2024-09-17T12:00:00Z", + "measurement-id": "550e8400-e29b-41d4-a716-446655440000", + "id": { + "name": "StreamLoc321", + "office-id": "SPK" + }, + "streamflow-measurement": { + "gage-height": 6.0, + "flow": 275.0, + "quality": "Fair" + }, + "supplemental-streamflow-measurement": { + "channel-flow": 320.0, + "overbank-flow": 45.0, + "overbank-max-depth": 4.5, + "channel-max-depth": 9.5, + "avg-velocity": 1.8, + "surface-velocity": 2.5, + "max-velocity": 3.5, + "effective-flow-area": 220.0, + "cross-sectional-area": 260.0, + "mean-gage": 21.0, + "top-width": 32.0, + "main-channel-area": 160.0, + "overbank-area": 85.0 + }, + "usgs-measurement": { + "remarks": "Post-rain conditions.", + "current-rating": "2", + "control-condition": "FILL", + "flow-adjustment": "OTHR", + "shift-used": 0.15, + "percent-difference": 4.5, + "delta-height": 0.1, + "delta-time": 15.0, + "air-temp": 18.0, + "water-temp": 16.0 + } + } +] diff --git a/cwms-data-api/src/test/resources/cwms/cda/data/dto/measurement_with_meas_id.json b/cwms-data-api/src/test/resources/cwms/cda/data/dto/measurement_with_meas_id.json new file mode 100644 index 0000000000..b3c64031b2 --- /dev/null +++ b/cwms-data-api/src/test/resources/cwms/cda/data/dto/measurement_with_meas_id.json @@ -0,0 +1,49 @@ +{ + "height-unit": "ft", + "flow-unit": "cfs", + "temp-unit": "F", + "velocity-unit": "ft/s", + "area-unit": "sq ft", + "used": true, + "agency": "USGS", + "party": "Survey Party", + "wm-comments": "Measurement made during normal flow conditions.", + "instant": "2024-09-16T00:00:00Z", + "measurement-id": "123456", + "id": { + "name": "Buckhorn", + "office-id": "SPK" + }, + "streamflow-measurement": { + "gage-height": 5.5, + "flow": 250.0, + "quality": "Good" + }, + "supplemental-streamflow-measurement": { + "channel-flow": 300.0, + "overbank-flow": 50.0, + "overbank-max-depth": 5.0, + "channel-max-depth": 10.0, + "avg-velocity": 1.5, + "surface-velocity": 2.0, + "max-velocity": 3.0, + "effective-flow-area": 200.0, + "cross-sectional-area": 250.0, + "mean-gage": 20.0, + "top-width": 30.0, + "main-channel-area": 150.0, + "overbank-area": 80.0 + }, + "usgs-measurement": { + "remarks": "Remarks", + "current-rating": "Rating", + "control-condition": "Condition", + "flow-adjustment": "Adjustment", + "shift-used": 0.1, + "percent-difference": 5.0, + "delta-height": 0.05, + "delta-time": 10.0, + "air-temp": 20.0, + "water-temp": 15.0 + } +} diff --git a/cwms-data-api/src/test/resources/cwms/cda/data/dto/measurement.json b/cwms-data-api/src/test/resources/cwms/cda/data/dto/measurement_with_number.json similarity index 100% rename from cwms-data-api/src/test/resources/cwms/cda/data/dto/measurement.json rename to cwms-data-api/src/test/resources/cwms/cda/data/dto/measurement_with_number.json diff --git a/cwms-data-api/src/test/resources/cwms/cda/data/dto/measurements.json b/cwms-data-api/src/test/resources/cwms/cda/data/dto/measurements_with_number.json similarity index 100% rename from cwms-data-api/src/test/resources/cwms/cda/data/dto/measurements.json rename to cwms-data-api/src/test/resources/cwms/cda/data/dto/measurements_with_number.json diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 523452fae6..47e8b635da 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -1,7 +1,7 @@ [versions] jaxb-api = "2.3.1" jaxb-impl = "3.0.2" -jooq-codegen = "26.02.17-RC01-oracle19c" +jooq-codegen = "latest-dev_sha256_e7740339fdf4-oracle19c" jooq = "3.18.7-jdk11" slf4j = "2.0.17" hec-monolith = "3.3.20"