Skip to content
Open
Original file line number Diff line number Diff line change
Expand Up @@ -75,7 +75,7 @@
import java.nio.charset.StandardCharsets;
import java.time.Instant;
import java.time.ZoneId;
import java.time.ZonedDateTime;
import java.time.temporal.ChronoUnit;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
Expand Down Expand Up @@ -124,6 +124,12 @@ public void create(@NotNull Context ctx) {
LocationLevel level = deserializeLocationLevel(ctx);
level.validate();

if (!LocationLevel.truncateDate(level.getLevelDate()).equals(level.getLevelDate())) {
Map<String, String> errorDetails = new HashMap<>();
errorDetails.put("message", "Level effective date cannot have seconds");
throw new BadRequestResponse("", errorDetails);
}

DSLContext dsl = getDslContext(ctx);
LocationLevelsDao levelsDao = getLevelsDao(dsl);
levelsDao.storeLocationLevel(level);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -26,14 +26,6 @@

package cwms.cda.data.dto.locationlevel;

import cwms.cda.data.dto.catalog.LocationAlias;
import java.math.BigDecimal;
import java.time.Instant;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.function.Consumer;
import com.fasterxml.jackson.annotation.JsonCreator;
import com.fasterxml.jackson.annotation.JsonFormat;
import com.fasterxml.jackson.annotation.JsonIgnore;
Expand All @@ -47,13 +39,23 @@
import com.fasterxml.jackson.databind.annotation.JsonPOJOBuilder;
import cwms.cda.data.dto.CwmsDTO;
import cwms.cda.data.dto.CwmsDTOValidator;
import cwms.cda.data.dto.catalog.LocationAlias;
import cwms.cda.formatters.Formats;
import cwms.cda.formatters.UnsupportedFormatException;
import cwms.cda.formatters.annotations.FormattableWith;
import cwms.cda.formatters.json.JsonV1;
import cwms.cda.formatters.json.JsonV2;
import cwms.cda.formatters.xml.XMLv2;
import io.swagger.v3.oas.annotations.media.Schema;
import java.math.BigDecimal;
import java.time.Instant;
import java.time.ZonedDateTime;
import java.time.temporal.ChronoUnit;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.function.Consumer;

@JsonRootName("LocationLevel")
@JsonDeserialize(builder = LocationLevel.Builder.class)
Expand Down Expand Up @@ -95,7 +97,8 @@ public abstract class LocationLevel extends CwmsDTO {
@Schema(description = "Units the provided levels are in")

private final String levelUnitsId;
@Schema(description = "The date/time at which this location level configuration takes effect.")
@Schema(description = "The date/time at which this location level configuration takes effect. "
+ "Must be limited to minute accuracy.")
@JsonFormat(shape = JsonFormat.Shape.STRING)

private final Instant levelDate;
Expand Down Expand Up @@ -501,4 +504,12 @@ protected void validateInternal(CwmsDTOValidator validator) {
validator.required(getLocationLevelId(), "location-level-id");
validator.required(getLevelDate(), "level-date");
}

public static ZonedDateTime truncateDate(ZonedDateTime date) {
return date.truncatedTo(ChronoUnit.MINUTES);
}

public static Instant truncateDate(Instant date) {
return date.truncatedTo(ChronoUnit.MINUTES);
}
}
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
{
"constant-value": 10,
"level-units-id": "ft",
"level-date": "2008-12-03T10:15:30+01:00[UTC]",
"level-date": "2008-12-03T10:15:00+01:00[UTC]",
"level-comment": "Lowest Point",
"interpolate-string": "F",
"location-level-id": "Sacramento.Elev.Inst.0.Bottom of Inlet",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,9 +11,9 @@
}
],
"level-units-id": "ft",
"level-date": "2008-12-03T10:15:30+01:00[UTC]",
"level-date": "2008-12-03T10:15:00+01:00[UTC]",
"level-comment": "Lowest Point",
"interval-origin": "2008-12-03T10:15:30+01:00[UTC]",
"interval-origin": "2008-12-03T10:15:00+01:00[UTC]",
"interval-months": 1,
"interpolate-string": "F",
"location-level-id": "Sacramento.Elev.Inst.0.Bottom of Inlet",
Expand Down
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
{
"seasonal-time-series-id": "Sacramento.Stage.Inst.5Minutes.0.Bottom of Inlet",
"level-units-id": "ft",
"level-date": "2008-12-03T10:15:30+01:00[UTC]",
"level-date": "2008-12-03T10:15:00+01:00[UTC]",
"level-comment": "Bottom of Inlet, lowest level",
"interpolate-string": "F",
"location-level-id": "Sacramento.Elev.Inst.0.Bottom of Inlet",
Expand Down
142 changes: 114 additions & 28 deletions cwms-data-api/src/test/java/cwms/cda/api/LevelsControllerTestIT.java
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,33 @@

package cwms.cda.api;

import static cwms.cda.api.Controllers.BEGIN;
import static cwms.cda.api.Controllers.EFFECTIVE_DATE;
import static cwms.cda.api.Controllers.EFFECTIVE_DATE_EXACT;
import static cwms.cda.api.Controllers.END;
import static cwms.cda.api.Controllers.FORMAT;
import static cwms.cda.api.Controllers.INCLUDE_ALIASES;
import static cwms.cda.api.Controllers.INTERVAL;
import static cwms.cda.api.Controllers.LEVEL_ID_MASK;
import static cwms.cda.api.Controllers.PAGE;
import static cwms.cda.api.Controllers.PAGE_SIZE;
import static cwms.cda.api.Controllers.START;
import static cwms.cda.api.Controllers.UNIT;
import static cwms.cda.security.ApiKeyIdentityProvider.AUTH_HEADER;
import static helpers.FloatCloseTo.floatCloseTo;
import static io.restassured.RestAssured.given;
import static org.hamcrest.MatcherAssert.assertThat;
import static org.hamcrest.Matchers.closeTo;
import static org.hamcrest.Matchers.containsString;
import static org.hamcrest.Matchers.equalTo;
import static org.hamcrest.Matchers.is;
import static org.hamcrest.Matchers.isOneOf;
import static org.hamcrest.Matchers.notNullValue;
import static org.hamcrest.Matchers.nullValue;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertTrue;
import static org.junit.jupiter.api.Assertions.fail;

import cwms.cda.ApiServlet;
import cwms.cda.data.dao.LocationCategoryDao;
import cwms.cda.data.dao.LocationGroupDao;
Expand All @@ -33,9 +60,9 @@
import cwms.cda.data.dto.AssignedLocation;
import cwms.cda.data.dto.LocationCategory;
import cwms.cda.data.dto.LocationGroup;
import cwms.cda.data.dto.TimeSeries;
import cwms.cda.data.dto.locationlevel.ConstantLocationLevel;
import cwms.cda.data.dto.locationlevel.LocationLevel;
import cwms.cda.data.dto.TimeSeries;
import cwms.cda.data.dto.locationlevel.SeasonalLocationLevel;
import cwms.cda.data.dto.locationlevel.SeasonalValueBean;
import cwms.cda.data.dto.locationlevel.TimeSeriesLocationLevel;
Expand All @@ -44,11 +71,24 @@
import fixtures.CwmsDataApiSetupCallback;
import fixtures.MinimumSchema;
import fixtures.TestAccounts;
import hec.data.RatingException;
import hec.data.cwmsRating.io.RatingSetContainer;
import hec.data.cwmsRating.io.RatingSpecContainer;
import io.restassured.filter.log.LogDetail;

import io.restassured.path.json.JsonPath;
import io.restassured.response.ExtractableResponse;
import io.restassured.response.Response;
import java.io.IOException;
import java.sql.SQLException;
import java.time.Instant;
import java.time.ZoneId;
import java.time.ZonedDateTime;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import java.util.NavigableMap;
import java.util.TreeMap;
import javax.servlet.http.HttpServletResponse;
import mil.army.usace.hec.cwms.rating.io.xml.RatingContainerXmlFactory;
import mil.army.usace.hec.cwms.rating.io.xml.RatingSetContainerXmlFactory;
import mil.army.usace.hec.cwms.rating.io.xml.RatingSpecXmlFactory;
Expand All @@ -61,32 +101,6 @@
import org.junit.jupiter.params.provider.EnumSource;
import org.junit.jupiter.params.provider.ValueSource;

import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.sql.SQLException;
import java.time.Instant;
import java.time.ZoneId;
import java.time.ZonedDateTime;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import java.util.NavigableMap;
import java.util.TreeMap;

import hec.data.RatingException;
import hec.data.cwmsRating.io.RatingSetContainer;
import hec.data.cwmsRating.io.RatingSpecContainer;

import static cwms.cda.api.Controllers.*;
import static cwms.cda.security.ApiKeyIdentityProvider.AUTH_HEADER;
import static helpers.FloatCloseTo.floatCloseTo;
import static io.restassured.RestAssured.given;
import static org.hamcrest.MatcherAssert.assertThat;
import static org.hamcrest.Matchers.*;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertTrue;
import static org.junit.jupiter.api.Assertions.fail;

@Tag("integration")
public class LevelsControllerTestIT extends DataApiTestIT {

Expand Down Expand Up @@ -2081,6 +2095,42 @@ void testStoreConstantLevel() throws Exception {
.body("constant-value", equalTo(8675.309f));
}

@Test
void testStoreTimeRestrictedLevel() throws Exception {
String locName = "restrictedLoc123";
createLocation(locName, true, OFFICE);
String levelId = String.format("%s.Elev.Ave.1Day.Regulating", locName);
ZonedDateTime time = ZonedDateTime.ofInstant(Instant.parse("2024-01-01T00:00:12Z"), ZoneId.of("UTC"));
ConstantLocationLevel level = new ConstantLocationLevel.Builder(levelId, time.toInstant())
.withOfficeId(OFFICE)
.withLevelUnitsId("ft")
.withConstantValue(8675.309)
.withExpirationDate(time.plusYears(50).toInstant())
.build();

String levelJson = Formats.format(new ContentType(Formats.JSONV2), level);

given()
.log().ifValidationFails(LogDetail.ALL, true)
.queryParam(Controllers.OFFICE, OFFICE)
.header("Authorization", TestAccounts.KeyUser.SPK_NORMAL.toHeaderValue())
.body(levelJson)
.contentType(Formats.JSONV2)
.when()
.redirects()
.follow(true)
.redirects()
.max(3)
.post("/levels/")
.then()
.log().ifValidationFails(LogDetail.ALL, true)
.assertThat()
.statusCode(is(HttpServletResponse.SC_BAD_REQUEST))
.body("details.message", equalTo("Level effective date cannot have seconds"))
.body("source", equalTo("User Input"))
.body(MESSAGE, equalTo("Bad Request"));
}

enum GetAllTestLegacy {
JSON(Formats.JSON_LEGACY, Formats.JSON),
XML(Formats.XML_LEGACY, Formats.XML),
Expand Down Expand Up @@ -2414,6 +2464,42 @@ void test_get_aliases() throws Exception {
;
}

@Test
void test_create_subminute_level() throws Exception {
String locName = "subminuteloc";
createLocation(locName, true, OFFICE);
String levelId = String.format("%s.Elev.Ave.1Day.Top of Inlet", locName);
ZonedDateTime time = ZonedDateTime.ofInstant(Instant.parse("2024-01-01T00:00:25Z"), ZoneId.of("UTC"));
ConstantLocationLevel level = new ConstantLocationLevel.Builder(levelId, time.toInstant())
.withOfficeId(OFFICE)
.withLevelUnitsId("ft")
.withConstantValue(8675.309)
.withExpirationDate(time.toInstant())
.build();

String levelJson = Formats.format(new ContentType(Formats.JSONV2), level);

given()
.log().ifValidationFails(LogDetail.ALL, true)
.queryParam(Controllers.OFFICE, OFFICE)
.header("Authorization", TestAccounts.KeyUser.SPK_NORMAL.toHeaderValue())
.body(levelJson)
.contentType(Formats.JSONV2)
.when()
.redirects()
.follow(true)
.redirects()
.max(3)
.post("/levels/")
.then()
.log().ifValidationFails(LogDetail.ALL, true)
.assertThat()
.statusCode(is(HttpServletResponse.SC_BAD_REQUEST))
.body("details.message", is("Level effective date cannot have seconds"))
.body("message", is("Bad Request"))
.body("source", is("User Input"));
}

enum GetAllTestNewAliases {
DEFAULT(Formats.DEFAULT, Formats.JSONV2),
JSON(Formats.JSON, Formats.JSONV2),
Expand Down
16 changes: 8 additions & 8 deletions cwms-data-api/src/test/java/cwms/cda/api/LockControllerIT.java
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@
import static org.hamcrest.Matchers.not;
import static org.hamcrest.Matchers.nullValue;

import com.google.common.flogger.FluentLogger;
import cwms.cda.api.enums.Nation;
import cwms.cda.api.errors.NotFoundException;
import cwms.cda.data.dao.DeleteRule;
Expand All @@ -45,10 +46,10 @@
import cwms.cda.data.dao.location.kind.LockDao;
import cwms.cda.data.dto.CwmsId;
import cwms.cda.data.dto.Location;
import cwms.cda.data.dto.locationlevel.ConstantLocationLevel;
import cwms.cda.data.dto.locationlevel.LocationLevel;
import cwms.cda.data.dto.location.kind.Lock;
import cwms.cda.data.dto.location.kind.LockLocationLevelRef;
import cwms.cda.data.dto.locationlevel.ConstantLocationLevel;
import cwms.cda.data.dto.locationlevel.LocationLevel;
import cwms.cda.formatters.ContentType;
import cwms.cda.formatters.Formats;
import fixtures.CwmsDataApiSetupCallback;
Expand All @@ -65,7 +66,6 @@
import java.time.ZonedDateTime;
import java.util.ArrayList;
import java.util.List;
import com.google.common.flogger.FluentLogger;
import javax.servlet.http.HttpServletResponse;
import mil.army.usace.hec.test.database.CwmsDatabaseContainer;
import org.apache.commons.io.IOUtils;
Expand Down Expand Up @@ -1049,36 +1049,36 @@ private static PROJECT_OBJ_T buildProject(Location projectLocation) {

private List<LocationLevel> createLocationLevelList(Lock lock) {
List<LocationLevel> retVal = new ArrayList<>();
var lowLowerLevel = new ConstantLocationLevel.Builder(lock.getLowWaterLowerPoolLocationLevel().getLevelId(), ZonedDateTime.now().toInstant())
var lowLowerLevel = new ConstantLocationLevel.Builder(lock.getLowWaterLowerPoolLocationLevel().getLevelId(), LocationLevel.truncateDate(ZonedDateTime.now()).toInstant())
.withLevelUnitsId(lock.getElevationUnits())
.withOfficeId(lock.getLowWaterLowerPoolLocationLevel().getOfficeId())
.withSpecifiedLevelId(lock.getLowWaterLowerPoolLocationLevel().getSpecifiedLevelId())
.withConstantValue(lock.getLowWaterLowerPoolLocationLevel().getLevelValue())
.build();
retVal.add(lowLowerLevel);
var lowUpperLevel = new ConstantLocationLevel.Builder(lock.getLowWaterUpperPoolLocationLevel().getLevelId(), ZonedDateTime.now().toInstant())
var lowUpperLevel = new ConstantLocationLevel.Builder(lock.getLowWaterUpperPoolLocationLevel().getLevelId(), LocationLevel.truncateDate(ZonedDateTime.now()).toInstant())
.withLevelUnitsId(lock.getElevationUnits())
.withOfficeId(lock.getLowWaterUpperPoolLocationLevel().getOfficeId())
.withSpecifiedLevelId(lock.getLowWaterUpperPoolLocationLevel().getSpecifiedLevelId())
.withConstantValue(lock.getLowWaterUpperPoolLocationLevel().getLevelValue())
.build();
retVal.add(lowUpperLevel);
var highLowerLevel = new ConstantLocationLevel.Builder(lock.getHighWaterLowerPoolLocationLevel().getLevelId(), ZonedDateTime.now().toInstant())
var highLowerLevel = new ConstantLocationLevel.Builder(lock.getHighWaterLowerPoolLocationLevel().getLevelId(), LocationLevel.truncateDate(ZonedDateTime.now()).toInstant())
.withLevelUnitsId(lock.getElevationUnits())
.withOfficeId(lock.getHighWaterLowerPoolLocationLevel().getOfficeId())
.withSpecifiedLevelId(lock.getHighWaterLowerPoolLocationLevel().getSpecifiedLevelId())
.withConstantValue(lock.getHighWaterLowerPoolLocationLevel().getLevelValue())
.build();
retVal.add(highLowerLevel);
var highUpperLevel = new ConstantLocationLevel.Builder(lock.getHighWaterUpperPoolLocationLevel().getLevelId(), ZonedDateTime.now().toInstant())
var highUpperLevel = new ConstantLocationLevel.Builder(lock.getHighWaterUpperPoolLocationLevel().getLevelId(), LocationLevel.truncateDate(ZonedDateTime.now()).toInstant())
.withLevelUnitsId(lock.getElevationUnits())

.withOfficeId(lock.getHighWaterUpperPoolLocationLevel().getOfficeId())
.withSpecifiedLevelId(lock.getHighWaterUpperPoolLocationLevel().getSpecifiedLevelId())
.withConstantValue(lock.getHighWaterUpperPoolLocationLevel().getLevelValue())
.build();
retVal.add(highUpperLevel);
var warningBuffer = new ConstantLocationLevel.Builder(String.format("%s.Elev-Closure.Inst.0.Warning Buffer", lock.getLocation().getName()), ZonedDateTime.now().toInstant())
var warningBuffer = new ConstantLocationLevel.Builder(String.format("%s.Elev-Closure.Inst.0.Warning Buffer", lock.getLocation().getName()), LocationLevel.truncateDate(ZonedDateTime.now()).toInstant())
.withLevelUnitsId(lock.getElevationUnits())
.withOfficeId(lock.getLocation().getOfficeId())
.withSpecifiedLevelId("Warning Buffer")
Expand Down
Loading
Loading