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 b83729c0b..f67ae3572 100644 --- a/cwms-data-api/src/main/java/cwms/cda/ApiServlet.java +++ b/cwms-data-api/src/main/java/cwms/cda/ApiServlet.java @@ -72,6 +72,7 @@ import cwms.cda.api.ProjectController; import cwms.cda.api.PropertyController; import cwms.cda.api.RatingController; +import cwms.cda.api.RatingLatestController; import cwms.cda.api.RatingMetadataController; import cwms.cda.api.RatingSpecController; import cwms.cda.api.RatingTemplateController; @@ -572,6 +573,7 @@ protected void configureRoutes() { new RatingSpecController(metrics), requiredRoles,5, TimeUnit.MINUTES); cdaCrudCache("/ratings/metadata/{rating-id}", new RatingMetadataController(metrics), requiredRoles,5, TimeUnit.MINUTES); + get("/ratings/{rating-id}/latest", new RatingLatestController(metrics)); cdaCrudCache("/ratings/{rating-id}", new RatingController(metrics), requiredRoles,5, TimeUnit.MINUTES); cdaCrudCache("/catalog/{dataset}", diff --git a/cwms-data-api/src/main/java/cwms/cda/api/RatingLatestController.java b/cwms-data-api/src/main/java/cwms/cda/api/RatingLatestController.java new file mode 100644 index 000000000..c26c57dfd --- /dev/null +++ b/cwms-data-api/src/main/java/cwms/cda/api/RatingLatestController.java @@ -0,0 +1,129 @@ +/* + * + * 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 static cwms.cda.api.Controllers.GET_ONE; +import static cwms.cda.api.Controllers.OFFICE; +import static cwms.cda.api.Controllers.RATING_ID; +import static cwms.cda.api.Controllers.STATUS_200; +import static cwms.cda.data.dao.JooqDao.getDslContext; + +import com.codahale.metrics.MetricRegistry; +import com.codahale.metrics.Timer; +import com.fasterxml.jackson.core.JsonProcessingException; +import cwms.cda.data.dao.JsonRatingUtils; +import cwms.cda.data.dao.RatingDao; +import cwms.cda.data.dao.RatingSetDao; +import cwms.cda.formatters.ContentType; +import cwms.cda.formatters.Formats; +import io.javalin.http.Context; +import io.javalin.http.Handler; +import io.javalin.http.HttpCode; +import io.javalin.plugin.openapi.annotations.OpenApi; +import io.javalin.plugin.openapi.annotations.OpenApiContent; +import io.javalin.plugin.openapi.annotations.OpenApiParam; +import io.javalin.plugin.openapi.annotations.OpenApiResponse; +import org.jetbrains.annotations.NotNull; +import org.jooq.DSLContext; + + +public class RatingLatestController implements Handler { + private static final String TAG = "Ratings"; + private final MetricRegistry metrics; + + public RatingLatestController(MetricRegistry metrics) { + this.metrics = metrics; + } + + private Timer.Context markAndTime(String subject) { + return Controllers.markAndTime(metrics, getClass().getName(), subject); + } + + @NotNull + protected RatingDao getRatingDao(DSLContext dsl) { + return new RatingSetDao(dsl); + } + + @OpenApi( + pathParams = { + @OpenApiParam(name = RATING_ID, required = true, description = "The rating-id of the effective " + + "dates to be retrieve. "), + }, + queryParams = { + @OpenApiParam(name = OFFICE, required = true, description = + "Specifies the owning office of the ratingset to be included in the " + + "response."), + }, + responses = { + @OpenApiResponse(status = STATUS_200, content = { + @OpenApiContent(type = Formats.JSONV2), + @OpenApiContent(type = Formats.XMLV2)}) + }, + description = "Returns CWMS Rating Data", + tags = {TAG}) + @Override + public void handle(@NotNull Context ctx) throws Exception { + try (final Timer.Context ignored = markAndTime(GET_ONE)) { + String rating = ctx.pathParam(RATING_ID); + + ContentType contentType = new ContentType(ctx.contentType() != null ? ctx.contentType() : Formats.JSONV2); + + String officeId = ctx.queryParam(OFFICE); + + if (!contentType.toString().equals(Formats.JSONV2) && !contentType.toString().equals(Formats.XMLV2)) { + ctx.status(HttpCode.UNSUPPORTED_MEDIA_TYPE); + } + + String body = getLatestRatingSet(ctx, officeId, rating, contentType); + ctx.contentType(contentType.toString()); + if (body != null) { + ctx.result(body); + ctx.status(HttpCode.OK); + } else { + ctx.status(HttpCode.NOT_FOUND); + } + } + } + + private String getLatestRatingSet(Context ctx, String officeId, String rating, ContentType contentType) + throws JsonProcessingException { + String ratingSet = null; + try (final Timer.Context ignored = markAndTime("getLatestRatingSet")) { + DSLContext dsl = getDslContext(ctx); + + RatingDao ratingDao = getRatingDao(dsl); + + ratingSet = ratingDao.retrieveLatestXML(officeId, rating); + + if (contentType.toString().equals(Formats.JSONV2)) { + ratingSet = JsonRatingUtils.xmlToJson(ratingSet); + } + } + + return ratingSet; + } +} diff --git a/cwms-data-api/src/main/java/cwms/cda/data/dao/RatingDao.java b/cwms-data-api/src/main/java/cwms/cda/data/dao/RatingDao.java index 13240adf0..27396e782 100644 --- a/cwms-data-api/src/main/java/cwms/cda/data/dao/RatingDao.java +++ b/cwms-data-api/src/main/java/cwms/cda/data/dao/RatingDao.java @@ -26,7 +26,6 @@ import hec.data.RatingException; import hec.data.cwmsRating.RatingSet; - import java.io.IOException; import java.time.Instant; import java.util.regex.Matcher; @@ -34,13 +33,15 @@ public interface RatingDao { - static final Pattern officeMatcher = Pattern.compile(".*office-id=\"(.*?)\""); + Pattern officeMatcher = Pattern.compile(".*office-id=\"(.*?)\""); void create(String ratingSet, boolean storeTemplate) throws IOException, RatingException; RatingSet retrieve(RatingSet.DatabaseLoadMethod method, String officeId, String specificationId, Instant start, Instant end) throws IOException, RatingException; + String retrieveLatestXML(String officeId, String specificationId); + String retrieveRatings(String format, String names, String unit, String datum, String office, String start, String end, String timezone); @@ -52,7 +53,7 @@ String retrieveRatings(String format, String names, String unit, String datum, S static String extractOfficeFromXml(String xml) { Matcher officeMatch = officeMatcher.matcher(xml); - if(officeMatch.find()) { + if (officeMatch.find()) { return officeMatch.group(1); } else { throw new RuntimeException("Unable to determine office for data set"); diff --git a/cwms-data-api/src/main/java/cwms/cda/data/dao/RatingSetDao.java b/cwms-data-api/src/main/java/cwms/cda/data/dao/RatingSetDao.java index 37374c40f..e0b30c65a 100644 --- a/cwms-data-api/src/main/java/cwms/cda/data/dao/RatingSetDao.java +++ b/cwms-data-api/src/main/java/cwms/cda/data/dao/RatingSetDao.java @@ -29,21 +29,20 @@ import com.fasterxml.jackson.dataformat.xml.XmlMapper; import hec.data.RatingException; import hec.data.cwmsRating.RatingSet; +import java.io.IOException; +import java.sql.Connection; +import java.sql.Timestamp; +import java.time.Instant; +import java.util.List; import mil.army.usace.hec.cwms.rating.io.jdbc.ConnectionProvider; import mil.army.usace.hec.cwms.rating.io.jdbc.RatingJdbcFactory; import org.jooq.DSLContext; import org.jooq.exception.DataAccessException; import usace.cwms.db.jooq.codegen.packages.CWMS_RATING_PACKAGE; -import java.io.IOException; -import java.sql.Connection; -import java.sql.Timestamp; -import java.time.Instant; -import java.util.List; public class RatingSetDao extends JooqDao implements RatingDao { - public RatingSetDao(DSLContext dsl) { super(dsl); } @@ -57,8 +56,7 @@ public void create(String ratingSetXml, boolean storeTemplate) throws IOExceptio DSLContext context = getDslContext(c, office); String errs = CWMS_RATING_PACKAGE.call_STORE_RATINGS_XML__5(context.configuration(), ratingSetXml, "T", storeTemplate ? "T" : "F"); - if (errs != null && !errs.isEmpty()) - { + if (errs != null && !errs.isEmpty()) { throw new DataAccessException(errs); } }); @@ -83,11 +81,19 @@ private static String extractOfficeId(String ratingSet) throws JsonProcessingExc return office; } + @Override + public String retrieveLatestXML(String officeId, String specificationId) { + return connectionResult(dsl, c -> { + DSLContext context = getDslContext(c, officeId); + return CWMS_RATING_PACKAGE.call_RETRIEVE_EFF_RATINGS_XML_F(context.configuration(), specificationId, + Timestamp.from(Instant.now()), Timestamp.from(Instant.now()), null, officeId); + }); + } + @Override public RatingSet retrieve(RatingSet.DatabaseLoadMethod method, String officeId, String specificationId, Instant startZdt, Instant endZdt ) throws IOException, RatingException { - final RatingSet[] retval = new RatingSet[1]; try { final Long start; @@ -112,7 +118,8 @@ public RatingSet retrieve(RatingSet.DatabaseLoadMethod method, String officeId, connection(dsl, c -> retval[0] = RatingJdbcFactory.ratingSet(finalMethod, new RatingConnectionProvider(c), officeId, - specificationId, start, end, false)); + specificationId, start, end, false)); + } catch (DataAccessException ex) { Throwable cause = ex.getCause(); @@ -151,7 +158,7 @@ public void store(String ratingSetXml, boolean includeTemplate) throws IOExcepti public void delete(String officeId, String specificationId, Instant start, Instant end) { Timestamp startDate = new Timestamp(start.toEpochMilli()); Timestamp endDate = new Timestamp(end.toEpochMilli()); - dsl.connection(c-> + dsl.connection(c -> CWMS_RATING_PACKAGE.call_DELETE_RATINGS( getDslContext(c,officeId).configuration(), specificationId, startDate, endDate, "UTC", officeId @@ -170,15 +177,15 @@ public String retrieveRatings(String format, String names, String unit, String d } private static final class RatingConnectionProvider implements ConnectionProvider { - private final Connection c; + private final Connection conn; - private RatingConnectionProvider(Connection c) { - this.c = c; + private RatingConnectionProvider(Connection conn) { + this.conn = conn; } @Override public Connection getConnection() { - return c; + return conn; } @Override diff --git a/cwms-data-api/src/test/java/cwms/cda/api/RatingsControllerTestIT.java b/cwms-data-api/src/test/java/cwms/cda/api/RatingsControllerTestIT.java index 2a55eb8c0..7d87cebd7 100644 --- a/cwms-data-api/src/test/java/cwms/cda/api/RatingsControllerTestIT.java +++ b/cwms-data-api/src/test/java/cwms/cda/api/RatingsControllerTestIT.java @@ -7,34 +7,38 @@ package cwms.cda.api; +import cwms.cda.data.dao.JooqDao; import cwms.cda.formatters.Formats; import fixtures.TestAccounts; import hec.data.cwmsRating.io.RatingSetContainer; import hec.data.cwmsRating.io.RatingSpecContainer; import io.restassured.filter.log.LogDetail; +import io.restassured.response.ExtractableResponse; +import io.restassured.response.Response; 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; +import org.junit.jupiter.api.AfterAll; import org.junit.jupiter.api.BeforeAll; import org.junit.jupiter.api.Tag; +import org.junit.jupiter.api.Test; import org.junit.jupiter.params.ParameterizedTest; import org.junit.jupiter.params.provider.EnumSource; import javax.servlet.http.HttpServletResponse; -import java.util.Arrays; - -import static cwms.cda.api.Controllers.FORMAT; -import static cwms.cda.api.Controllers.OFFICE; -import static cwms.cda.api.Controllers.STORE_TEMPLATE; +import static cwms.cda.api.Controllers.*; import static io.restassured.RestAssured.given; import static org.hamcrest.Matchers.is; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNotNull; @Tag("integration") -public class RatingsControllerTestIT extends DataApiTestIT +class RatingsControllerTestIT extends DataApiTestIT { private static final String EXISTING_LOC = "RatingsControllerTestIT"; private static final String EXISTING_SPEC = EXISTING_LOC + ".Stage;Flow.COE.Production"; + private static final String TEMPLATE = "Stage;Flow.COE"; private static final String SPK = "SPK"; @BeforeAll @@ -45,7 +49,11 @@ static void beforeAll() throws Exception String ratingXml = readResourceFile("cwms/cda/api/Zanesville_Stage_Flow_COE_Production.xml"); ratingXml = ratingXml.replaceAll("Zanesville", EXISTING_LOC); + String ratingXml2 = ratingXml.replaceAll("2002-04-09T13:53:01Z", "2016-06-06T00:00:00Z"); + String ratingXml3 = ratingXml.replaceAll("2002-04-09T13:53:01Z", "2025-06-06T00:00:00Z"); RatingSetContainer container = RatingSetContainerXmlFactory.ratingSetContainerFromXml(ratingXml); + RatingSetContainer container2 = RatingSetContainerXmlFactory.ratingSetContainerFromXml(ratingXml2); + RatingSetContainer container3 = RatingSetContainerXmlFactory.ratingSetContainerFromXml(ratingXml3); RatingSpecContainer specContainer = container.ratingSpecContainer; specContainer.officeId = SPK; specContainer.specOfficeId = SPK; @@ -53,6 +61,8 @@ static void beforeAll() throws Exception String specXml = RatingSpecXmlFactory.toXml(specContainer, "", 0, true); String templateXml = RatingSpecXmlFactory.toXml(specContainer, "", 0); String setXml = RatingContainerXmlFactory.toXml(container, "", 0, true, false); + String setXml2 = RatingContainerXmlFactory.toXml(container2, "", 0, true, false); + String setXml3 = RatingContainerXmlFactory.toXml(container3, "", 0, true, false); TestAccounts.KeyUser user = TestAccounts.KeyUser.SPK_NORMAL; //Create Template @@ -103,6 +113,62 @@ static void beforeAll() throws Exception .assertThat() .log().ifValidationFails(LogDetail.ALL,true) .statusCode(is(HttpServletResponse.SC_OK)); + + //Create the second set + given() + .log().ifValidationFails(LogDetail.ALL,true) + .contentType(Formats.XMLV2) + .body(setXml2) + .header("Authorization", user.toHeaderValue()) + .queryParam(OFFICE, SPK) + .queryParam(STORE_TEMPLATE, false) + .when() + .redirects().follow(true) + .redirects().max(3) + .post("/ratings") + .then() + .log().ifValidationFails(LogDetail.ALL,true) + .assertThat() + .statusCode(is(HttpServletResponse.SC_OK)); + + // Create the third set + given() + .log().ifValidationFails(LogDetail.ALL,true) + .contentType(Formats.XMLV2) + .body(setXml3) + .header("Authorization", user.toHeaderValue()) + .queryParam(OFFICE, SPK) + .queryParam(STORE_TEMPLATE, false) + .when() + .redirects().follow(true) + .redirects().max(3) + .post("/ratings") + .then() + .log().ifValidationFails(LogDetail.ALL,true) + .assertThat() + .statusCode(is(HttpServletResponse.SC_OK)); + } + + @AfterAll + static void afterAll() + { + TestAccounts.KeyUser user = TestAccounts.KeyUser.SPK_NORMAL; + + // Delete Template + given() + .log().ifValidationFails(LogDetail.ALL,true) + .contentType(Formats.XMLV2) + .header("Authorization", user.toHeaderValue()) + .queryParam(OFFICE, SPK) + .queryParam(METHOD, JooqDao.DeleteMethod.DELETE_ALL) + .when() + .redirects().follow(true) + .redirects().max(3) + .delete("/ratings/template/" + TEMPLATE) + .then() + .log().ifValidationFails(LogDetail.ALL,true) + .assertThat() + .statusCode(is(HttpServletResponse.SC_NO_CONTENT)); } @ParameterizedTest @@ -111,7 +177,7 @@ void test_getAll_legacy(GetAllLegacyTest test) { given() .log().ifValidationFails(LogDetail.ALL,true) - .queryParam(FORMAT, test._queryParam) + .queryParam(FORMAT, test.queryParam) .when() .redirects().follow(true) .redirects().max(3) @@ -120,7 +186,7 @@ void test_getAll_legacy(GetAllLegacyTest test) .assertThat() .log().ifValidationFails(LogDetail.ALL,true) .statusCode(is(HttpServletResponse.SC_OK)) - .contentType(is(test._expectedContentType)); + .contentType(is(test.expectedContentType)); } @ParameterizedTest @@ -129,7 +195,7 @@ void test_getAll(GetAllTest test) { given() .log().ifValidationFails(LogDetail.ALL,true) - .accept(test._accept) + .accept(test.accept) .when() .redirects().follow(true) .redirects().max(3) @@ -138,7 +204,7 @@ void test_getAll(GetAllTest test) .assertThat() .log().ifValidationFails(LogDetail.ALL,true) .statusCode(is(HttpServletResponse.SC_OK)) - .contentType(is(test._expectedContentType)); + .contentType(is(test.expectedContentType)); } @ParameterizedTest @@ -147,7 +213,7 @@ void test_getOne(GetOneTest test) { given() .log().ifValidationFails(LogDetail.ALL,true) - .accept(test._accept) + .accept(test.accept) .queryParam(OFFICE, SPK) .when() .redirects().follow(true) @@ -157,7 +223,56 @@ void test_getOne(GetOneTest test) .assertThat() .log().ifValidationFails(LogDetail.ALL,true) .statusCode(is(HttpServletResponse.SC_OK)) - .contentType(is(test._expectedContentType)); + .contentType(is(test.expectedContentType)); + } + + @Test + void test_get_one_latest() { + // get latest json + ExtractableResponse response = given() + .log().ifValidationFails(LogDetail.ALL,true) + .contentType(Formats.JSONV2) + .queryParam(OFFICE, SPK) + .when() + .redirects().follow(true) + .redirects().max(3) + .get("/ratings/" + EXISTING_SPEC + "/latest") + .then() + .assertThat() + .log().ifValidationFails(LogDetail.ALL,true) + .statusCode(is(HttpServletResponse.SC_OK)) + .contentType(is(Formats.JSONV2)) + .extract(); + + String effectiveDate = response.path("ratings.simple-rating[0].effective-date"); + if (effectiveDate == null) { + effectiveDate = response.path("simple-rating.effective-date"); + } + assertNotNull(effectiveDate); + assertEquals("2016-06-06T00:00:00Z", effectiveDate); + + // get latest xml + response = given() + .log().ifValidationFails(LogDetail.ALL,true) + .contentType(Formats.XMLV2) + .queryParam(OFFICE, SPK) + .when() + .redirects().follow(true) + .redirects().max(3) + .get("/ratings/" + EXISTING_SPEC + "/latest") + .then() + .log().ifValidationFails(LogDetail.ALL,true) + .assertThat() + .statusCode(is(HttpServletResponse.SC_OK)) + .contentType(is(Formats.XMLV2)) + .extract(); + + effectiveDate = response.path("ratings.simple-rating[0].effective-date"); + if (effectiveDate == null) { + effectiveDate = response.path("simple-rating.effective-date"); + } + assertNotNull(effectiveDate); + assertEquals("2016-06-06T00:00:00Z", effectiveDate); } enum GetOneTest @@ -169,13 +284,13 @@ enum GetOneTest JSONV2(Formats.JSONV2, Formats.JSONV2), ; - final String _accept; - final String _expectedContentType; + final String accept; + final String expectedContentType; GetOneTest(String accept, String expectedContentType) { - _accept = accept; - _expectedContentType = expectedContentType; + this.accept = accept; + this.expectedContentType = expectedContentType; } } @@ -185,13 +300,13 @@ enum GetAllLegacyTest XML(Formats.XML_LEGACY, Formats.XML), ; - final String _queryParam; - final String _expectedContentType; + final String queryParam; + final String expectedContentType; GetAllLegacyTest(String queryParam, String expectedContentType) { - _queryParam = queryParam; - _expectedContentType = expectedContentType; + this.queryParam = queryParam; + this.expectedContentType = expectedContentType; } } @@ -206,13 +321,13 @@ enum GetAllTest JSONV2(Formats.JSONV2, Formats.JSONV2), ; - final String _accept; - final String _expectedContentType; + final String accept; + final String expectedContentType; GetAllTest(String accept, String expectedContentType) { - _accept = accept; - _expectedContentType = expectedContentType; + this.accept = accept; + this.expectedContentType = expectedContentType; } } }