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 b4f0a4ba2..4082bc32c 100644 --- a/cwms-data-api/src/main/java/cwms/cda/ApiServlet.java +++ b/cwms-data-api/src/main/java/cwms/cda/ApiServlet.java @@ -26,6 +26,7 @@ import static cwms.cda.api.Controllers.CONTRACT_NAME; +import static cwms.cda.api.Controllers.LOCATION_ID; import static cwms.cda.api.Controllers.NAME; import static cwms.cda.api.Controllers.OFFICE; import static cwms.cda.api.Controllers.PROJECT_ID; @@ -214,6 +215,7 @@ "/streams/*", "/stream-locations/*", "/stream-reaches/*", + "/measurements/*", "/blobs/*", "/clobs/*", "/pools/*", @@ -538,6 +540,9 @@ protected void configureRoutes() { new StreamLocationController(metrics), requiredRoles,5, TimeUnit.MINUTES); cdaCrudCache(format("/stream-reaches/{%s}", NAME), new StreamReachController(metrics), requiredRoles,1, TimeUnit.DAYS); + String measurements = "/measurements/"; + cdaCrudCache(format(measurements + "{%s}", LOCATION_ID), + new cwms.cda.api.MeasurementController(metrics), requiredRoles,5, TimeUnit.MINUTES); cdaCrudCache("/blobs/{blob-id}", new BlobController(metrics), requiredRoles,5, TimeUnit.MINUTES); cdaCrudCache("/clobs/{clob-id}", diff --git a/cwms-data-api/src/main/java/cwms/cda/api/Controllers.java b/cwms-data-api/src/main/java/cwms/cda/api/Controllers.java index 58e1aec84..154f83849 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 @@ -134,6 +134,15 @@ public final class Controllers { public static final String ISSUE_DATE = "issue-date"; public static final String LOCATION_KIND_LIKE = "location-kind-like"; public static final String LOCATION_TYPE_LIKE = "location-type-like"; + public static final String MIN_NUMBER = "min-number"; + public static final String MAX_NUMBER = "max-number"; + public static final String MIN_HEIGHT = "min-height"; + public static final String MAX_HEIGHT = "max-height"; + public static final String MIN_FLOW = "min-flow"; + public static final String MAX_FLOW = "max-flow"; + public static final String AGENCY = "agency"; + public static final String QUALITY = "quality"; + public static final String GROUP_ID = "group-id"; public static final String REPLACE_ASSIGNED_LOCS = "replace-assigned-locs"; @@ -372,6 +381,16 @@ public static T requiredParamAs(io.javalin.http.Context ctx, String name, Cl .getOrThrow(e -> new RequiredQueryParameterException(name)); } + @Nullable + public static Double queryParamAsDouble(Context ctx, String param) { + Double retVal = null; + String numberStr = ctx.queryParam(param); + if (numberStr != null) { + retVal = Double.parseDouble(numberStr); + } + return retVal; + } + @Nullable public static ZonedDateTime queryParamAsZdt(Context ctx, String param, String timezone) { ZonedDateTime beginZdt = null; 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 new file mode 100644 index 000000000..8f2607176 --- /dev/null +++ b/cwms-data-api/src/main/java/cwms/cda/api/MeasurementController.java @@ -0,0 +1,267 @@ +/* + * 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.AGENCY; +import static cwms.cda.api.Controllers.BEGIN; +import static cwms.cda.api.Controllers.CREATE; +import static cwms.cda.api.Controllers.DATE_FORMAT; +import static cwms.cda.api.Controllers.DELETE; +import static cwms.cda.api.Controllers.EXAMPLE_DATE; +import static cwms.cda.api.Controllers.FAIL_IF_EXISTS; +import static cwms.cda.api.Controllers.GET_ALL; +import static cwms.cda.api.Controllers.GET_ONE; +import static cwms.cda.api.Controllers.ID_MASK; +import static cwms.cda.api.Controllers.LOCATION_ID; +import static cwms.cda.api.Controllers.END; +import static cwms.cda.api.Controllers.MAX_FLOW; +import static cwms.cda.api.Controllers.MAX_HEIGHT; +import static cwms.cda.api.Controllers.MIN_FLOW; +import static cwms.cda.api.Controllers.MIN_HEIGHT; +import static cwms.cda.api.Controllers.NOT_SUPPORTED_YET; +import static cwms.cda.api.Controllers.MIN_NUMBER; +import static cwms.cda.api.Controllers.MAX_NUMBER; +import static cwms.cda.api.Controllers.OFFICE; +import static cwms.cda.api.Controllers.OFFICE_MASK; +import static cwms.cda.api.Controllers.QUALITY; +import static cwms.cda.api.Controllers.TIMEZONE; +import static cwms.cda.api.Controllers.UNIT_SYSTEM; +import static cwms.cda.api.Controllers.queryParamAsDouble; +import static cwms.cda.api.Controllers.queryParamAsInstant; +import static cwms.cda.api.Controllers.requiredParam; +import cwms.cda.api.enums.UnitSystem; +import cwms.cda.data.dao.MeasurementDao; +import cwms.cda.data.dto.measurement.Measurement; +import cwms.cda.formatters.ContentType; +import cwms.cda.formatters.Formats; +import io.javalin.apibuilder.CrudHandler; +import io.javalin.core.util.Header; +import io.javalin.http.Context; +import io.javalin.plugin.openapi.annotations.HttpMethod; +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.OpenApiRequestBody; +import io.javalin.plugin.openapi.annotations.OpenApiResponse; +import java.time.Instant; +import org.jetbrains.annotations.NotNull; +import org.jooq.DSLContext; + +import javax.servlet.http.HttpServletResponse; +import java.util.List; + +import static cwms.cda.data.dao.JooqDao.getDslContext; + +public final class MeasurementController implements CrudHandler { + + static final String TAG = "Measurements"; + + private final MetricRegistry metrics; + private final Histogram requestResultSize; + + public MeasurementController(MetricRegistry metrics) { + this.metrics = metrics; + String className = this.getClass().getName(); + requestResultSize = this.metrics.histogram(name(className, "results", "size")); + } + + private Timer.Context markAndTime(String subject) { + return Controllers.markAndTime(metrics, getClass().getName(), subject); + } + + @OpenApi( + 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 = BEGIN, description = "The start of the time " + + "window to delete. The format for this field is ISO 8601 extended, with " + + "optional offset and timezone, i.e., '" + DATE_FORMAT + "', e.g., '" + + EXAMPLE_DATE + "'. A null value is treated as an unbounded start."), + @OpenApiParam(name = END, description = "The end of the time " + + "window to delete.The format for this field is ISO 8601 extended, with " + + "optional offset and timezone, i.e., '" + DATE_FORMAT + "', e.g., '" + + EXAMPLE_DATE + "'.A null value is treated as an unbounded end."), + @OpenApiParam(name = TIMEZONE, description = "This field specifies a default timezone " + + "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_HEIGHT, description = "Minimum height for filtering measurements."), + @OpenApiParam(name = MAX_HEIGHT, description = "Maximum height for filtering measurements."), + @OpenApiParam(name = MIN_FLOW, description = "Minimum flow for filtering measurements."), + @OpenApiParam(name = MAX_FLOW, description = "Maximum flow for filtering measurements."), + @OpenApiParam(name = AGENCY, description = "Agencies for filtering measurements."), + @OpenApiParam(name = QUALITY, description = "Quality for filtering measurements."), + @OpenApiParam(name = UNIT_SYSTEM, description = "Specifies the unit system" + + " of the response. Valid values for the unit field are: " + + "\n* `EN` Specifies English unit system. Location values will be in the " + + "default English units for their parameters." + + "\n* `SI` Specifies the SI unit system. Location values will be in the " + + "default SI units for their parameters. If not specified, EN is used.") + }, + responses = { + @OpenApiResponse(status = "200", content = { + @OpenApiContent(isArray = true, type = Formats.JSONV1, from = Measurement.class), + @OpenApiContent(isArray = true, type = Formats.JSON, from = Measurement.class) + }) + }, + description = "Returns matching measurement data.", + tags = {TAG} + ) + @Override + public void getAll(@NotNull Context ctx) { + String officeId = ctx.queryParam(OFFICE_MASK); + String locationId = ctx.queryParam(ID_MASK); + String unitSystem = ctx.queryParamAsClass(UNIT_SYSTEM, String.class).getOrDefault(UnitSystem.EN.value()); + Instant minDate = queryParamAsInstant(ctx, BEGIN); + Instant maxDate = queryParamAsInstant(ctx, END); + String minNum = ctx.queryParam(MIN_NUMBER); + String maxNum = ctx.queryParam(MAX_NUMBER); + Number minHeight = queryParamAsDouble(ctx, MIN_HEIGHT); + Number maxHeight = queryParamAsDouble(ctx, MAX_HEIGHT); + Number minFlow = queryParamAsDouble(ctx, MIN_FLOW); + Number maxFlow = queryParamAsDouble(ctx, MAX_FLOW); + String agency = ctx.queryParam(AGENCY); + String quality = ctx.queryParam(QUALITY); + try (Timer.Context ignored = markAndTime(GET_ALL)) { + 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); + String formatHeader = ctx.header(Header.ACCEPT); + ContentType contentType = Formats.parseHeader(formatHeader, Measurement.class); + ctx.contentType(contentType.toString()); + String serialized = Formats.format(contentType, measurements, Measurement.class); + ctx.result(serialized); + ctx.status(HttpServletResponse.SC_OK); + requestResultSize.update(serialized.length()); + } + } + + @OpenApi(ignore = true) + @Override + public void getOne(@NotNull Context ctx, @NotNull String locationId) { + try (final Timer.Context ignored = markAndTime(GET_ONE)) { + throw new UnsupportedOperationException(NOT_SUPPORTED_YET); + } + + } + + @OpenApi( + requestBody = @OpenApiRequestBody( + content = { + @OpenApiContent(isArray = true, from = Measurement.class, type = Formats.JSONV1), + @OpenApiContent(isArray = true, from = Measurement.class, type = Formats.JSON) + }, + required = true), + queryParams = { + @OpenApiParam(name = FAIL_IF_EXISTS, type = Boolean.class, + description = "Create will fail if provided Measurement(s) already exist. Default: true") + }, + description = "Create new measurement(s).", + method = HttpMethod.POST, + tags = {TAG}, + responses = { + @OpenApiResponse(status = "204", description = "Measurement(s) successfully stored.") + } + ) + @Override + public void create(Context ctx) { + + try (Timer.Context ignored = markAndTime(CREATE)) { + String formatHeader = ctx.req.getContentType(); + ContentType contentType = Formats.parseHeader(formatHeader, Measurement.class); + List measurements = Formats.parseContentList(contentType, ctx.body(), Measurement.class); + boolean failIfExists = ctx.queryParamAsClass(FAIL_IF_EXISTS, Boolean.class).getOrDefault(true); + DSLContext dsl = getDslContext(ctx); + MeasurementDao dao = new MeasurementDao(dsl); + dao.storeMeasurements(measurements, failIfExists); + String statusMsg = "Created Measurement"; + if(measurements.size() > 1) + { + statusMsg += "s"; + } + ctx.status(HttpServletResponse.SC_CREATED).json(statusMsg); + } + } + + @OpenApi(ignore = true) + @Override + public void update(@NotNull Context ctx, @NotNull String locationId) { + try (final Timer.Context ignored = markAndTime(GET_ONE)) { + throw new UnsupportedOperationException(NOT_SUPPORTED_YET); + } + } + + @OpenApi( + pathParams = { + @OpenApiParam(name = LOCATION_ID, description = "Specifies the location-id of " + + "the measurement(s) to be deleted."), + }, + queryParams = { + @OpenApiParam(name = OFFICE, required = true, description = "Specifies the office of the measurements to delete"), + @OpenApiParam(name = BEGIN, required = true, description = "The start of the time " + + "window to delete. The format for this field is ISO 8601 extended, with " + + "optional offset and timezone, i.e., '" + DATE_FORMAT + "', e.g., '" + + EXAMPLE_DATE + "'."), + @OpenApiParam(name = END, required = true, description = "The end of the time " + + "window to delete.The format for this field is ISO 8601 extended, with " + + "optional offset and timezone, i.e., '" + DATE_FORMAT + "', e.g., '" + + EXAMPLE_DATE + "'."), + @OpenApiParam(name = TIMEZONE, description = "This field specifies a default timezone " + + "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."), + }, + description = "Delete an existing measurement.", + method = HttpMethod.DELETE, + tags = {TAG}, + responses = { + @OpenApiResponse(status = "204", description = "Measurement successfully deleted."), + @OpenApiResponse(status = "404", description = "Measurement not found.") + } + ) + @Override + public void delete(@NotNull Context ctx, @NotNull String locationId) { + String officeId = requiredParam(ctx, OFFICE); + String minNum = ctx.queryParam(MIN_NUMBER); + String maxNum = ctx.queryParam(MAX_NUMBER); + Instant minDate = queryParamAsInstant(ctx, BEGIN); + Instant maxDate = queryParamAsInstant(ctx, END); + try (Timer.Context ignored = markAndTime(DELETE)) { + DSLContext dsl = getDslContext(ctx); + MeasurementDao dao = new MeasurementDao(dsl); + dao.deleteMeasurements(officeId, locationId, minDate, maxDate,minNum, maxNum); + ctx.status(HttpServletResponse.SC_NO_CONTENT).json( "Measurements for " + locationId + " Deleted"); + } + } + +} 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 1d1b9e4ee..5aaac5932 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 @@ -59,10 +59,8 @@ import java.time.ZonedDateTime; import java.time.format.DateTimeFormatter; import java.util.ArrayList; -import java.util.Comparator; import java.util.Date; import java.util.TimeZone; -import java.util.stream.Collectors; import mil.army.usace.hec.metadata.location.LocationTemplate; import org.jooq.DSLContext; @@ -72,7 +70,6 @@ import org.jooq.impl.DSL; import usace.cwms.db.dao.util.OracleTypeMap; import usace.cwms.db.jooq.codegen.packages.CWMS_STREAM_PACKAGE; -import static usace.cwms.db.jooq.codegen.tables.AV_STREAMFLOW_MEAS.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; @@ -115,23 +112,6 @@ private static List retrieveMeasurementsJooq(Connection conn, Strin return retVal; } - /** - * Store a measurement - * - * @param measurement - the measurement to store - * @param failIfExists - if true, fail if the measurement already exists - */ - public void storeMeasurement(Measurement measurement, boolean failIfExists) { - connection(dsl, conn -> storeMeasurementJooq(conn, measurement, failIfExists)); - } - - private void storeMeasurementJooq(Connection conn, Measurement measurement, boolean failIfExists) throws SQLException, JsonProcessingException { - setOffice(conn, measurement.getOfficeId()); - String failIfExistsStr = formatBool(failIfExists); - String xml = toDbXml(measurement); - CWMS_STREAM_PACKAGE.call_STORE_MEAS_XML(DSL.using(conn).configuration(), xml, failIfExistsStr); - } - /** * Store a list of measurements * @param measurements - the measurements to store @@ -151,92 +131,27 @@ private void storeMeasurementsJooq(Connection conn, List measuremen } } - /** - * Updates an existing measurement - * @param measurement - the measurement to update - */ - public void updateMeasurement(Measurement measurement) { - connection(dsl, conn -> { - setOffice(conn, measurement.getOfficeId()); - verifyMeasurementExists(conn, measurement); - storeMeasurementJooq(conn, measurement, false); - }); - } - - /** - * Updates a list of existing measurements - * @param measurements - the measurements to update - */ - public void updateMeasurements(List measurements) - { - connection(dsl, conn -> { - if(!measurements.isEmpty()) { - List> measurementsByOffice = new ArrayList<>(measurements.stream() - .collect(Collectors.groupingBy(Measurement::getOfficeId)) - .values()); - for (List measurementsList : measurementsByOffice) { - Measurement measurement = measurements.get(0); - String officeId = measurement.getOfficeId(); - setOffice(conn, officeId); - //group measurementsList by locationId - List> measurementsByLocation = new ArrayList<>(measurementsList.stream() - .collect(Collectors.groupingBy(Measurement::getLocationId)) - .values()); - for (List locationMeasurements : measurementsByLocation) { - String locationId = locationMeasurements.get(0).getLocationId(); - verifyMeasurementsExists(conn, officeId, locationId, locationMeasurements); - storeMeasurementsJooq(conn, locationMeasurements, false); - } - } - } - }); - } - - // Helper method to retrieve existing measurement numbers from the database - private List getExistingMeasurementNumbers(Connection conn, String officeId, String locationId, List measurementNumbers) { - usace.cwms.db.jooq.codegen.tables.AV_STREAMFLOW_MEAS view = AV_STREAMFLOW_MEAS; - return getDslContext(conn, officeId) - .selectDistinct(view.LOCATION_ID, view.OFFICE_ID, view.MEAS_NUMBER) - .from(view) - .where(view.LOCATION_ID.eq(locationId) - .and(view.OFFICE_ID.eq(officeId)) - .and(view.MEAS_NUMBER.in(measurementNumbers))) - .groupBy(view.LOCATION_ID, view.OFFICE_ID, view.MEAS_NUMBER) - .fetch() - .stream() - .map(r -> r.get(view.MEAS_NUMBER)) - .collect(Collectors.toList()); - } - /** * Delete a measurement * * @param officeId - the office id * @param locationId - the location id of the measurement to delete + * @param minNum */ - public void deleteMeasurements(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) { + public void deleteMeasurements(String officeId, String locationId, Instant minDateMask, Instant maxDateMask, String minNum, + 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(); - verifyMeasurementsExists(conn, officeId, locationId, minNum, maxNum); - CWMS_STREAM_PACKAGE.call_DELETE_STREAMFLOW_MEAS(DSL.using(conn).configuration(), locationId, unitSystem, minTimestamp, maxTimestamp, - minHeight, maxHeight, minFlow, maxFlow, minNum, maxNum, agencies, qualities, timeZoneId, officeId); + 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 verifyMeasurementExists(Connection conn, Measurement measurement) { - List measurements = retrieveMeasurementsJooq(conn, measurement.getOfficeId(), measurement.getLocationId(), UnitSystem.EN.toString(), - null, null, null, null, measurement.getNumber(), measurement.getNumber(), null, null, null, null, OracleTypeMap.GMT_TIME_ZONE); - if (measurements.isEmpty() || measurements.stream().noneMatch(lt -> lt.getNumber().equals(measurement.getNumber()))) { - throw new NotFoundException("Could not find measurement."); - } - } - private void verifyMeasurementsExists(Connection conn, String officeId, String locationId, String minNum, String maxNum) { List measurements = retrieveMeasurementsJooq(conn, officeId, locationId, UnitSystem.EN.toString(), null, null, null, null, minNum, maxNum, null, null, null, null, OracleTypeMap.GMT_TIME_ZONE); @@ -245,34 +160,11 @@ private void verifyMeasurementsExists(Connection conn, String officeId, String l } } - private void verifyMeasurementsExists(Connection conn, String officeId, String locationId, List locationMeasurements) { - List measurementNumbers = locationMeasurements.stream() - .map(Measurement::getNumber) - .collect(Collectors.toList()); - - // Retrieve existing measurements from the database - List existingNumbers = getExistingMeasurementNumbers(conn, officeId, locationId, measurementNumbers); - - // Find missing numbers - List missingNumbers = new ArrayList<>(measurementNumbers); - missingNumbers.removeAll(existingNumbers); - - if (!missingNumbers.isEmpty()) { - throw new NotFoundException("Could not find measurements " + String.join(",", missingNumbers) + - " for " + locationId + " in office " + officeId + "."); - } - } - static String toDbXml(List measurements) throws JsonProcessingException { MeasurementsXmlDto xmlDto = convertMeasurementsToXmlDto(measurements); return XML_MAPPER.writeValueAsString(xmlDto); } - static String toDbXml(Measurement measurement) throws JsonProcessingException { - MeasurementXmlDto xmlDto = convertMeasurementToXmlDto(measurement); - return XML_MAPPER.writeValueAsString(xmlDto); - } - static Measurement fromJooqMeasurementRecord(STREAMFLOW_MEAS2_T record) { LocationTemplate locationTemplate = new LocationTemplate(record.getLOCATION().getOFFICE_ID(), record.getLOCATION().getBASE_LOCATION_ID(), record.getLOCATION().getSUB_LOCATION_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 1df9a1675..491b035b9 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.JsonFormat; import com.fasterxml.jackson.annotation.JsonIgnore; import com.fasterxml.jackson.annotation.JsonInclude; import com.fasterxml.jackson.annotation.JsonProperty; @@ -128,6 +129,7 @@ public CwmsId getId() { return id; } + @JsonFormat(shape = JsonFormat.Shape.STRING) public Instant getInstant() { return instant; } diff --git a/cwms-data-api/src/test/java/cwms/cda/api/ControllersTest.java b/cwms-data-api/src/test/java/cwms/cda/api/ControllersTest.java index 2b07b7a2c..39a0c65f9 100644 --- a/cwms-data-api/src/test/java/cwms/cda/api/ControllersTest.java +++ b/cwms-data-api/src/test/java/cwms/cda/api/ControllersTest.java @@ -413,4 +413,25 @@ void testRequiredParamAs() { assertThrows(RequiredQueryParameterException.class, () -> Controllers.requiredParamAs(ctx, Controllers.OFFICE, String.class)); } + + @Test + void testQueryParamAsDouble() { + final HttpServletRequest request = mock(HttpServletRequest.class); + final HttpServletResponse response = mock(HttpServletResponse.class); + Map urlParams = new LinkedHashMap<>(); + urlParams.put("a_double", "1.0"); + urlParams.put("an_int", "1"); + String paramStr = ControllerTest.buildParamStr(urlParams); + when(request.getQueryString()).thenReturn(paramStr); + Context ctx = new Context(request, response, new LinkedHashMap()); + + Double retVal = Controllers.queryParamAsDouble(ctx, "a_double"); + assertEquals(1.0, retVal); + + Double retVal2 = Controllers.queryParamAsDouble(ctx, "an_int"); + assertEquals(1.0, retVal2); + + Double retVal3 = Controllers.queryParamAsDouble(ctx, "null"); + assertNull(retVal3); + } } \ No newline at end of file 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 new file mode 100644 index 000000000..aab51f262 --- /dev/null +++ b/cwms-data-api/src/test/java/cwms/cda/api/MeasurementControllerTestIT.java @@ -0,0 +1,431 @@ +/* + * 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.google.common.flogger.FluentLogger; +import cwms.cda.api.enums.UnitSystem; +import static cwms.cda.data.dao.DaoTest.getDslContext; +import cwms.cda.data.dao.DeleteRule; +import cwms.cda.data.dao.MeasurementDao; +import static cwms.cda.data.dao.MeasurementDaoTestIT.MINIMUM_SCHEMA; +import cwms.cda.data.dao.StreamDao; +import cwms.cda.data.dto.CwmsId; +import cwms.cda.data.dto.measurement.Measurement; +import cwms.cda.data.dto.stream.Stream; +import cwms.cda.formatters.ContentType; +import cwms.cda.formatters.Formats; +import static cwms.cda.security.KeyAccessManager.AUTH_HEADER; +import fixtures.CwmsDataApiSetupCallback; +import fixtures.MinimumSchema; +import fixtures.TestAccounts; +import static io.restassured.RestAssured.given; +import io.restassured.filter.log.LogDetail; +import java.io.IOException; +import java.io.InputStream; +import java.nio.charset.StandardCharsets; +import java.sql.SQLException; +import java.time.Instant; +import java.util.ArrayList; +import java.util.List; +import javax.servlet.http.HttpServletResponse; +import mil.army.usace.hec.test.database.CwmsDatabaseContainer; +import org.apache.commons.io.IOUtils; +import org.junit.jupiter.api.AfterAll; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.Tag; +import org.junit.jupiter.api.Test; +import static org.hamcrest.Matchers.*; + +@Tag("integration") +final class MeasurementControllerTestIT extends DataApiTestIT { + + + private static final FluentLogger LOGGER = FluentLogger.forEnclosingClass(); + private static final String OFFICE_ID = TestAccounts.KeyUser.SPK_NORMAL.getOperatingOffice(); + private static final List TEST_STREAMS = new ArrayList<>(); + private static final List TEST_STREAM_LOC_IDS = new ArrayList<>(); + + @BeforeAll + public static void setup() throws SQLException { + String testLoc = "StreamLoc321"; // match the stream location name in the json file + createLocation(testLoc, true, OFFICE_ID, "STREAM_LOCATION"); + TEST_STREAM_LOC_IDS.add(testLoc); + createAndStoreTestStream("ImOnThisStream2"); + } + + static void createAndStoreTestStream(String testLoc) throws SQLException { + createLocation(testLoc, true, OFFICE_ID, "STREAM"); + CwmsDatabaseContainer db = CwmsDataApiSetupCallback.getDatabaseLink(); + db.connection(c -> { + StreamDao streamDao = new StreamDao(getDslContext(c, OFFICE_ID)); + Stream streamToStore = new Stream.Builder() + .withId(new CwmsId.Builder() + .withOfficeId(OFFICE_ID) + .withName(testLoc) + .build()) + .withLength(100.0) + .withLengthUnits("km") + .build(); + TEST_STREAMS.add(streamToStore); + streamDao.storeStream(streamToStore, false); + }, CwmsDataApiSetupCallback.getWebUser()); + } + + @AfterAll + public static void tearDown() { + for (Stream stream : TEST_STREAMS) { + try { + CwmsDatabaseContainer db = CwmsDataApiSetupCallback.getDatabaseLink(); + db.connection(c -> { + StreamDao streamDao = new StreamDao(getDslContext(c, OFFICE_ID)); + try { + streamDao.deleteStream(stream.getId().getOfficeId(), stream.getId().getName(), DeleteRule.DELETE_ALL); + } catch (Exception e) { + LOGGER.atInfo().log("Failed to delete stream: " + stream.getId().getName() + ". Stream likely already deleted"); + } + }, CwmsDataApiSetupCallback.getWebUser()); + } catch (SQLException ex) { + throw new RuntimeException(ex); + } + } + TEST_STREAMS.clear(); + for(String measLoc: TEST_STREAM_LOC_IDS) + { + try { + CwmsDatabaseContainer db = CwmsDataApiSetupCallback.getDatabaseLink(); + db.connection(c -> { + MeasurementDao measDao = new MeasurementDao(getDslContext(c, OFFICE_ID)); + try { + measDao.deleteMeasurements(OFFICE_ID, measLoc, null, null, null, null); + } catch (Exception e) { + LOGGER.atInfo().log("Failed to delete measurements for: " + measLoc + ". Measurement(s) likely already deleted"); + } + }, CwmsDataApiSetupCallback.getWebUser()); + } catch (SQLException ex) { + throw new RuntimeException(ex); + } + } + } + + @Test + @MinimumSchema(MINIMUM_SCHEMA) + void test_create_retrieve_delete_measurement() throws IOException { + InputStream resource = this.getClass().getResourceAsStream("/cwms/cda/api/measurement.json"); + assertNotNull(resource); + String json = IOUtils.toString(resource, StandardCharsets.UTF_8); + assertNotNull(json); + List measurements = Formats.parseContentList(new ContentType(Formats.JSON), json, Measurement.class); + Measurement measurement = measurements.get(0); + TestAccounts.KeyUser user = TestAccounts.KeyUser.SPK_NORMAL; + + // Create the Measurement + given() + .log().ifValidationFails(LogDetail.ALL, true) + .accept(Formats.JSON) + .contentType(Formats.JSON) + .body(json) + .header(AUTH_HEADER, user.toHeaderValue()) + .when() + .redirects().follow(true) + .redirects().max(3) + .post("/measurements/") + .then() + .log().ifValidationFails(LogDetail.ALL, true) + .assertThat() + .statusCode(is(HttpServletResponse.SC_CREATED)); + + String locationId = measurement.getLocationId(); + String number = measurement.getNumber(); + + // Retrieve the Measurement and assert that it exists + given() + .log().ifValidationFails(LogDetail.ALL, true) + .accept(Formats.JSON) + .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.MIN_HEIGHT, 0.0) + .queryParam(Controllers.MAX_FLOW, 1000) + .queryParam(Controllers.UNIT_SYSTEM, UnitSystem.EN.getValue()) + .when() + .redirects().follow(true) + .redirects().max(3) + .get("/measurements/") + .then() + .log().ifValidationFails(LogDetail.ALL, true) + .assertThat() + .statusCode(is(HttpServletResponse.SC_OK)) + .body("[0].height-unit", equalTo(measurement.getHeightUnit())) + .body("[0].flow-unit", equalTo(measurement.getFlowUnit())) + .body("[0].temp-unit", equalTo(measurement.getTempUnit())) + .body("[0].velocity-unit", equalTo(measurement.getVelocityUnit())) + .body("[0].area-unit", equalTo(measurement.getAreaUnit())) + .body("[0].used", equalTo(measurement.isUsed())) + .body("[0].agency", equalTo(measurement.getAgency())) + .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].id.name", equalTo(measurement.getLocationId())) + .body("[0].id.office-id", equalTo(measurement.getOfficeId())) + .body("[0].streamflow-measurement.gage-height", equalTo(measurement.getStreamflowMeasurement().getGageHeight().floatValue())) + .body("[0].streamflow-measurement.flow", equalTo(measurement.getStreamflowMeasurement().getFlow().floatValue())) + .body("[0].streamflow-measurement.quality", equalTo(measurement.getStreamflowMeasurement().getQuality())) + .body("[0].supplemental-streamflow-measurement.channel-flow", equalTo(measurement.getSupplementalStreamflowMeasurement().getChannelFlow().floatValue())) + .body("[0].supplemental-streamflow-measurement.overbank-flow", equalTo(measurement.getSupplementalStreamflowMeasurement().getOverbankFlow().floatValue())) + .body("[0].supplemental-streamflow-measurement.overbank-max-depth", equalTo(measurement.getSupplementalStreamflowMeasurement().getOverbankMaxDepth().floatValue())) + .body("[0].supplemental-streamflow-measurement.channel-max-depth", equalTo(measurement.getSupplementalStreamflowMeasurement().getChannelMaxDepth().floatValue())) + .body("[0].supplemental-streamflow-measurement.avg-velocity", equalTo(measurement.getSupplementalStreamflowMeasurement().getAvgVelocity().floatValue())) + .body("[0].supplemental-streamflow-measurement.surface-velocity", equalTo(measurement.getSupplementalStreamflowMeasurement().getSurfaceVelocity().floatValue())) + .body("[0].supplemental-streamflow-measurement.max-velocity", equalTo(measurement.getSupplementalStreamflowMeasurement().getMaxVelocity().floatValue())) + .body("[0].supplemental-streamflow-measurement.effective-flow-area", equalTo(measurement.getSupplementalStreamflowMeasurement().getEffectiveFlowArea().floatValue())) + .body("[0].supplemental-streamflow-measurement.cross-sectional-area", equalTo(measurement.getSupplementalStreamflowMeasurement().getCrossSectionalArea().floatValue())) + .body("[0].supplemental-streamflow-measurement.mean-gage", equalTo(measurement.getSupplementalStreamflowMeasurement().getMeanGage().floatValue())) + .body("[0].supplemental-streamflow-measurement.top-width", equalTo(measurement.getSupplementalStreamflowMeasurement().getTopWidth().floatValue())) + .body("[0].supplemental-streamflow-measurement.main-channel-area", equalTo(measurement.getSupplementalStreamflowMeasurement().getMainChannelArea().floatValue())) + .body("[0].supplemental-streamflow-measurement.overbank-area", equalTo(measurement.getSupplementalStreamflowMeasurement().getOverbankArea().floatValue())) + .body("[0].usgs-measurement.remarks", equalTo(measurement.getUsgsMeasurement().getRemarks())) + .body("[0].usgs-measurement.current-rating", equalTo(measurement.getUsgsMeasurement().getCurrentRating())) + .body("[0].usgs-measurement.control-condition", equalTo(measurement.getUsgsMeasurement().getControlCondition())) + .body("[0].usgs-measurement.flow-adjustment", equalTo(measurement.getUsgsMeasurement().getFlowAdjustment())) + .body("[0].usgs-measurement.shift-used", equalTo(measurement.getUsgsMeasurement().getShiftUsed().floatValue())) + .body("[0].usgs-measurement.percent-difference", equalTo(measurement.getUsgsMeasurement().getPercentDifference().floatValue())) + .body("[0].usgs-measurement.delta-height", equalTo(measurement.getUsgsMeasurement().getDeltaHeight().floatValue())) + .body("[0].usgs-measurement.delta-time", equalTo(measurement.getUsgsMeasurement().getDeltaTime().floatValue())) + .body("[0].usgs-measurement.air-temp", equalTo(measurement.getUsgsMeasurement().getAirTemp().floatValue())) + .body("[0].usgs-measurement.water-temp", equalTo(measurement.getUsgsMeasurement().getWaterTemp().floatValue())); + + // Delete the Measurement + given() + .log().ifValidationFails(LogDetail.ALL, true) + .accept(Formats.JSON) + .header(AUTH_HEADER, user.toHeaderValue()) + .queryParam(Controllers.OFFICE, measurement.getId().getOfficeId()) + .queryParam(Controllers.MIN_NUMBER, number) + .queryParam(Controllers.MAX_NUMBER, number) + .when() + .redirects().follow(true) + .redirects().max(3) + .delete("/measurements/" + locationId) + .then() + .log().ifValidationFails(LogDetail.ALL, true) + .assertThat() + .statusCode(is(HttpServletResponse.SC_NO_CONTENT)); + + // Retrieve the Measurement and assert that it does not exist + given() + .log().ifValidationFails(LogDetail.ALL, true) + .accept(Formats.JSON) + .queryParam(Controllers.OFFICE, measurement.getId().getOfficeId()) + .queryParam(Controllers.ID_MASK, measurement.getLocationId()) + .queryParam(Controllers.MIN_NUMBER, number) + .queryParam(Controllers.MAX_NUMBER, number) + .queryParam(Controllers.UNIT_SYSTEM, UnitSystem.EN.getValue()) + .when() + .redirects().follow(true) + .redirects().max(3) + .get("/measurements/") + .then() + .log().ifValidationFails(LogDetail.ALL, true) + .assertThat() + .statusCode(is(HttpServletResponse.SC_NOT_FOUND)); + } + + @Test + @MinimumSchema(MINIMUM_SCHEMA) + void test_create_retrieve_delete_measurement_multiple() throws IOException { + InputStream resource = this.getClass().getResourceAsStream("/cwms/cda/api/measurements.json"); + assertNotNull(resource); + String json = IOUtils.toString(resource, StandardCharsets.UTF_8); + assertNotNull(json); + List measurements = Formats.parseContentList(new ContentType(Formats.JSON), json, Measurement.class); + + Measurement measurement1 = measurements.get(0); + Measurement measurement2 = measurements.get(1); + TestAccounts.KeyUser user = TestAccounts.KeyUser.SPK_NORMAL; + + // Create the Measurements + given() + .log().ifValidationFails(LogDetail.ALL, true) + .accept(Formats.JSON) + .contentType(Formats.JSON) + .body(json) + .header(AUTH_HEADER, user.toHeaderValue()) + .when() + .redirects().follow(true) + .redirects().max(3) + .post("/measurements/") + .then() + .log().ifValidationFails(LogDetail.ALL, true) + .assertThat() + .statusCode(is(HttpServletResponse.SC_CREATED)); + + // Retrieve the Measurements and assert that they exists + given() + .log().ifValidationFails(LogDetail.ALL, true) + .accept(Formats.JSON) + .queryParam(Controllers.OFFICE_MASK, measurement1.getId().getOfficeId()) + .queryParam(Controllers.ID_MASK, measurement1.getLocationId()) + .queryParam(Controllers.UNIT_SYSTEM, UnitSystem.EN.getValue()) + .when() + .redirects().follow(true) + .redirects().max(3) + .get("/measurements/") + .then() + .log().ifValidationFails(LogDetail.ALL, true) + .assertThat() + .statusCode(is(HttpServletResponse.SC_OK)) + .body("[0].height-unit", equalTo(measurement1.getHeightUnit())) + .body("[0].flow-unit", equalTo(measurement1.getFlowUnit())) + .body("[0].temp-unit", equalTo(measurement1.getTempUnit())) + .body("[0].velocity-unit", equalTo(measurement1.getVelocityUnit())) + .body("[0].area-unit", equalTo(measurement1.getAreaUnit())) + .body("[0].used", equalTo(measurement1.isUsed())) + .body("[0].agency", equalTo(measurement1.getAgency())) + .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].id.name", equalTo(measurement1.getLocationId())) + .body("[0].id.office-id", equalTo(measurement1.getOfficeId())) + .body("[0].streamflow-measurement.gage-height", equalTo(measurement1.getStreamflowMeasurement().getGageHeight().floatValue())) + .body("[0].streamflow-measurement.flow", equalTo(measurement1.getStreamflowMeasurement().getFlow().floatValue())) + .body("[0].streamflow-measurement.quality", equalTo(measurement1.getStreamflowMeasurement().getQuality())) + .body("[0].supplemental-streamflow-measurement.channel-flow", equalTo(measurement1.getSupplementalStreamflowMeasurement().getChannelFlow().floatValue())) + .body("[0].supplemental-streamflow-measurement.overbank-flow", equalTo(measurement1.getSupplementalStreamflowMeasurement().getOverbankFlow().floatValue())) + .body("[0].supplemental-streamflow-measurement.overbank-max-depth", equalTo(measurement1.getSupplementalStreamflowMeasurement().getOverbankMaxDepth().floatValue())) + .body("[0].supplemental-streamflow-measurement.channel-max-depth", equalTo(measurement1.getSupplementalStreamflowMeasurement().getChannelMaxDepth().floatValue())) + .body("[0].supplemental-streamflow-measurement.avg-velocity", equalTo(measurement1.getSupplementalStreamflowMeasurement().getAvgVelocity().floatValue())) + .body("[0].supplemental-streamflow-measurement.surface-velocity", equalTo(measurement1.getSupplementalStreamflowMeasurement().getSurfaceVelocity().floatValue())) + .body("[0].supplemental-streamflow-measurement.max-velocity", equalTo(measurement1.getSupplementalStreamflowMeasurement().getMaxVelocity().floatValue())) + .body("[0].supplemental-streamflow-measurement.effective-flow-area", equalTo(measurement1.getSupplementalStreamflowMeasurement().getEffectiveFlowArea().floatValue())) + .body("[0].supplemental-streamflow-measurement.cross-sectional-area", equalTo(measurement1.getSupplementalStreamflowMeasurement().getCrossSectionalArea().floatValue())) + .body("[0].supplemental-streamflow-measurement.mean-gage", equalTo(measurement1.getSupplementalStreamflowMeasurement().getMeanGage().floatValue())) + .body("[0].supplemental-streamflow-measurement.top-width", equalTo(measurement1.getSupplementalStreamflowMeasurement().getTopWidth().floatValue())) + .body("[0].supplemental-streamflow-measurement.main-channel-area", equalTo(measurement1.getSupplementalStreamflowMeasurement().getMainChannelArea().floatValue())) + .body("[0].supplemental-streamflow-measurement.overbank-area", equalTo(measurement1.getSupplementalStreamflowMeasurement().getOverbankArea().floatValue())) + .body("[0].usgs-measurement.remarks", equalTo(measurement1.getUsgsMeasurement().getRemarks())) + .body("[0].usgs-measurement.current-rating", equalTo(measurement1.getUsgsMeasurement().getCurrentRating())) + .body("[0].usgs-measurement.control-condition", equalTo(measurement1.getUsgsMeasurement().getControlCondition())) + .body("[0].usgs-measurement.flow-adjustment", equalTo(measurement1.getUsgsMeasurement().getFlowAdjustment())) + .body("[0].usgs-measurement.shift-used", equalTo(measurement1.getUsgsMeasurement().getShiftUsed().floatValue())) + .body("[0].usgs-measurement.percent-difference", equalTo(measurement1.getUsgsMeasurement().getPercentDifference().floatValue())) + .body("[0].usgs-measurement.delta-height", equalTo(measurement1.getUsgsMeasurement().getDeltaHeight().floatValue())) + .body("[0].usgs-measurement.delta-time", equalTo(measurement1.getUsgsMeasurement().getDeltaTime().floatValue())) + .body("[0].usgs-measurement.air-temp", equalTo(measurement1.getUsgsMeasurement().getAirTemp().floatValue())) + .body("[0].usgs-measurement.water-temp", equalTo(measurement1.getUsgsMeasurement().getWaterTemp().floatValue())) + .body("[1].height-unit", equalTo(measurement2.getHeightUnit())) + .body("[1].flow-unit", equalTo(measurement2.getFlowUnit())) + .body("[1].temp-unit", equalTo(measurement2.getTempUnit())) + .body("[1].velocity-unit", equalTo(measurement2.getVelocityUnit())) + .body("[1].area-unit", equalTo(measurement2.getAreaUnit())) + .body("[1].used", equalTo(measurement2.isUsed())) + .body("[1].agency", equalTo(measurement2.getAgency())) + .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].id.name", equalTo(measurement2.getLocationId())) + .body("[1].id.office-id", equalTo(measurement2.getOfficeId())) + .body("[1].streamflow-measurement.gage-height", equalTo(measurement2.getStreamflowMeasurement().getGageHeight().floatValue())) + .body("[1].streamflow-measurement.flow", equalTo(measurement2.getStreamflowMeasurement().getFlow().floatValue())) + .body("[1].streamflow-measurement.quality", equalTo(measurement2.getStreamflowMeasurement().getQuality())) + .body("[1].supplemental-streamflow-measurement.channel-flow", equalTo(measurement2.getSupplementalStreamflowMeasurement().getChannelFlow().floatValue())) + .body("[1].supplemental-streamflow-measurement.overbank-flow", equalTo(measurement2.getSupplementalStreamflowMeasurement().getOverbankFlow().floatValue())) + .body("[1].supplemental-streamflow-measurement.overbank-max-depth", equalTo(measurement2.getSupplementalStreamflowMeasurement().getOverbankMaxDepth().floatValue())) + .body("[1].supplemental-streamflow-measurement.channel-max-depth", equalTo(measurement2.getSupplementalStreamflowMeasurement().getChannelMaxDepth().floatValue())) + .body("[1].supplemental-streamflow-measurement.avg-velocity", equalTo(measurement2.getSupplementalStreamflowMeasurement().getAvgVelocity().floatValue())) + .body("[1].supplemental-streamflow-measurement.surface-velocity", equalTo(measurement2.getSupplementalStreamflowMeasurement().getSurfaceVelocity().floatValue())) + .body("[1].supplemental-streamflow-measurement.max-velocity", equalTo(measurement2.getSupplementalStreamflowMeasurement().getMaxVelocity().floatValue())) + .body("[1].supplemental-streamflow-measurement.effective-flow-area", equalTo(measurement2.getSupplementalStreamflowMeasurement().getEffectiveFlowArea().floatValue())) + .body("[1].supplemental-streamflow-measurement.cross-sectional-area", equalTo(measurement2.getSupplementalStreamflowMeasurement().getCrossSectionalArea().floatValue())) + .body("[1].supplemental-streamflow-measurement.mean-gage", equalTo(measurement2.getSupplementalStreamflowMeasurement().getMeanGage().floatValue())) + .body("[1].supplemental-streamflow-measurement.top-width", equalTo(measurement2.getSupplementalStreamflowMeasurement().getTopWidth().floatValue())) + .body("[1].supplemental-streamflow-measurement.main-channel-area", equalTo(measurement2.getSupplementalStreamflowMeasurement().getMainChannelArea().floatValue())) + .body("[1].supplemental-streamflow-measurement.overbank-area", equalTo(measurement2.getSupplementalStreamflowMeasurement().getOverbankArea().floatValue())) + .body("[1].usgs-measurement.remarks", equalTo(measurement2.getUsgsMeasurement().getRemarks())) + .body("[1].usgs-measurement.current-rating", equalTo(measurement2.getUsgsMeasurement().getCurrentRating())) + .body("[1].usgs-measurement.control-condition", equalTo(measurement2.getUsgsMeasurement().getControlCondition())) + .body("[1].usgs-measurement.flow-adjustment", equalTo(measurement2.getUsgsMeasurement().getFlowAdjustment())) + .body("[1].usgs-measurement.shift-used", equalTo(measurement2.getUsgsMeasurement().getShiftUsed().floatValue())) + .body("[1].usgs-measurement.percent-difference", equalTo(measurement2.getUsgsMeasurement().getPercentDifference().floatValue())) + .body("[1].usgs-measurement.delta-height", equalTo(measurement2.getUsgsMeasurement().getDeltaHeight().floatValue())) + .body("[1].usgs-measurement.delta-time", equalTo(measurement2.getUsgsMeasurement().getDeltaTime().floatValue())) + .body("[1].usgs-measurement.air-temp", equalTo(measurement2.getUsgsMeasurement().getAirTemp().floatValue())) + .body("[1].usgs-measurement.water-temp", equalTo(measurement2.getUsgsMeasurement().getWaterTemp().floatValue())); + + // Delete the Measurements + given() + .log().ifValidationFails(LogDetail.ALL, true) + .accept(Formats.JSON) + .header(AUTH_HEADER, user.toHeaderValue()) + .queryParam(Controllers.OFFICE, OFFICE_ID) + .when() + .redirects().follow(true) + .redirects().max(3) + .delete("/measurements/" + measurement1.getLocationId()) + .then() + .log().ifValidationFails(LogDetail.ALL, true) + .assertThat() + .statusCode(is(HttpServletResponse.SC_NO_CONTENT)); + + // Retrieve the Measurements and assert that they do not exist + given() + .log().ifValidationFails(LogDetail.ALL, true) + .accept(Formats.JSON) + .queryParam(Controllers.OFFICE, OFFICE_ID) + .queryParam(Controllers.ID_MASK, measurement1.getLocationId()) + .queryParam(Controllers.UNIT_SYSTEM, UnitSystem.EN.getValue()) + .when() + .redirects().follow(true) + .redirects().max(3) + .get("/measurements/") + .then() + .log().ifValidationFails(LogDetail.ALL, true) + .assertThat() + .statusCode(is(HttpServletResponse.SC_NOT_FOUND)); + } + + @Test + @MinimumSchema(MINIMUM_SCHEMA) + void test_delete_does_not_exist() { + TestAccounts.KeyUser user = TestAccounts.KeyUser.SPK_NORMAL; + // Delete a Measurement + 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/" + Instant.now().toEpochMilli()) + .then() + .log().ifValidationFails(LogDetail.ALL,true) + .assertThat() + .statusCode(is(HttpServletResponse.SC_NOT_FOUND)); + } + +} 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 95ead4790..0007a3b0b 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 @@ -115,23 +115,6 @@ void testConvertToXmlMeasurementDto() assertMatch(meas, xmlDto); } - @Test - void testToDbXml() throws Exception - { - InputStream resource = this.getClass().getResourceAsStream("/cwms/cda/data/dao/dbMeasurement.xml"); - assertNotNull(resource); - String expectedXml = IOUtils.toString(resource, StandardCharsets.UTF_8); - MeasurementDao.MeasurementXmlDto expectedXmlDto = MeasurementDao.XML_MAPPER.readValue(expectedXml, MeasurementDao.MeasurementXmlDto.class); - - Measurement meas = buildTestMeasurement(); - String xml = MeasurementDao.toDbXml(meas); - assertNotNull(xml); - assertFalse(xml.isEmpty()); - MeasurementDao.MeasurementXmlDto actualXmlDto = MeasurementDao.XML_MAPPER.readValue(xml, MeasurementDao.MeasurementXmlDto.class); - - assertMatch(expectedXmlDto, actualXmlDto); - } - @Test void testConvertToMeasurementsXmlDto() { Measurement meas1 = buildTestMeasurement(); 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 b8fec3445..30331c1c7 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 @@ -14,6 +14,7 @@ import cwms.cda.data.dto.stream.StreamLocation; import cwms.cda.helpers.DTOMatch; import fixtures.CwmsDataApiSetupCallback; +import fixtures.MinimumSchema; import fixtures.TestAccounts; import java.sql.SQLException; import java.time.Instant; @@ -27,16 +28,16 @@ import static org.junit.jupiter.api.Assertions.assertNotNull; import static org.junit.jupiter.api.Assertions.assertThrows; import org.junit.jupiter.api.BeforeAll; -import org.junit.jupiter.api.Disabled; import org.junit.jupiter.api.Tag; import org.junit.jupiter.api.Test; @Tag("integration") -final class MeasurementDaoTestIT extends DataApiTestIT { +public final class MeasurementDaoTestIT extends DataApiTestIT { private static final String OFFICE_ID = TestAccounts.KeyUser.SPK_NORMAL.getOperatingOffice(); private static final List STREAM_LOC_IDS = new ArrayList<>(); private static final List STREAMS_CREATED = new ArrayList<>(); + public static final int MINIMUM_SCHEMA = 999999; @BeforeAll public static void setup() { @@ -79,97 +80,8 @@ public static void tearDown() { } @Test - @Disabled - void testRoundTrip() throws Exception { - CwmsDatabaseContainer databaseLink = CwmsDataApiSetupCallback.getDatabaseLink(); - String webUser = CwmsDataApiSetupCallback.getWebUser(); - databaseLink.connection(c -> { - DSLContext context = getDslContext(c, databaseLink.getOfficeId()); - StreamLocationDao streamLocationDao = new StreamLocationDao(context); - //build stream locations - String streamLocId = STREAM_LOC_IDS.get(0); - StreamLocation streamLocation = StreamLocationDaoTestIT.buildTestStreamLocation("TEST_STREAM_123", streamLocId, OFFICE_ID, 10.0, Bank.LEFT); - String streamLocId2 = STREAM_LOC_IDS.get(1); - StreamLocation streamLocation2 = StreamLocationDaoTestIT.buildTestStreamLocation("TEST_STREAM_123", streamLocId2, OFFICE_ID, 11.0, Bank.RIGHT); - - try { - //store stream locations - streamLocationDao.storeStreamLocation(streamLocation, false); - streamLocationDao.storeStreamLocation(streamLocation2, false); - - Measurement meas1 = buildMeasurement1(streamLocId); - Measurement meas1B = buildMeasurement2(streamLocId); - - Measurement meas2 = buildMeasurement1(streamLocId2); - - MeasurementDao measurementDao = new MeasurementDao(context); - measurementDao.storeMeasurement(meas1, false); - measurementDao.storeMeasurement(meas1B, false); - measurementDao.storeMeasurement(meas2, false); - - List measurements = measurementDao.retrieveMeasurements(OFFICE_ID, streamLocId, null, null, UnitSystem.EN.getValue(), - null, null, null, null, null, null, null, null); - assertEquals(2, measurements.size()); - - DTOMatch.assertMatch(meas1, measurements.get(0)); - DTOMatch.assertMatch(meas1B, measurements.get(1)); - - List measurementsAll = measurementDao.retrieveMeasurements(OFFICE_ID, null, null, null, UnitSystem.EN.getValue(), - null, null, null, null, null, null, null, null); - List meas1List = measurementsAll.stream() - .filter(m -> m.getLocationId().equals(streamLocId)) - .collect(Collectors.toList()); - assertEquals(2, meas1List.size()); - DTOMatch.assertMatch(meas1, meas1List.get(0)); - DTOMatch.assertMatch(meas1B, meas1List.get(1)); - - Measurement meas2Found = measurementsAll.stream() - .filter(m -> m.getLocationId().equals(streamLocId2)) - .findFirst() - .orElse(null); - assertNotNull(meas2Found); - DTOMatch.assertMatch(meas2, meas2Found); - - //test update - meas1 = buildMeasurement1(streamLocId, 200); - measurementDao.updateMeasurement(meas1); - - List retrievedMeasurements = measurementDao.retrieveMeasurements(OFFICE_ID, streamLocId, null, null, UnitSystem.EN.getValue(), - null, null, null, null, meas1.getNumber(), meas1.getNumber(), null, null); - DTOMatch.assertMatch(meas1, retrievedMeasurements.get(0)); - - Measurement doesntExist = buildMeasurementDoesntExist(streamLocId); - assertThrows(NotFoundException.class, () -> measurementDao.updateMeasurement(doesntExist)); - - //delete measurements - measurementDao.deleteMeasurements(meas1.getId().getOfficeId(), meas1.getId().getName(), null, null, null, null, null, null, null, null, null, null, null); - measurementDao.deleteMeasurements(meas2.getId().getOfficeId(), meas2.getId().getName(), null, null, null, null, null, null, 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)); - assertThrows(NotFoundException.class, () -> measurementDao.retrieveMeasurements(meas2F.getId().getOfficeId(), meas2F.getId().getName(), - null, null, UnitSystem.EN.getValue(), null, null, null, null, null, null, null, null)); - } finally { - //delete stream locations - streamLocationDao.deleteStreamLocation( - streamLocation.getStreamLocationNode().getId().getOfficeId(), - streamLocation.getStreamLocationNode().getStreamNode().getStreamId().getName(), - streamLocation.getStreamLocationNode().getId().getName() - ); - streamLocationDao.deleteStreamLocation( - streamLocation2.getStreamLocationNode().getId().getOfficeId(), - streamLocation2.getStreamLocationNode().getStreamNode().getStreamId().getName(), - streamLocation2.getStreamLocationNode().getId().getName() - ); - } - }, webUser); - } - - @Test - @Disabled - void testRoundTripMultipleStore() throws Exception { + @MinimumSchema(MINIMUM_SCHEMA) + void testRoundTripStore() throws Exception { CwmsDatabaseContainer databaseLink = CwmsDataApiSetupCallback.getDatabaseLink(); String webUser = CwmsDataApiSetupCallback.getWebUser(); databaseLink.connection(c -> { @@ -221,14 +133,6 @@ void testRoundTripMultipleStore() throws Exception { assertNotNull(meas2Found); DTOMatch.assertMatch(meas2, meas2Found); - //test update - meas1 = buildMeasurement1(streamLocId, 400); - meas1B = buildMeasurement2(streamLocId, 500); - List updatedMeasurements = new ArrayList<>(); - updatedMeasurements.add(meas1); - updatedMeasurements.add(meas1B); - measurementDao.updateMeasurements(updatedMeasurements); - retrievedMeasurements = measurementDao.retrieveMeasurements(OFFICE_ID, streamLocId, null, null, UnitSystem.EN.getValue(), null, null, null, null, null, null, null, null); @@ -247,13 +151,9 @@ void testRoundTripMultipleStore() throws Exception { assertNotNull(retrievedMeas1B); DTOMatch.assertMatch(meas1B, retrievedMeas1B); - Measurement doesntExist = buildMeasurementDoesntExist(streamLocId); - updatedMeasurements.add(doesntExist); - assertThrows(NotFoundException.class, () -> measurementDao.updateMeasurements(updatedMeasurements)); - //delete measurements - measurementDao.deleteMeasurements(meas1.getId().getOfficeId(), meas1.getId().getName(), null, null, null, null, null, null, null, null, null, null, null); - measurementDao.deleteMeasurements(meas2.getId().getOfficeId(), meas2.getId().getName(), null, null, null, null, null, null, null, null, null, null, null); + measurementDao.deleteMeasurements(meas1.getId().getOfficeId(), meas1.getId().getName(), null, null, null, null); + measurementDao.deleteMeasurements(meas2.getId().getOfficeId(), meas2.getId().getName(), null, null, null, null); final Measurement meas1F = meas1; final Measurement meas2F = meas2; 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 new file mode 100644 index 000000000..0c23d00c6 --- /dev/null +++ b/cwms-data-api/src/test/resources/cwms/cda/api/measurement.json @@ -0,0 +1,51 @@ +[ + { + "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", + "number": "123456", + "id": { + "name": "StreamLoc321", + "office-id": "SPK" + }, + "streamflow-measurement": { + "gage-height": 5.5, + "flow": 250.0, + "quality": "G" + }, + "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 + } + } +] 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 new file mode 100644 index 000000000..3db3ce30b --- /dev/null +++ b/cwms-data-api/src/test/resources/cwms/cda/api/measurements.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", + "number": "123456", + "id": { + "name": "StreamLoc321", + "office-id": "SPK" + }, + "streamflow-measurement": { + "gage-height": 5.5, + "flow": 250.0, + "quality": "G" + }, + "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", + "number": "654321", + "id": { + "name": "StreamLoc321", + "office-id": "SPK" + }, + "streamflow-measurement": { + "gage-height": 6.0, + "flow": 275.0, + "quality": "F" + }, + "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/dao/dbMeasurement.xml b/cwms-data-api/src/test/resources/cwms/cda/data/dao/dbMeasurement.xml deleted file mode 100644 index 4f5e878d6..000000000 --- a/cwms-data-api/src/test/resources/cwms/cda/data/dao/dbMeasurement.xml +++ /dev/null @@ -1,40 +0,0 @@ - - 12345 - 2024-01-01T00:00:00Z - Walnut_Ck - SomeParty - Test comment - USGS - - 2.0 - 100.0 - good - - - Some remarks - 1 - UNSPECIFIED - 11.0 - 10.0 - UNKNOWN - 0.5 - 60.0 - 25.0 - 15.0 - - - 100.0 - 50.0 - 2.0 - 5.0 - 1.5 - 1.0 - 2.0 - 75.0 - 60.0 - 3.0 - 10.0 - 150.0 - 200.0 - -