diff --git a/cwms-data-api/src/main/java/cwms/cda/api/LevelsController.java b/cwms-data-api/src/main/java/cwms/cda/api/LevelsController.java index 6a65f4977..34cf9943c 100644 --- a/cwms-data-api/src/main/java/cwms/cda/api/LevelsController.java +++ b/cwms-data-api/src/main/java/cwms/cda/api/LevelsController.java @@ -206,10 +206,8 @@ public void delete(@NotNull Context ctx, @NotNull String levelId) { + " the default English units for their parameters." + "\n* `SI` " + "Specifies the SI unit system. Location level values will be in " - + "the default SI units for their parameters." - + "\n* `Other` " - + "Any unit returned in the response to the units URI request that is " - + "appropriate for the requested parameters. "), + + "the default SI units for their parameters. " + + "\n\nThe default unit system is SI."), @OpenApiParam(name = DATUM, description = "Specifies the elevation datum of" + " the response. This field affects only elevation location levels. " + "Valid values for this field are:" @@ -325,14 +323,24 @@ public void getAll(@NotNull Context ctx) { @OpenApiParam(name = OFFICE, required = true, description = "Specifies the " + "office of the Location Level to be returned"), @OpenApiParam(name = EFFECTIVE_DATE, required = true, description = "Specifies " - + "the effective date of Location Level to be returned"), + + "the effective date of Location Level to be returned. " + + "Expected formats are `YYYY-MM-DDTHH:MM` or `YYYY-MM-DDTHH:MM:SS`"), @OpenApiParam(name = TIMEZONE, description = "Specifies the time zone of " + "the values of the effective date field (unless otherwise " + "specified), as well as the time zone of any times in the response." + " If this field is not specified, the default time zone of UTC " + "shall be used."), - @OpenApiParam(name = UNIT, description = "Desired unit for " - + "the values retrieved.") + @OpenApiParam(name = UNIT, description = "Specifies the unit or unit system" + + " of the response. Valid values for the unit field are:" + + "\n* `EN` " + + "Specifies English unit system. Location level values will be in" + + " the default English units for their parameters." + + "\n* `SI` " + + "Specifies the SI unit system. Location level values will be in " + + "the default SI units for their parameters." + + "\n* `Other` " + + "Any unit returned in the response to the units URI request that is " + + "appropriate for the requested parameters. "), }, responses = { @OpenApiResponse(status = STATUS_200,content = { diff --git a/cwms-data-api/src/main/java/cwms/cda/data/dao/LocationLevelsDaoImpl.java b/cwms-data-api/src/main/java/cwms/cda/data/dao/LocationLevelsDaoImpl.java index 470e89000..9195cc741 100644 --- a/cwms-data-api/src/main/java/cwms/cda/data/dao/LocationLevelsDaoImpl.java +++ b/cwms-data-api/src/main/java/cwms/cda/data/dao/LocationLevelsDaoImpl.java @@ -62,7 +62,6 @@ import java.util.Objects; import java.util.Optional; import java.util.Set; -import java.util.TimeZone; import java.util.logging.Level; import java.util.logging.Logger; import java.util.regex.Pattern; @@ -96,6 +95,9 @@ public class LocationLevelsDaoImpl extends JooqDao implements Loc private static final String ATTRIBUTE_ID_PARSING_REGEXP = "(.*)\\.(.*)\\.(.*)"; public static final Pattern attributeIdParsingPattern = Pattern.compile(ATTRIBUTE_ID_PARSING_REGEXP); + private static final String LOCATION_LEVEL_ID_PARSING_REGEXP = "\\."; + public static final Pattern locationLevelIdParsingPattern = + Pattern.compile(LOCATION_LEVEL_ID_PARSING_REGEXP); public LocationLevelsDaoImpl(DSLContext dsl) { super(dsl); @@ -184,8 +186,10 @@ private static class LevelLookup { private final JDomLocationLevelRef locationLevelRef; private final Date effectiveDate; - public LevelLookup(String officeId, String locLevelId, String attributeId, String attributeValue, String attributeUnits, Date effectiveDate) { - this(new JDomLocationLevelRef(officeId, locLevelId, attributeId, attributeValue, attributeUnits), effectiveDate); + public LevelLookup(String officeId, String locLevelId, String attributeId, String attributeValue, + String attributeUnits, Date effectiveDate) { + this(new JDomLocationLevelRef(officeId, locLevelId, attributeId, attributeValue, attributeUnits), + effectiveDate); } public LevelLookup(JDomLocationLevelRef locationLevelRef, Date effectiveDate) { @@ -203,7 +207,8 @@ public boolean equals(Object o) { } LevelLookup that = (LevelLookup) o; - return Objects.equals(locationLevelRef, that.locationLevelRef) && Objects.equals(effectiveDate, that.effectiveDate); + return Objects.equals(locationLevelRef, that.locationLevelRef) + && Objects.equals(effectiveDate, that.effectiveDate); } @Override @@ -242,13 +247,12 @@ private static SEASONAL_VALUE_TAB_T getSeasonalValues(LocationLevel locationLeve List seasonalValues = locationLevel.getSeasonalValues(); SEASONAL_VALUE_TAB_T pSeasonalValues = null; - if(seasonalValues != null && !seasonalValues.isEmpty()) { + if (seasonalValues != null && !seasonalValues.isEmpty()) { pSeasonalValues = new SEASONAL_VALUE_TAB_T(); - for(SeasonalValueBean seasonalValue : seasonalValues) - { + for(SeasonalValueBean seasonalValue : seasonalValues) { SEASONAL_VALUE_T seasonalValueT = new SEASONAL_VALUE_T(); seasonalValueT.setOFFSET_MINUTES(toBigDecimal(seasonalValue.getOffsetMinutes())); - if(seasonalValue.getOffsetMonths() != null) { + if (seasonalValue.getOffsetMonths() != null) { seasonalValueT.setOFFSET_MONTHS(seasonalValue.getOffsetMonths().byteValue()); } seasonalValueT.setVALUE(toBigDecimal(seasonalValue.getValue())); @@ -327,16 +331,25 @@ public void renameLocationLevel(String oldLocationLevelName, String newLocationL public LocationLevel retrieveLocationLevel(String locationLevelName, String pUnits, ZonedDateTime effectiveDate, String officeId) { Timestamp date = Timestamp.from(effectiveDate.toInstant()); + String[] levelIdParts = locationLevelIdParsingPattern.split(locationLevelName); + if (levelIdParts.length <= 2) { + throw new IllegalArgumentException("Location level name is in an invalid format, must be separated by '.'"); + } + String parameter = levelIdParts[1]; return connectionResult(dsl, c -> { String units = pUnits; Configuration configuration = getDslContext(c, officeId).configuration(); + if (units != null && (units.equalsIgnoreCase("SI") + || units.equalsIgnoreCase("EN"))) { + units = CWMS_UTIL_PACKAGE.call_GET_DEFAULT_UNITS(configuration, + parameter, units); + } RETRIEVE_LOCATION_LEVEL3 level = CWMS_LEVEL_PACKAGE.call_RETRIEVE_LOCATION_LEVEL3( configuration, locationLevelName, units, date, "UTC", null, null, units, "F", officeId); List seasonalValues = buildSeasonalValues(level); if (units == null) { - String parameter = locationLevelName.split("\\.")[1]; logger.info("Getting default units for " + parameter); String defaultUnits = CWMS_UTIL_PACKAGE.call_GET_DEFAULT_UNITS( configuration, parameter, UnitSystem.SI.getValue()); @@ -464,12 +477,12 @@ private void addSeasonalValue(Record r, // offset.setDaysHoursMinutesString(dayToSecond.toString()); // } // seasonalValuesImpl.setOffset(offset); - // TODO: LocationLevel is missing seasonal origin and offset. + // TODO: LocationLevel is missing seasonal origin and offset. String calOffset = r.get(view.CALENDAR_OFFSET); String timeOffset = r.get(view.TIME_OFFSET); JDomSeasonalIntervalImpl newSeasonalOffset = buildSeasonalOffset(calOffset, timeOffset); - SeasonalValueBean seasonalValue = buildSeasonalValueBean(seasonalLevel, newSeasonalOffset) ; + SeasonalValueBean seasonalValue = buildSeasonalValueBean(seasonalLevel, newSeasonalOffset); builder.withSeasonalValue(seasonalValue); } } diff --git a/cwms-data-api/src/test/java/cwms/cda/api/LevelsControllerTestIT.java b/cwms-data-api/src/test/java/cwms/cda/api/LevelsControllerTestIT.java index d275556e4..7ad386057 100644 --- a/cwms-data-api/src/test/java/cwms/cda/api/LevelsControllerTestIT.java +++ b/cwms-data-api/src/test/java/cwms/cda/api/LevelsControllerTestIT.java @@ -464,6 +464,99 @@ void test_get_all_location_level() throws Exception { assertThat(response.path("levels[1].constant-value"), floatCloseTo(2.0, 0.01)); } + @Test + void test_get_one_units() throws Exception { + createLocation("level_as_single_value", true, OFFICE); + String levelId = "level_as_single_value.Stor.Ave.1Day.Regulating"; + ZonedDateTime time = ZonedDateTime.of(2023, 6, 1, 0, 0, 0, 0, ZoneId.of("America/Los_Angeles")); + LocationLevel level = new LocationLevel.Builder(levelId, time) + .withOfficeId(OFFICE) + .withConstantValue(1.0) + .withLevelUnitsId("ac-ft") + .build(); + CwmsDataApiSetupCallback.getDatabaseLink().connection(c -> { + DSLContext dsl = dslContext(c, OFFICE); + LocationLevelsDaoImpl dao = new LocationLevelsDaoImpl(dsl); + dao.storeLocationLevel(level); + }); + + //Read level with unit + given() + .log().ifValidationFails(LogDetail.ALL,true) + .accept(Formats.JSONV2) + .contentType(Formats.JSONV2) + .queryParam(Controllers.OFFICE, OFFICE) + .queryParam(UNIT, "SI") + .queryParam(EFFECTIVE_DATE, time.toInstant().toString()) + .when() + .redirects().follow(true) + .redirects().max(3) + .get("/levels/{level-id}", levelId) + .then() + .log().ifValidationFails(LogDetail.ALL,true) + .assertThat() + .statusCode(is(HttpServletResponse.SC_OK)) + .body("level-units-id", equalTo("m3")) + // I think we need to create a custom matcher. + // This really shouldn't use equals but due to a quirk in + // RestAssured it appears to be necessary. + .body("constant-value", equalTo(1233.4818f)); // 1 ac-ft to m3 + + given() + .log().ifValidationFails(LogDetail.ALL,true) + .accept(Formats.JSONV2) + .contentType(Formats.JSONV2) + .queryParam("office", OFFICE) + .queryParam(EFFECTIVE_DATE, time.toInstant().toString()) + .queryParam(UNIT, "ac-ft") + .when() + .redirects().follow(true) + .redirects().max(3) + .get("/levels/{level-id}", levelId) + .then() + .assertThat() + .log().ifValidationFails(LogDetail.ALL,true) + .statusCode(is(HttpServletResponse.SC_OK)) + .body("level-units-id",equalTo("ac-ft")) + .body("constant-value",equalTo(1.0F)); + + given() + .log().ifValidationFails(LogDetail.ALL,true) + .accept(Formats.JSONV2) + .contentType(Formats.JSONV2) + .queryParam("office", OFFICE) + .queryParam(EFFECTIVE_DATE, time.toInstant().toString()) + .queryParam(UNIT, "EN") + .when() + .redirects().follow(true) + .redirects().max(3) + .get("/levels/{level-id}", levelId) + .then() + .assertThat() + .log().ifValidationFails(LogDetail.ALL,true) + .statusCode(is(HttpServletResponse.SC_OK)) + .body("level-units-id",equalTo("ac-ft")) + .body("constant-value",equalTo(1.0F)); + + given() + .log().ifValidationFails(LogDetail.ALL,true) + .accept(Formats.JSONV2) + .contentType(Formats.JSONV2) + .queryParam("office", OFFICE) + .queryParam(EFFECTIVE_DATE, time.toInstant().toString()) + .queryParam(UNIT, "ft3") + .when() + .redirects().follow(true) + .redirects().max(3) + .get("/levels/{level-id}", levelId) + .then() + .log().ifValidationFails(LogDetail.ALL,true) + .assertThat() + .statusCode(is(HttpServletResponse.SC_OK)) + .body("level-units-id",equalTo("ft3")) + .body("constant-value",equalTo(43560.0F)); + } + @Test void test_get_all_earliest_time() throws Exception { String locId = "level_get_all_loc1"; @@ -583,6 +676,25 @@ void test_get_all_earliest_time() throws Exception { assertThat(actual1, closeTo(2466.9636f, 1.0)); } + @Test + void testRetrievalInvalidLevelName() + { + given() + .log().ifValidationFails(LogDetail.ALL, true) + .accept(Formats.JSONV2) + .contentType(Formats.JSONV2) + .queryParam(Controllers.OFFICE, OFFICE) + .queryParam(EFFECTIVE_DATE, "2023-06-01T00:00:00Z") + .when() + .redirects().follow(true) + .redirects().max(3) + .get("/levels/invalid.level_name") + .then() + .assertThat() + .log().ifValidationFails(LogDetail.ALL, true) + .statusCode(is(HttpServletResponse.SC_BAD_REQUEST)); + } + @ParameterizedTest @EnumSource(GetAllTestNewAliases.class) void test_get_all_aliases_new(GetAllTestNewAliases test) diff --git a/cwms-data-api/src/test/java/fixtures/CwmsDataApiSetupCallback.java b/cwms-data-api/src/test/java/fixtures/CwmsDataApiSetupCallback.java index 1fa52cd27..861e8f86e 100644 --- a/cwms-data-api/src/test/java/fixtures/CwmsDataApiSetupCallback.java +++ b/cwms-data-api/src/test/java/fixtures/CwmsDataApiSetupCallback.java @@ -166,7 +166,7 @@ private void loadDefaultData(CwmsDatabaseContainer cwmsDb) throws SQLException { private ArrayList getDefaultList() { ArrayList list = new ArrayList<>(); InputStream listStream = getClass().getResourceAsStream("/cwms/cda/data/sql/defaultload.txt"); - try( BufferedReader br = new BufferedReader( new InputStreamReader(listStream) )) { + try( BufferedReader br = new BufferedReader(new InputStreamReader(listStream))) { String line = null; while( (line = br.readLine() ) != null){ if( line.trim().startsWith("#") ) continue;