diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 57fd198d98..a9778fbe85 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -23,7 +23,7 @@ jobs: - env: release image: "ghcr.io/hydrologicengineeringcenter/cwms-database/cwms/database-ready-ora-23.5:26.02.17" - env: next-release - image: "ghcr.io/hydrologicengineeringcenter/cwms-database/cwms/database-ready-ora-23.5:26.07.16-RC01" + image: "ghcr.io/hydrologicengineeringcenter/cwms-database/cwms/database-ready-ora-23.5:26.07.16-RC02" - env: previous image: "ghcr.io/hydrologicengineeringcenter/cwms-database/cwms/database-ready-ora-23.5:25.07.01" name: build and test (jdk ${{matrix.jdk}}, schema ${{matrix.schema.env}}) diff --git a/cwms-data-api/src/main/java/cwms/cda/api/LocationController.java b/cwms-data-api/src/main/java/cwms/cda/api/LocationController.java index 3acf92fc97..9c23e178fa 100644 --- a/cwms-data-api/src/main/java/cwms/cda/api/LocationController.java +++ b/cwms-data-api/src/main/java/cwms/cda/api/LocationController.java @@ -439,7 +439,8 @@ public void delete(@NotNull Context ctx, @NotNull String locationId) { ctx.status(HttpServletResponse.SC_OK).json(re); } catch (DataAccessException ex) { SQLException cause = ex.getCause(SQLException.class); - if (cause != null && cause.getErrorCode() == 20031) { + if (cause != null && + (cause.getErrorCode() == 20031 || cause.getErrorCode() == 20056)) { throw new DeleteConflictException("Unable to delete requested location: " + locationId + " for office: " + office, cause); } diff --git a/cwms-data-api/src/main/java/cwms/cda/api/rating/RatingController.java b/cwms-data-api/src/main/java/cwms/cda/api/rating/RatingController.java index 9aa2593184..9225df4c8a 100644 --- a/cwms-data-api/src/main/java/cwms/cda/api/rating/RatingController.java +++ b/cwms-data-api/src/main/java/cwms/cda/api/rating/RatingController.java @@ -67,6 +67,7 @@ import cwms.cda.data.dto.CwmsDTOBase; import cwms.cda.data.dto.StatusResponse; import cwms.cda.data.dto.VerticalDatumInfo; +import cwms.cda.data.dto.rating.Ratings; import cwms.cda.formatters.ContentType; import cwms.cda.formatters.Formats; import cwms.cda.formatters.annotations.FormattableWith; @@ -184,7 +185,7 @@ private String deserializeRatingSet(Context ctx, boolean storeTemplate) throws I String formatHeader = ctx.req.getContentType(); //Using placeholder CwmsDTOBase.class since we do not have a RatingSet DTO //The contentType will match against the standard listing of Formats constants - ContentType contentType = Formats.parseHeader(formatHeader, CwmsDTOBase.class); + ContentType contentType = Formats.parseHeader(formatHeader, Ratings.class); String body = ctx.body(); return deserializeRatingSet(body, contentType.getType(), storeTemplate); } diff --git a/cwms-data-api/src/main/java/cwms/cda/data/dto/Location.java b/cwms-data-api/src/main/java/cwms/cda/data/dto/Location.java index 4c9bb0accb..c2a6c289d5 100644 --- a/cwms-data-api/src/main/java/cwms/cda/data/dto/Location.java +++ b/cwms-data-api/src/main/java/cwms/cda/data/dto/Location.java @@ -12,6 +12,7 @@ import cwms.cda.api.enums.Nation; import cwms.cda.data.dto.catalog.LocationAlias; import cwms.cda.formatters.Formats; +import cwms.cda.formatters.OutputFormatter; import cwms.cda.formatters.annotations.FormattableWith; import cwms.cda.formatters.json.JsonV1; import cwms.cda.formatters.json.JsonV2; @@ -33,6 +34,7 @@ @FormattableWith(contentType = Formats.XMLV2, formatter = XMLv2.class, aliases = {Formats.XML}) @FormattableWith(contentType = Formats.JSONV2, formatter = JsonV2.class, aliases = {Formats.DEFAULT, Formats.JSON}) @FormattableWith(contentType = Formats.JSONV1, formatter = JsonV1.class) +@FormattableWith(contentType = Formats.GEOJSON, formatter = OutputFormatter.DUMMY.class) public final class Location extends CwmsDTO { @JsonProperty(required = true) private final String name; diff --git a/cwms-data-api/src/main/java/cwms/cda/data/dto/Parameter.java b/cwms-data-api/src/main/java/cwms/cda/data/dto/Parameter.java index fbc454c4a4..1b695e438b 100644 --- a/cwms-data-api/src/main/java/cwms/cda/data/dto/Parameter.java +++ b/cwms-data-api/src/main/java/cwms/cda/data/dto/Parameter.java @@ -12,7 +12,9 @@ import com.fasterxml.jackson.databind.annotation.JsonNaming; import cwms.cda.formatters.Formats; import cwms.cda.formatters.annotations.FormattableWith; +import cwms.cda.formatters.json.JsonV1; import cwms.cda.formatters.json.JsonV2; +import cwms.cda.formatters.xml.XMLv1; import cwms.cda.formatters.xml.XMLv2; import java.util.Objects; @@ -20,7 +22,9 @@ @JsonRootName("parameter") @JsonNaming(PropertyNamingStrategies.KebabCaseStrategy.class) @FormattableWith(contentType = Formats.XMLV2, formatter = XMLv2.class, aliases = {Formats.XML}) +@FormattableWith(contentType = Formats.XMLV1, formatter = XMLv1.class) @FormattableWith(contentType = Formats.JSONV2, formatter = JsonV2.class, aliases = {Formats.DEFAULT, Formats.JSON}) +@FormattableWith(contentType = Formats.JSONV1, formatter = JsonV1.class) public final class Parameter extends CwmsDTO { private String name; diff --git a/cwms-data-api/src/main/java/cwms/cda/data/dto/TimeSeries.java b/cwms-data-api/src/main/java/cwms/cda/data/dto/TimeSeries.java index ef6856ce07..b26f1b59f9 100644 --- a/cwms-data-api/src/main/java/cwms/cda/data/dto/TimeSeries.java +++ b/cwms-data-api/src/main/java/cwms/cda/data/dto/TimeSeries.java @@ -14,7 +14,9 @@ import com.fasterxml.jackson.databind.annotation.JsonSerialize; import cwms.cda.api.enums.VersionType; import cwms.cda.formatters.Formats; +import cwms.cda.formatters.OutputFormatter; import cwms.cda.formatters.annotations.FormattableWith; +import cwms.cda.formatters.json.JsonV1; import cwms.cda.formatters.json.JsonV2; import cwms.cda.formatters.json.adapters.TimeSeriesRecordSerializer; import cwms.cda.formatters.xml.XMLv2; @@ -35,6 +37,8 @@ @JsonNaming(PropertyNamingStrategies.KebabCaseStrategy.class) @FormattableWith(contentType = Formats.JSONV2, formatter = JsonV2.class, aliases = {Formats.DEFAULT, Formats.JSON}) @FormattableWith(contentType = Formats.XMLV2, formatter = XMLv2.class, aliases = {Formats.XML}) +@FormattableWith(contentType = Formats.JSONV1, formatter = JsonV1.class) +@FormattableWith(contentType = Formats.CSV, formatter = OutputFormatter.DUMMY.class) public class TimeSeries extends CwmsDTOPaginated { public static final String ZONED_DATE_TIME_FORMAT = "yyyy-MM-dd'T'HH:mm:ssZ'['VV']'"; diff --git a/cwms-data-api/src/main/java/cwms/cda/data/dto/TimeZoneId.java b/cwms-data-api/src/main/java/cwms/cda/data/dto/TimeZoneId.java index 9f68753db3..949f16b0b6 100644 --- a/cwms-data-api/src/main/java/cwms/cda/data/dto/TimeZoneId.java +++ b/cwms-data-api/src/main/java/cwms/cda/data/dto/TimeZoneId.java @@ -13,13 +13,17 @@ import com.fasterxml.jackson.databind.annotation.JsonNaming; import cwms.cda.formatters.Formats; import cwms.cda.formatters.annotations.FormattableWith; +import cwms.cda.formatters.json.JsonV1; import cwms.cda.formatters.json.JsonV2; +import cwms.cda.formatters.xml.XMLv1; import cwms.cda.formatters.xml.XMLv2; import java.time.ZoneId; @JsonRootName("time-zone") @FormattableWith(contentType = Formats.JSONV2, formatter = JsonV2.class, aliases = {Formats.DEFAULT, Formats.JSON}) @FormattableWith(contentType = Formats.XMLV2, formatter = XMLv2.class, aliases = {Formats.XML}) +@FormattableWith(contentType = Formats.JSONV1, formatter = JsonV1.class) +@FormattableWith(contentType = Formats.XMLV1, formatter = XMLv1.class) @JsonNaming(PropertyNamingStrategies.KebabCaseStrategy.class) public final class TimeZoneId extends CwmsDTOBase { @JsonProperty(required = true) diff --git a/cwms-data-api/src/main/java/cwms/cda/data/dto/Unit.java b/cwms-data-api/src/main/java/cwms/cda/data/dto/Unit.java index 7bd1d1caec..0a013d6f51 100644 --- a/cwms-data-api/src/main/java/cwms/cda/data/dto/Unit.java +++ b/cwms-data-api/src/main/java/cwms/cda/data/dto/Unit.java @@ -12,7 +12,9 @@ import com.fasterxml.jackson.databind.annotation.JsonNaming; import cwms.cda.formatters.Formats; import cwms.cda.formatters.annotations.FormattableWith; +import cwms.cda.formatters.json.JsonV1; import cwms.cda.formatters.json.JsonV2; +import cwms.cda.formatters.xml.XMLv1; import cwms.cda.formatters.xml.XMLv2; import java.util.ArrayList; @@ -22,6 +24,8 @@ @JsonRootName("unit") @FormattableWith(contentType = Formats.JSONV2, formatter = JsonV2.class, aliases = {Formats.DEFAULT, Formats.JSON}) @FormattableWith(contentType = Formats.XMLV2, formatter = XMLv2.class, aliases = {Formats.XML}) +@FormattableWith(contentType = Formats.JSONV1, formatter = JsonV1.class) +@FormattableWith(contentType = Formats.XMLV1, formatter = XMLv1.class) @JsonNaming(PropertyNamingStrategies.KebabCaseStrategy.class) public final class Unit extends CwmsDTOBase { diff --git a/cwms-data-api/src/main/java/cwms/cda/data/dto/basinconnectivity/Basin.java b/cwms-data-api/src/main/java/cwms/cda/data/dto/basinconnectivity/Basin.java index fd54420796..f432e9ab19 100644 --- a/cwms-data-api/src/main/java/cwms/cda/data/dto/basinconnectivity/Basin.java +++ b/cwms-data-api/src/main/java/cwms/cda/data/dto/basinconnectivity/Basin.java @@ -2,6 +2,7 @@ import cwms.cda.data.dto.CwmsDTO; import cwms.cda.formatters.annotations.FormattableWith; +import cwms.cda.formatters.json.JsonV1; import cwms.cda.formatters.json.NamedPgJsonFormatter; import cwms.cda.formatters.Formats; import cwms.cda.formatters.json.PgJsonFormatter; @@ -9,6 +10,7 @@ @FormattableWith(contentType = Formats.NAMED_PGJSON, formatter = NamedPgJsonFormatter.class, aliases = {Formats.DEFAULT, Formats.JSON}) @FormattableWith(contentType = Formats.PGJSON, formatter = PgJsonFormatter.class) +@FormattableWith(contentType = Formats.JSONV1, formatter = JsonV1.class) public final class Basin extends CwmsDTO { private final String basinName; diff --git a/cwms-data-api/src/main/java/cwms/cda/data/dto/location/kind/Turbine.java b/cwms-data-api/src/main/java/cwms/cda/data/dto/location/kind/Turbine.java index 5d1e0af0d9..ce5c6402b0 100644 --- a/cwms-data-api/src/main/java/cwms/cda/data/dto/location/kind/Turbine.java +++ b/cwms-data-api/src/main/java/cwms/cda/data/dto/location/kind/Turbine.java @@ -35,7 +35,7 @@ import cwms.cda.formatters.annotations.FormattableWith; import cwms.cda.formatters.json.JsonV1; -@FormattableWith(contentType = Formats.JSONV1, formatter = JsonV1.class, aliases = {Formats.DEFAULT, Formats.JSONV1}) +@FormattableWith(contentType = Formats.JSONV1, formatter = JsonV1.class, aliases = {Formats.DEFAULT, Formats.JSON}) @JsonDeserialize(builder = Turbine.Builder.class) @JsonInclude(JsonInclude.Include.NON_NULL) @JsonNaming(PropertyNamingStrategies.KebabCaseStrategy.class) diff --git a/cwms-data-api/src/main/java/cwms/cda/data/dto/locationlevel/LocationLevels.java b/cwms-data-api/src/main/java/cwms/cda/data/dto/locationlevel/LocationLevels.java index 76fbbd204b..a9de210ab9 100644 --- a/cwms-data-api/src/main/java/cwms/cda/data/dto/locationlevel/LocationLevels.java +++ b/cwms-data-api/src/main/java/cwms/cda/data/dto/locationlevel/LocationLevels.java @@ -32,6 +32,7 @@ import cwms.cda.data.dto.CwmsDTOPaginated; import cwms.cda.formatters.Formats; import cwms.cda.formatters.annotations.FormattableWith; +import cwms.cda.formatters.json.JsonV1; import cwms.cda.formatters.json.JsonV2; import io.swagger.v3.oas.annotations.media.Schema; @@ -42,6 +43,7 @@ @JsonRootName("location-levels") @FormattableWith(contentType = Formats.JSONV2, formatter = JsonV2.class, aliases = {Formats.DEFAULT, Formats.JSON}) +@FormattableWith(contentType = Formats.JSONV1, formatter = JsonV1.class) public class LocationLevels extends CwmsDTOPaginated { @JacksonXmlElementWrapper @JacksonXmlProperty(localName = "location-level") diff --git a/cwms-data-api/src/main/java/cwms/cda/data/dto/rating/Ratings.java b/cwms-data-api/src/main/java/cwms/cda/data/dto/rating/Ratings.java new file mode 100644 index 0000000000..0c339057f3 --- /dev/null +++ b/cwms-data-api/src/main/java/cwms/cda/data/dto/rating/Ratings.java @@ -0,0 +1,13 @@ +package cwms.cda.data.dto.rating; + +import cwms.cda.data.dto.CwmsDTOBase; +import cwms.cda.formatters.Formats; +import cwms.cda.formatters.OutputFormatter; +import cwms.cda.formatters.annotations.FormattableWith; +import cwms.cda.formatters.xml.XMLv2; + +@FormattableWith(contentType = Formats.XMLV2, formatter = XMLv2.class, aliases = {Formats.DEFAULT, Formats.XML}) +@FormattableWith(contentType = Formats.JSONV2, formatter = OutputFormatter.DUMMY.class, aliases = {Formats.JSON}) +public class Ratings extends CwmsDTOBase { + /** marker interface for ContentType Annotations */ +} diff --git a/cwms-data-api/src/main/java/cwms/cda/formatters/ContentTypeAliasMap.java b/cwms-data-api/src/main/java/cwms/cda/formatters/ContentTypeAliasMap.java index ec1ae7bc7d..f839cb558b 100644 --- a/cwms-data-api/src/main/java/cwms/cda/formatters/ContentTypeAliasMap.java +++ b/cwms-data-api/src/main/java/cwms/cda/formatters/ContentTypeAliasMap.java @@ -24,7 +24,7 @@ private ContentTypeAliasMap(Class dtoClass) { FormattableWith[] formats = dtoClass.getAnnotationsByType(FormattableWith.class); for (FormattableWith format : formats) { ContentType type = new ContentType(format.contentType()); - + contentTypeMap.put(type, type); // we can always map to our self. for (String alias : format.aliases()) { contentTypeMap.put(new ContentType(alias), type); } diff --git a/cwms-data-api/src/main/java/cwms/cda/formatters/Formats.java b/cwms-data-api/src/main/java/cwms/cda/formatters/Formats.java index 1073721637..44f4ee669b 100644 --- a/cwms-data-api/src/main/java/cwms/cda/formatters/Formats.java +++ b/cwms-data-api/src/main/java/cwms/cda/formatters/Formats.java @@ -412,14 +412,6 @@ public static ContentType parseQueryParam(String queryParam, Class List parseContentList(String content, Class dtoList) { + throw new UnsupportedOperationException("Unimplemented method 'format'"); + } + } } diff --git a/cwms-data-api/src/test/java/cwms/cda/api/LocationControllerTestIT.java b/cwms-data-api/src/test/java/cwms/cda/api/LocationControllerTestIT.java index c4ccf7173d..8e77861106 100644 --- a/cwms-data-api/src/test/java/cwms/cda/api/LocationControllerTestIT.java +++ b/cwms-data-api/src/test/java/cwms/cda/api/LocationControllerTestIT.java @@ -54,6 +54,7 @@ import cwms.cda.formatters.ContentType; import cwms.cda.formatters.Formats; import cwms.cda.formatters.json.JsonV1; +import cwms.cda.helpers.DatabaseHelpers.SCHEMA_VERSION; import fixtures.CwmsDataApiSetupCallback; import fixtures.TestAccounts.KeyUser; import io.restassured.filter.log.LogDetail; @@ -1209,6 +1210,15 @@ void testDeleteConflict() throws Exception { String timeseriesId = "Putah_Creek.Elev.Ave.30Minutes.30Minutes.Raw"; createTimeseries(user.getOperatingOffice(), timeseriesId); + String expectedMessage = "Unable to delete requested location: Putah_Creek for office: SPK: ORA-20056: CAN_NOT_DELETE_LOC_2: Can not delete location: " + + "\"Putah_Creek\" because dependent data exists: time series identifiers=1."; + + if (CwmsDataApiSetupCallback.getSchemaVersion() <= SCHEMA_VERSION.V2026_07_16.numeric()) { + expectedMessage = "Unable to delete requested location: " + + "Putah_Creek for office: SPK: ORA-20031: CAN_NOT_DELETE_LOC_1: " + + "Can not delete location: \"Putah_Creek\" because Timeseries Identifiers exist."; + } + // attempt to delete location that is referenced by TS given() .log().ifValidationFails(LogDetail.ALL,true) @@ -1227,9 +1237,7 @@ void testDeleteConflict() throws Exception { .body("source", equalTo("Database")) .body("message", equalTo("Cannot delete this record because it is linked to other data in CWMS")) - .body("details.message", equalTo("Unable to delete requested location: " - + "Putah_Creek for office: SPK: ORA-20031: CAN_NOT_DELETE_LOC_1: " - + "Can not delete location: \"Putah_Creek\" because Timeseries Identifiers exist.")); + .body("details.message", equalTo(expectedMessage)); } @Test diff --git a/cwms-data-api/src/test/java/cwms/cda/api/rss/RssHandlerIT.java b/cwms-data-api/src/test/java/cwms/cda/api/rss/RssHandlerIT.java index 6da2d70432..e4925d1e59 100644 --- a/cwms-data-api/src/test/java/cwms/cda/api/rss/RssHandlerIT.java +++ b/cwms-data-api/src/test/java/cwms/cda/api/rss/RssHandlerIT.java @@ -144,7 +144,7 @@ void test_rss_feed_with_pagination() { String waitStr = nextPage.header("Retry-After"); int wait = waitStr != null && !waitStr.isEmpty() ? Integer.parseInt(waitStr) : 10; try { - Thread.sleep(wait*1000); + Thread.sleep(wait*1000 + 500 /* extra half second just to avoid the best being brittle */); } catch (InterruptedException ex) { LOGGER.atFine().withCause(ex).log("Next query wait was interrupted."); } diff --git a/cwms-data-api/src/test/java/cwms/cda/formatters/FormatsTest.java b/cwms-data-api/src/test/java/cwms/cda/formatters/FormatsTest.java index 266e00b69b..d91a66a791 100644 --- a/cwms-data-api/src/test/java/cwms/cda/formatters/FormatsTest.java +++ b/cwms-data-api/src/test/java/cwms/cda/formatters/FormatsTest.java @@ -12,6 +12,7 @@ import cwms.cda.data.dto.CwmsDTOBase; import cwms.cda.data.dto.locationlevel.LocationLevels; import cwms.cda.data.dto.Office; +import cwms.cda.data.dto.RecentValue; import cwms.cda.data.dto.State; import cwms.cda.data.dto.basinconnectivity.Basin; import cwms.cda.data.dto.project.LockRevokerRights; @@ -93,17 +94,9 @@ void testParseHeaderAndQueryParmXml() { Map parameters = contentType.getParameters(); assertTrue(parameters == null || parameters.isEmpty()); - - contentType = Formats.parseHeaderAndQueryParm("application/xml;version=2", null, Catalog.class); - - assertNotNull(contentType); - assertEquals("application/xml", contentType.getType()); - parameters = contentType.getParameters(); - assertNotNull(parameters); - assertFalse(parameters.isEmpty()); - assertTrue(parameters.containsKey("version")); - assertEquals("2", parameters.get("version")); - + /** xml;version=2 is not a supported format of Catalog */ + assertThrows(UnsupportedFormatException.class, + () -> Formats.parseHeaderAndQueryParm("application/xml;version=2", null, Catalog.class)); } @Test @@ -114,6 +107,10 @@ void testParseHeader() { assertNotNull(contentType); assertEquals("application/json", contentType.getType()); + contentType = Formats.parseHeader("application/json;version=1", Catalog.class); + assertNotNull(contentType); + assertEquals("application/json", contentType.getType()); + contentType = Formats.parseHeader("application/json;version=2", Catalog.class); assertNotNull(contentType); assertEquals("application/json", contentType.getType()); @@ -131,9 +128,10 @@ void testParseHeader() { assertThrows(FormattingException.class, () -> Formats.parseHeader("abc", Catalog.class)); assertThrows(FormattingException.class, () -> Formats.parseHeader("abc,def", Catalog.class)); + contentType = Formats.parseHeader("text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8", RecentValue.class); + assertEquals("application/json", contentType.getType()); } - @ParameterizedTest @EnumSource(ParseQueryOrParamTest.class) void test_header_or_query_parm(ParseQueryOrParamTest test) { diff --git a/cwms-data-api/src/test/java/fixtures/CwmsDataApiSetupCallback.java b/cwms-data-api/src/test/java/fixtures/CwmsDataApiSetupCallback.java index 8ee280deda..b1a95b3770 100644 --- a/cwms-data-api/src/test/java/fixtures/CwmsDataApiSetupCallback.java +++ b/cwms-data-api/src/test/java/fixtures/CwmsDataApiSetupCallback.java @@ -32,6 +32,7 @@ import fixtures.tomcat.SingleSignOnWrapper; import helpers.TsRandomSampler; import io.restassured.RestAssured; +import io.restassured.config.EncoderConfig; import io.restassured.config.JsonConfig; import io.restassured.filter.log.LogDetail; import io.restassured.path.json.config.JsonPathConfig; @@ -179,11 +180,24 @@ public void beforeAll(ExtensionContext context) throws Exception { RestAssured.baseURI=CwmsDataApiSetupCallback.httpUrl(); RestAssured.port = CwmsDataApiSetupCallback.httpPort(); RestAssured.basePath = System.getProperty("warContext"); - // we only use doubles - RestAssured.config() - .jsonConfig( - JsonConfig.jsonConfig() - .numberReturnType(JsonPathConfig.NumberReturnType.DOUBLE)); + // actually assign the new config to the global configuration. just running this here without + // the assignment apparently does nothing. + RestAssured.config = RestAssured.config() + // we only use doubles (NOTE: this is commend out because this config was + // never originally active and will be addressed in a followup) + // .jsonConfig( + // JsonConfig.jsonConfig() + // .numberReturnType(JsonPathConfig.NumberReturnType.DOUBLE)) + // our content type processing is a bit more picky now. + // I also don't recal seeing any default COntent-Type or Accept header + // defaults from browsers that include this much. + // if we start seeing it we need to add explicity @FormattableWith annotations + // per character as that is a distinct content-type. + .encoderConfig( + EncoderConfig.encoderConfig() + .appendDefaultContentCharsetToContentTypeIfUndefined( + false + )); healthCheck(); } }