From 177acd8f4eca610b6c026b442666f725c93366ce Mon Sep 17 00:00:00 2001 From: rma-rripken <89810919+rma-rripken@users.noreply.github.com> Date: Thu, 29 Jan 2026 13:07:19 -0800 Subject: [PATCH 01/14] Added example json for independent rounding --- .../cwms/cda/data/dto/rating/independent_rounding_spec.json | 4 ++++ 1 file changed, 4 insertions(+) create mode 100644 cwms-data-api/src/test/resources/cwms/cda/data/dto/rating/independent_rounding_spec.json diff --git a/cwms-data-api/src/test/resources/cwms/cda/data/dto/rating/independent_rounding_spec.json b/cwms-data-api/src/test/resources/cwms/cda/data/dto/rating/independent_rounding_spec.json new file mode 100644 index 000000000..d887a1dc9 --- /dev/null +++ b/cwms-data-api/src/test/resources/cwms/cda/data/dto/rating/independent_rounding_spec.json @@ -0,0 +1,4 @@ +{ + "position": 1, + "value": "12345" +} From 89cdac3304faf1791ef58902e25c5e0cdd6f8c76 Mon Sep 17 00:00:00 2001 From: rma-rripken <89810919+rma-rripken@users.noreply.github.com> Date: Thu, 29 Jan 2026 13:10:15 -0800 Subject: [PATCH 02/14] Added test for IndependentRoundingSpec JSON --- .../rating/IndependentRoundingSpecTest.java | 48 +++++++++++++++++++ 1 file changed, 48 insertions(+) create mode 100644 cwms-data-api/src/test/java/cwms/cda/data/dto/rating/IndependentRoundingSpecTest.java diff --git a/cwms-data-api/src/test/java/cwms/cda/data/dto/rating/IndependentRoundingSpecTest.java b/cwms-data-api/src/test/java/cwms/cda/data/dto/rating/IndependentRoundingSpecTest.java new file mode 100644 index 000000000..aff7f7782 --- /dev/null +++ b/cwms-data-api/src/test/java/cwms/cda/data/dto/rating/IndependentRoundingSpecTest.java @@ -0,0 +1,48 @@ +package cwms.cda.data.dto.rating; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNotNull; + +import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.databind.ObjectMapper; +import cwms.cda.formatters.json.JsonV2; +import cwms.cda.formatters.xml.XMLv2; +import java.io.IOException; +import java.io.InputStream; +import java.nio.charset.StandardCharsets; +import org.apache.commons.io.IOUtils; +import org.junit.jupiter.api.Test; + +public class IndependentRoundingSpecTest { + + @Test + void testDeserializeJSON() throws IOException { + InputStream resource = getClass().getResourceAsStream("/cwms/cda/data/dto/rating/independent_rounding_spec.json"); + assertNotNull(resource); + String json = IOUtils.toString(resource, StandardCharsets.UTF_8); + + ObjectMapper om = JsonV2.buildObjectMapper(); + IndependentRoundingSpec spec = om.readValue(json, IndependentRoundingSpec.class); + + assertNotNull(spec); + assertEquals(1, spec.getPosition()); + assertEquals("12345", spec.getValue()); + } + + @Test + void testRoundtripJSON() throws JsonProcessingException { + IndependentRoundingSpec spec = new IndependentRoundingSpec(2, "54321"); + + ObjectMapper om = JsonV2.buildObjectMapper(); + String json = om.writeValueAsString(spec); + assertNotNull(json); + + IndependentRoundingSpec spec2 = om.readValue(json, IndependentRoundingSpec.class); + assertNotNull(spec2); + assertEquals(spec.getPosition(), spec2.getPosition()); + assertEquals(spec.getValue(), spec2.getValue()); + assertEquals(spec, spec2); + } + + +} From 1b1aecdcb0223f5f6b74d8dbbda948c3f294cb6d Mon Sep 17 00:00:00 2001 From: rma-rripken <89810919+rma-rripken@users.noreply.github.com> Date: Thu, 29 Jan 2026 13:18:58 -0800 Subject: [PATCH 03/14] Added test for IndependentRoundingSpec XML --- .../dto/rating/IndependentRoundingSpec.java | 9 +++- .../IndependentRoundingSpecDeserializer.java | 50 +++++++++++++++++++ .../rating/IndependentRoundingSpecTest.java | 44 ++++++++++++++++ .../dto/rating/independent_rounding_spec.xml | 1 + 4 files changed, 103 insertions(+), 1 deletion(-) create mode 100644 cwms-data-api/src/main/java/cwms/cda/data/dto/rating/IndependentRoundingSpecDeserializer.java create mode 100644 cwms-data-api/src/test/resources/cwms/cda/data/dto/rating/independent_rounding_spec.xml diff --git a/cwms-data-api/src/main/java/cwms/cda/data/dto/rating/IndependentRoundingSpec.java b/cwms-data-api/src/main/java/cwms/cda/data/dto/rating/IndependentRoundingSpec.java index 036d9f042..f68ef1a87 100644 --- a/cwms-data-api/src/main/java/cwms/cda/data/dto/rating/IndependentRoundingSpec.java +++ b/cwms-data-api/src/main/java/cwms/cda/data/dto/rating/IndependentRoundingSpec.java @@ -1,10 +1,17 @@ package cwms.cda.data.dto.rating; +import cwms.cda.data.dto.CwmsDTOBase; import com.fasterxml.jackson.annotation.JsonProperty; +import com.fasterxml.jackson.databind.annotation.JsonDeserialize; +import com.fasterxml.jackson.dataformat.xml.annotation.JacksonXmlProperty; +import com.fasterxml.jackson.dataformat.xml.annotation.JacksonXmlText; -public class IndependentRoundingSpec { +@JsonDeserialize(using = IndependentRoundingSpecDeserializer.class) +public class IndependentRoundingSpec extends CwmsDTOBase { + @JacksonXmlProperty(isAttribute = true) private final Integer position; + @JacksonXmlText private final String value; public IndependentRoundingSpec(@JsonProperty("position") Integer position, @JsonProperty( diff --git a/cwms-data-api/src/main/java/cwms/cda/data/dto/rating/IndependentRoundingSpecDeserializer.java b/cwms-data-api/src/main/java/cwms/cda/data/dto/rating/IndependentRoundingSpecDeserializer.java new file mode 100644 index 000000000..96752b14d --- /dev/null +++ b/cwms-data-api/src/main/java/cwms/cda/data/dto/rating/IndependentRoundingSpecDeserializer.java @@ -0,0 +1,50 @@ +package cwms.cda.data.dto.rating; + +import com.fasterxml.jackson.core.JsonParser; +import com.fasterxml.jackson.core.JsonToken; +import com.fasterxml.jackson.databind.DeserializationContext; +import com.fasterxml.jackson.databind.JsonDeserializer; +import com.fasterxml.jackson.dataformat.xml.deser.FromXmlParser; + +import java.io.IOException; + +public class IndependentRoundingSpecDeserializer extends JsonDeserializer { + @Override + public IndependentRoundingSpec deserialize(JsonParser p, DeserializationContext ctxt) throws IOException { + Integer position = null; + String value = null; + + if (p instanceof FromXmlParser) { + FromXmlParser xmlP = (FromXmlParser) p; + if (xmlP.getCurrentToken() == JsonToken.START_OBJECT || xmlP.getCurrentToken() == JsonToken.FIELD_NAME) { + try { + String posAttr = xmlP.getStaxReader().getAttributeValue(null, "position"); + if (posAttr != null) { + position = Integer.parseInt(posAttr); + } + } catch (IllegalStateException e) { + // Not at START_ELEMENT, ignore + } + } + } + + if (p.getCurrentToken() == JsonToken.START_OBJECT) { + while (p.nextToken() != JsonToken.END_OBJECT) { + String fieldName = p.getCurrentName(); + p.nextToken(); + if ("position".equals(fieldName)) { + String posStr = p.getValueAsString(); + if (posStr != null) { + position = Integer.parseInt(posStr); + } + } else if ("value".equals(fieldName) || "".equals(fieldName)) { + value = p.getText(); + } + } + } else if (p.getCurrentToken() == JsonToken.VALUE_STRING) { + value = p.getText(); + } + + return new IndependentRoundingSpec(position, value); + } +} diff --git a/cwms-data-api/src/test/java/cwms/cda/data/dto/rating/IndependentRoundingSpecTest.java b/cwms-data-api/src/test/java/cwms/cda/data/dto/rating/IndependentRoundingSpecTest.java index aff7f7782..50866dd06 100644 --- a/cwms-data-api/src/test/java/cwms/cda/data/dto/rating/IndependentRoundingSpecTest.java +++ b/cwms-data-api/src/test/java/cwms/cda/data/dto/rating/IndependentRoundingSpecTest.java @@ -44,5 +44,49 @@ void testRoundtripJSON() throws JsonProcessingException { assertEquals(spec, spec2); } + @Test + void testDeserializeXML() throws IOException { + InputStream resource = getClass().getResourceAsStream("/cwms/cda/data/dto/rating/independent_rounding_spec.xml"); + assertNotNull(resource); + String xml = IOUtils.toString(resource, StandardCharsets.UTF_8); + + XMLv2 xmlv2 = new XMLv2(); + IndependentRoundingSpec spec = xmlv2.parseContent(xml, IndependentRoundingSpec.class); + + assertNotNull(spec); + assertEquals(1, spec.getPosition()); + assertEquals("12345", spec.getValue()); + } + + @Test + void testRoundtripXML() { + IndependentRoundingSpec spec = new IndependentRoundingSpec(3, "98765"); + + XMLv2 xmlv2 = new XMLv2(); + String xml = xmlv2.format(spec); + assertNotNull(xml); + + IndependentRoundingSpec spec2 = xmlv2.parseContent(xml, IndependentRoundingSpec.class); + assertNotNull(spec2); + assertEquals(spec.getPosition(), spec2.getPosition()); + assertEquals(spec.getValue(), spec2.getValue()); + assertEquals(spec, spec2); + } + + @Test + void testRoundtripXML_noPosition() { + IndependentRoundingSpec spec = new IndependentRoundingSpec("121212"); + + XMLv2 xmlv2 = new XMLv2(); + String xml = xmlv2.format(spec); + assertNotNull(xml); + + IndependentRoundingSpec spec2 = xmlv2.parseContent(xml, IndependentRoundingSpec.class); + assertNotNull(spec2); + assertEquals(spec.getPosition(), spec2.getPosition()); + assertEquals(spec.getValue(), spec2.getValue()); + assertEquals(spec, spec2); + } + } diff --git a/cwms-data-api/src/test/resources/cwms/cda/data/dto/rating/independent_rounding_spec.xml b/cwms-data-api/src/test/resources/cwms/cda/data/dto/rating/independent_rounding_spec.xml new file mode 100644 index 000000000..b86b36fb2 --- /dev/null +++ b/cwms-data-api/src/test/resources/cwms/cda/data/dto/rating/independent_rounding_spec.xml @@ -0,0 +1 @@ +12345 From 675bbceb987568cadf44cb06f4c7bfcc0db26f29 Mon Sep 17 00:00:00 2001 From: rma-rripken <89810919+rma-rripken@users.noreply.github.com> Date: Thu, 29 Jan 2026 13:38:01 -0800 Subject: [PATCH 04/14] Added test for RatinSpec XML --- .../cwms/cda/data/dto/rating/RatingSpec.java | 8 +- .../cda/data/dto/rating/RatingSpecTest.java | 145 ++++++++++-------- 2 files changed, 87 insertions(+), 66 deletions(-) diff --git a/cwms-data-api/src/main/java/cwms/cda/data/dto/rating/RatingSpec.java b/cwms-data-api/src/main/java/cwms/cda/data/dto/rating/RatingSpec.java index 8d4654220..e27d20b3a 100644 --- a/cwms-data-api/src/main/java/cwms/cda/data/dto/rating/RatingSpec.java +++ b/cwms-data-api/src/main/java/cwms/cda/data/dto/rating/RatingSpec.java @@ -6,11 +6,13 @@ import com.fasterxml.jackson.databind.annotation.JsonDeserialize; import com.fasterxml.jackson.databind.annotation.JsonNaming; import com.fasterxml.jackson.databind.annotation.JsonPOJOBuilder; -import cwms.cda.api.errors.FieldException; +import com.fasterxml.jackson.dataformat.xml.annotation.JacksonXmlElementWrapper; +import com.fasterxml.jackson.dataformat.xml.annotation.JacksonXmlProperty; import cwms.cda.data.dto.CwmsDTO; import cwms.cda.formatters.Formats; import cwms.cda.formatters.annotations.FormattableWith; import cwms.cda.formatters.json.JsonV2; +import cwms.cda.formatters.xml.XMLv2; import hec.data.cwmsRating.RatingConst; import java.time.ZonedDateTime; import java.util.ArrayList; @@ -22,6 +24,7 @@ @JsonInclude(JsonInclude.Include.NON_NULL) @JsonNaming(PropertyNamingStrategies.KebabCaseStrategy.class) @FormattableWith(contentType = Formats.JSONV2, formatter = JsonV2.class, aliases = {Formats.DEFAULT, Formats.JSON}) +@FormattableWith(contentType = Formats.XMLV2, formatter = XMLv2.class) public class RatingSpec extends CwmsDTO { private final String ratingId; private final String templateId; @@ -36,6 +39,9 @@ public class RatingSpec extends CwmsDTO { private final boolean autoUpdate; private final boolean autoActivate; private final boolean autoMigrateExtension; + + @JacksonXmlElementWrapper(localName = "independent-rounding-specs") + @JacksonXmlProperty(localName = "independent-rounding-spec") private final IndependentRoundingSpec[] independentRoundingSpecs; private final String dependentRoundingSpec; private final String description; diff --git a/cwms-data-api/src/test/java/cwms/cda/data/dto/rating/RatingSpecTest.java b/cwms-data-api/src/test/java/cwms/cda/data/dto/rating/RatingSpecTest.java index cf9a50b96..efad4c173 100644 --- a/cwms-data-api/src/test/java/cwms/cda/data/dto/rating/RatingSpecTest.java +++ b/cwms-data-api/src/test/java/cwms/cda/data/dto/rating/RatingSpecTest.java @@ -3,99 +3,114 @@ import static cwms.cda.data.dto.rating.RatingSpec.Builder.buildIndependentRoundingSpecs; import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertTrue; import com.fasterxml.jackson.core.JsonProcessingException; import com.fasterxml.jackson.databind.ObjectMapper; - import cwms.cda.formatters.json.JsonV2; - +import cwms.cda.formatters.xml.XMLv2; import org.junit.jupiter.api.Test; -public class RatingSpecTest -{ +public class RatingSpecTest { + + @Test + void testSerialize() throws JsonProcessingException { + String officeId = "SWT"; + String ratingId = "ARBU.Elev;Stor.Linear.Production"; + + RatingSpec spec = buildRatingSpec(officeId, ratingId); + + ObjectMapper om = JsonV2.buildObjectMapper(); + String serializedLocation = om.writeValueAsString(spec); + assertNotNull(serializedLocation); + + } - @Test - void testSerialize() throws JsonProcessingException - { - String officeId = "SWT"; - String ratingId = "ARBU.Elev;Stor.Linear.Production"; + @Test + void testRoundtripJSON() throws JsonProcessingException { + String officeId = "SWT"; + String ratingId = "ARBU.Elev;Stor.Linear.Production"; - RatingSpec spec = buildRatingSpec(officeId, ratingId); + RatingSpec spec = buildRatingSpec(officeId, ratingId); - ObjectMapper om = JsonV2.buildObjectMapper(); - String serializedLocation = om.writeValueAsString(spec); - assertNotNull(serializedLocation); + ObjectMapper om = JsonV2.buildObjectMapper(); + String serializedSpec = om.writeValueAsString(spec); + assertNotNull(serializedSpec); - } + RatingSpec spec2 = om.readValue(serializedSpec, RatingSpec.class); + assertNotNull(spec2); + assertEquals(spec, spec2); + } - @Test - void testRoundtripJSON() throws JsonProcessingException - { - String officeId = "SWT"; - String ratingId = "ARBU.Elev;Stor.Linear.Production"; + @Test + void testRoundtripXML() { + String officeId = "SWT"; + String ratingId = "ARBU.Elev;Stor.Linear.Production"; - RatingSpec spec = buildRatingSpec(officeId, ratingId); + RatingSpec spec = buildRatingSpec(officeId, ratingId); - ObjectMapper om = JsonV2.buildObjectMapper(); - String serializedLocation = om.writeValueAsString(spec); - assertNotNull(serializedLocation); + XMLv2 xmlv2 = new XMLv2(); - RatingSpec spec2 = om.readValue(serializedLocation, RatingSpec.class); - assertNotNull(spec2); + String xml = xmlv2.format(spec); + assertNotNull(xml); + System.out.println(xml); + assertTrue(xml.contains("ARBU.Elev;Stor.Linear.Production")); - } + RatingSpec spec2 = xmlv2.parseContent(xml, RatingSpec.class); + assertNotNull(spec2); + assertEquals(spec, spec2); + } - public static RatingSpec buildRatingSpec(String officeId, String ratingId) - { - RatingSpec retval; + public static RatingSpec buildRatingSpec(String officeId, String ratingId) { + RatingSpec retval; - String templateId = "Elev;Stor.Linear"; - String locId = "ARBU"; - String version = "Production"; - String agency = null; + String templateId = "Elev;Stor.Linear"; + String locId = "ARBU"; + String version = "Production"; + String agency = null; - boolean activeFlag = true; + boolean activeFlag = true; - boolean autoUpdateFlag = false; + boolean autoUpdateFlag = false; - boolean autoActivateFlag = false; + boolean autoActivateFlag = false; - boolean autoMigrateExtFlag = false; - String indRndSpecs = "2222233332"; + boolean autoMigrateExtFlag = false; + String indRndSpecs = "2222233332"; - String depRndSpecs = "2222233332"; - String desc = null; + String depRndSpecs = "2222233332"; + String desc = null; - String dateMethods = "LINEAR,NEAREST,LOWER"; + String dateMethods = "LINEAR,NEAREST,LOWER"; - RatingSpec.Builder builder = new RatingSpec.Builder(); - builder = builder - .withOfficeId(officeId).withRatingId(ratingId) - .withTemplateId(templateId).withLocationId(locId) - .withVersion(version).withSourceAgency(agency) - .withActive(activeFlag).withAutoUpdate(autoUpdateFlag) - .withAutoActivate(autoActivateFlag) - .withAutoMigrateExtension(autoMigrateExtFlag) - .withIndependentRoundingSpecs(buildIndependentRoundingSpecs(indRndSpecs)) - .withDependentRoundingSpec(depRndSpecs).withDescription(desc) - .withDateMethods(dateMethods); - retval = builder.build(); + RatingSpec.Builder builder = new RatingSpec.Builder(); + builder = builder + .withOfficeId(officeId).withRatingId(ratingId) + .withTemplateId(templateId).withLocationId(locId) + .withVersion(version).withSourceAgency(agency) + .withActive(activeFlag).withAutoUpdate(autoUpdateFlag) + .withAutoActivate(autoActivateFlag) + .withAutoMigrateExtension(autoMigrateExtFlag) + .withIndependentRoundingSpecs(buildIndependentRoundingSpecs(indRndSpecs)) + .withDependentRoundingSpec(depRndSpecs).withDescription(desc) + .withDateMethods(dateMethods); + retval = builder.build(); - assertEquals("LINEAR", retval.getOutRangeLowMethod()); - assertEquals("NEAREST", retval.getInRangeMethod()); - assertEquals("LOWER", retval.getOutRangeHighMethod()); + assertEquals("LINEAR", retval.getOutRangeLowMethod()); + assertEquals("NEAREST", retval.getInRangeMethod()); + assertEquals("LOWER", retval.getOutRangeHighMethod()); - RatingSpec testSpec = builder.withInRangeMethod("InRange") - .withOutRangeLowMethod("OutRangeLow") - .withOutRangeHighMethod("OutRangeHigh") - .build(); + RatingSpec testSpec = builder.withInRangeMethod("InRange") + .withOutRangeLowMethod("OutRangeLow") + .withOutRangeHighMethod("OutRangeHigh") + .build(); - assertEquals("OutRangeLow", testSpec.getOutRangeLowMethod()); - assertEquals("InRange", testSpec.getInRangeMethod()); - assertEquals("OutRangeHigh", testSpec.getOutRangeHighMethod()); + assertEquals("OutRangeLow", testSpec.getOutRangeLowMethod()); + assertEquals("InRange", testSpec.getInRangeMethod()); + assertEquals("OutRangeHigh", testSpec.getOutRangeHighMethod()); - return retval; - } + return retval; + } } \ No newline at end of file From d056fa14a2e4262a0b1549ab55d84c91f233f61c Mon Sep 17 00:00:00 2001 From: rma-rripken <89810919+rma-rripken@users.noreply.github.com> Date: Thu, 29 Jan 2026 16:10:33 -0800 Subject: [PATCH 05/14] Added deserialization tests for RatingSpec with test resource files --- .../cwms/cda/data/dto/rating/RatingSpec.java | 3 +- .../cda/data/dto/rating/RatingSpecTest.java | 29 +++++++++++++++++++ .../cwms/cda/data/dto/rating/rating_spec.json | 18 ++++++++++++ .../cwms/cda/data/dto/rating/rating_spec.xml | 18 ++++++++++++ 4 files changed, 67 insertions(+), 1 deletion(-) create mode 100644 cwms-data-api/src/test/resources/cwms/cda/data/dto/rating/rating_spec.json create mode 100644 cwms-data-api/src/test/resources/cwms/cda/data/dto/rating/rating_spec.xml diff --git a/cwms-data-api/src/main/java/cwms/cda/data/dto/rating/RatingSpec.java b/cwms-data-api/src/main/java/cwms/cda/data/dto/rating/RatingSpec.java index e27d20b3a..476abc92f 100644 --- a/cwms-data-api/src/main/java/cwms/cda/data/dto/rating/RatingSpec.java +++ b/cwms-data-api/src/main/java/cwms/cda/data/dto/rating/RatingSpec.java @@ -325,6 +325,7 @@ public Builder withIndependentRoundingSpecs(IndependentRoundingSpec[] indRoundin return this; } + public static IndependentRoundingSpec[] buildIndependentRoundingSpecs( String indRoundingSpecsStr) { IndependentRoundingSpec[] retval = null; @@ -336,7 +337,7 @@ public static IndependentRoundingSpec[] buildIndependentRoundingSpecs( } @NotNull - private static IndependentRoundingSpec[] buildIndependentRoundingSpecs( + public static IndependentRoundingSpec[] buildIndependentRoundingSpecs( String[] indRoundingSpecsStrArr) { IndependentRoundingSpec[] retval; retval = new IndependentRoundingSpec[indRoundingSpecsStrArr.length]; diff --git a/cwms-data-api/src/test/java/cwms/cda/data/dto/rating/RatingSpecTest.java b/cwms-data-api/src/test/java/cwms/cda/data/dto/rating/RatingSpecTest.java index efad4c173..a8c4178f1 100644 --- a/cwms-data-api/src/test/java/cwms/cda/data/dto/rating/RatingSpecTest.java +++ b/cwms-data-api/src/test/java/cwms/cda/data/dto/rating/RatingSpecTest.java @@ -9,10 +9,39 @@ import com.fasterxml.jackson.databind.ObjectMapper; import cwms.cda.formatters.json.JsonV2; import cwms.cda.formatters.xml.XMLv2; +import org.apache.commons.io.IOUtils; import org.junit.jupiter.api.Test; +import java.io.IOException; +import java.io.InputStream; +import java.nio.charset.StandardCharsets; + public class RatingSpecTest { + @Test + void testDeserializeJSON() throws IOException { + InputStream resource = getClass().getResourceAsStream("/cwms/cda/data/dto/rating/rating_spec.json"); + assertNotNull(resource); + String json = IOUtils.toString(resource, StandardCharsets.UTF_8); + + ObjectMapper om = JsonV2.buildObjectMapper(); + RatingSpec spec = om.readValue(json, RatingSpec.class); + + assertNotNull(spec); + } + + @Test + void testDeserializeXml() throws IOException { + InputStream resource = getClass().getResourceAsStream("/cwms/cda/data/dto/rating/rating_spec.xml"); + assertNotNull(resource); + String xml = IOUtils.toString(resource, StandardCharsets.UTF_8); + + XMLv2 xmlv2 = new XMLv2(); + RatingSpec spec = xmlv2.parseContent(xml, RatingSpec.class); + + assertNotNull(spec); + } + @Test void testSerialize() throws JsonProcessingException { String officeId = "SWT"; diff --git a/cwms-data-api/src/test/resources/cwms/cda/data/dto/rating/rating_spec.json b/cwms-data-api/src/test/resources/cwms/cda/data/dto/rating/rating_spec.json new file mode 100644 index 000000000..7e8d58bb4 --- /dev/null +++ b/cwms-data-api/src/test/resources/cwms/cda/data/dto/rating/rating_spec.json @@ -0,0 +1,18 @@ +{ + "office-id" : "SWT", + "rating-id" : "ARBU.Elev;Stor.Linear.Production", + "template-id" : "Elev;Stor.Linear", + "location-id" : "ARBU", + "version" : "Production", + "in-range-method" : "NEAREST", + "out-range-low-method" : "LINEAR", + "out-range-high-method" : "LOWER", + "active" : true, + "auto-update" : false, + "auto-activate" : false, + "auto-migrate-extension" : false, + "independent-rounding-specs" : [ { + "value" : "2222233332" + } ], + "dependent-rounding-spec" : "2222233332" +} \ No newline at end of file diff --git a/cwms-data-api/src/test/resources/cwms/cda/data/dto/rating/rating_spec.xml b/cwms-data-api/src/test/resources/cwms/cda/data/dto/rating/rating_spec.xml new file mode 100644 index 000000000..4da7cc474 --- /dev/null +++ b/cwms-data-api/src/test/resources/cwms/cda/data/dto/rating/rating_spec.xml @@ -0,0 +1,18 @@ + + SWT + ARBU.Elev;Stor.Linear.Production + Elev;Stor.Linear + ARBU + Production + NEAREST + LINEAR + LOWER + true + false + false + false + 2222233332 + + 2222233332 + + From 77e6549f2a507764f0876b5f34ae4fe4692b0d56 Mon Sep 17 00:00:00 2001 From: rma-rripken <89810919+rma-rripken@users.noreply.github.com> Date: Thu, 29 Jan 2026 18:18:31 -0800 Subject: [PATCH 06/14] Created a RatingSpecDao method that takes a CDA RatingSpec object and internally generates the necessary XML for pl/sql call. --- .../cda/api/rating/RatingSpecController.java | 121 ++++++++++-------- .../java/cwms/cda/data/dao/RatingSpecDao.java | 52 +++++--- .../cwms/cda/data/dao/RatingSpecXmlUtils.java | 84 ++++++++++++ .../java/cwms/cda/formatters/xml/XMLv2.java | 2 +- .../rating/RatingSpecControllerTestIT.java | 109 ++++++++++++++++ .../cda/data/dao/RatingSpecXmlUtilsTest.java | 37 ++++++ 6 files changed, 327 insertions(+), 78 deletions(-) create mode 100644 cwms-data-api/src/main/java/cwms/cda/data/dao/RatingSpecXmlUtils.java create mode 100644 cwms-data-api/src/test/java/cwms/cda/data/dao/RatingSpecXmlUtilsTest.java diff --git a/cwms-data-api/src/main/java/cwms/cda/api/rating/RatingSpecController.java b/cwms-data-api/src/main/java/cwms/cda/api/rating/RatingSpecController.java index c963ad079..d91cd9747 100644 --- a/cwms-data-api/src/main/java/cwms/cda/api/rating/RatingSpecController.java +++ b/cwms-data-api/src/main/java/cwms/cda/api/rating/RatingSpecController.java @@ -30,15 +30,18 @@ import com.codahale.metrics.Histogram; import com.codahale.metrics.MetricRegistry; import com.codahale.metrics.Timer; +import com.fasterxml.jackson.core.JacksonException; +import com.fasterxml.jackson.databind.ObjectMapper; import cwms.cda.api.Controllers; import cwms.cda.api.errors.CdaError; import cwms.cda.data.dao.JooqDao; -import cwms.cda.data.dao.JsonRatingUtils; import cwms.cda.data.dao.RatingSpecDao; import cwms.cda.data.dto.rating.RatingSpec; import cwms.cda.data.dto.rating.RatingSpecs; import cwms.cda.formatters.ContentType; import cwms.cda.formatters.Formats; +import cwms.cda.formatters.json.JsonV2; +import cwms.cda.formatters.xml.XMLv2; import io.javalin.apibuilder.CrudHandler; import io.javalin.core.util.Header; import io.javalin.http.Context; @@ -48,11 +51,13 @@ import io.javalin.plugin.openapi.annotations.OpenApiParam; import io.javalin.plugin.openapi.annotations.OpenApiRequestBody; import io.javalin.plugin.openapi.annotations.OpenApiResponse; -import java.io.IOException; + import java.util.Optional; + import com.google.common.flogger.FluentLogger; + import javax.servlet.http.HttpServletResponse; -import javax.xml.transform.TransformerException; + import org.jetbrains.annotations.NotNull; import org.jooq.DSLContext; @@ -100,7 +105,7 @@ private Timer.Context markAndTime(String subject) { ), @OpenApiParam(name = PAGE_SIZE, type = Integer.class, description = "How many entries per page returned. " - + "Default " + DEFAULT_PAGE_SIZE + "." + + "Default " + DEFAULT_PAGE_SIZE + "." ), }, responses = { @@ -122,7 +127,7 @@ public void getAll(Context ctx) { String formatHeader = ctx.header(Header.ACCEPT); ContentType contentType = Formats.parseHeader(formatHeader, RatingSpecs.class); - try (final Timer.Context timeContext = markAndTime(GET_ALL)){ + try (final Timer.Context ignored = markAndTime(GET_ALL)) { DSLContext dsl = getDslContext(ctx); RatingSpecDao ratingSpecDao = getRatingSpecDao(dsl); @@ -171,7 +176,7 @@ public void getOne(Context ctx, String ratingId) { String office = ctx.queryParam(OFFICE); - try (final Timer.Context timeContext = markAndTime(GET_ONE)){ + try (final Timer.Context ignored = markAndTime(GET_ONE)) { DSLContext dsl = getDslContext(ctx); RatingSpecDao ratingSpecDao = getRatingSpecDao(dsl); @@ -201,59 +206,63 @@ protected RatingSpecDao getRatingSpecDao(DSLContext dsl) { @OpenApi( - description = "Create new Rating Specification", - requestBody = @OpenApiRequestBody( - content = { - @OpenApiContent(from = RatingSpec.class, type = Formats.XMLV2) + description = "Create new Rating Specification", + requestBody = @OpenApiRequestBody( + content = { + @OpenApiContent(from = RatingSpec.class, type = Formats.JSON), + @OpenApiContent(from = RatingSpec.class, type = Formats.XMLV2) + }, + required = true), + queryParams = { + @OpenApiParam(name = FAIL_IF_EXISTS, type = Boolean.class, + description = "Create will fail if provided ID already exists. Default: true") }, - required = true), - queryParams = { - @OpenApiParam(name = FAIL_IF_EXISTS, type = Boolean.class, - description = "Create will fail if provided ID already exists. Default: true") - }, - method = HttpMethod.POST, - tags = {TAG} + method = HttpMethod.POST, + tags = {TAG} ) @Override public void create(Context ctx) { - try (final Timer.Context ignored = markAndTime(CREATE)){ + try (final Timer.Context ignored = markAndTime(CREATE)) { DSLContext dsl = getDslContext(ctx); String reqContentType = ctx.req.getContentType(); String formatHeader = reqContentType != null ? reqContentType : Formats.XMLV2; + boolean failIfExists = ctx.queryParamAsClass(FAIL_IF_EXISTS, Boolean.class).getOrDefault(false); String body = ctx.body(); - String xml = translateToXml(body, formatHeader); RatingSpecDao dao = new RatingSpecDao(dsl); - boolean failIfExists = ctx.queryParamAsClass(FAIL_IF_EXISTS, Boolean.class).getOrDefault(false); - dao.create(xml, failIfExists); - ctx.status(HttpServletResponse.SC_CREATED); - } - } - - private static String translateToXml(String body, String contentType) { - String retval; - if (contentType.contains(Formats.XMLV2)) { - retval = body; - } else if (contentType.contains(Formats.JSONV2)) { - retval = translateJsonToXml(body); - } else { - throw new IllegalArgumentException("Unexpected contentType format:" + contentType); - } + // First we are going to try and build a CDA RatingSpec from whatever the user gave us. + RatingSpec spec = null; + try { + if (formatHeader.contains(Formats.XMLV2)) { + XMLv2 xmlv2 = new XMLv2(); + spec = xmlv2.parseContent(body, RatingSpec.class); + } else if (formatHeader.contains(Formats.JSONV2)) { + ObjectMapper jsonMapper = JsonV2.buildObjectMapper(); + spec = jsonMapper.readValue(body, RatingSpec.class); + } + } catch (JacksonException e) { + logger.atInfo().withCause(e).log("Unable to parse Rating Spec from request body"); + } - return retval; - } + if (spec != null) { + // If we were able to parse the RatingSpec from the request body, then we + // should call the dao method that takes a CDA object and the dao will sort it out. + dao.create(spec, failIfExists); + ctx.status(HttpServletResponse.SC_CREATED); + } else if (formatHeader.contains(Formats.XMLV2)) { + // This branch is if the user said its xml and it doesn't parse into the CDA RatingSpec + // object. We'll let the dao try passing it thru to the pl/sql. + dao.create(body, failIfExists); + ctx.status(HttpServletResponse.SC_CREATED); + } else { + throw new IllegalArgumentException("Could not parse body with format:" + formatHeader); + } - private static String translateJsonToXml(String body) { - String retval; - try { - retval = JsonRatingUtils.jsonToXml(body); - } catch (IOException | TransformerException ex) { - throw new IllegalArgumentException("Failed to translate request into rating spec XML", ex); } - return retval; } + @OpenApi(ignore = true) @Override public void update(Context ctx, String locationCode) { @@ -261,22 +270,22 @@ public void update(Context ctx, String locationCode) { } @OpenApi( - pathParams = { - @OpenApiParam(name = RATING_ID, required = true, description = "The rating-spec-id of the ratings data to be deleted."), - }, - queryParams = { - @OpenApiParam(name = OFFICE, required = true, description = "Specifies the " - + "owning office of the ratings to be deleted."), - @OpenApiParam(name = METHOD, required = true, description = "Specifies the delete method used.", - type = JooqDao.DeleteMethod.class) - }, - description = "Deletes requested rating specification", - method = HttpMethod.DELETE, - tags = {TAG} + pathParams = { + @OpenApiParam(name = RATING_ID, required = true, description = "The rating-spec-id of the ratings data to be deleted."), + }, + queryParams = { + @OpenApiParam(name = OFFICE, required = true, description = "Specifies the " + + "owning office of the ratings to be deleted."), + @OpenApiParam(name = METHOD, required = true, description = "Specifies the delete method used.", + type = JooqDao.DeleteMethod.class) + }, + description = "Deletes requested rating specification", + method = HttpMethod.DELETE, + tags = {TAG} ) @Override public void delete(Context ctx, @NotNull String ratingSpecId) { - try (final Timer.Context ignored = markAndTime(DELETE)){ + try (final Timer.Context ignored = markAndTime(DELETE)) { DSLContext dsl = getDslContext(ctx); String office = ctx.queryParam(OFFICE); diff --git a/cwms-data-api/src/main/java/cwms/cda/data/dao/RatingSpecDao.java b/cwms-data-api/src/main/java/cwms/cda/data/dao/RatingSpecDao.java index 16a345da8..5c23885f0 100644 --- a/cwms-data-api/src/main/java/cwms/cda/data/dao/RatingSpecDao.java +++ b/cwms-data-api/src/main/java/cwms/cda/data/dao/RatingSpecDao.java @@ -34,6 +34,7 @@ import cwms.cda.data.dto.rating.RatingSpec; import cwms.cda.data.dto.rating.RatingSpecEffectiveDates; import cwms.cda.data.dto.rating.RatingSpecs; + import java.sql.CallableStatement; import java.sql.Connection; import java.sql.ResultSet; @@ -58,11 +59,14 @@ import java.util.TimeZone; import java.util.TreeMap; import java.util.TreeSet; + import com.google.common.flogger.FluentLogger; + import java.util.stream.Collectors; import java.util.stream.Stream; import javax.sql.rowset.CachedRowSet; import javax.sql.rowset.RowSetProvider; + import org.jetbrains.annotations.NotNull; import org.jooq.Condition; import org.jooq.DSLContext; @@ -388,7 +392,7 @@ public static RatingSpec buildRatingSpec(Record rec) { public void delete(String office, DeleteMethod deleteMethod, String ratingSpecId) { String deleteAction; - switch(deleteMethod) { + switch (deleteMethod) { case DELETE_ALL: deleteAction = DeleteRule.DELETE_ALL.getRule(); break; @@ -400,24 +404,31 @@ public void delete(String office, DeleteMethod deleteMethod, String ratingSpecId break; default: throw new IllegalArgumentException("Delete Method provided does not match accepted rule constants: " - + deleteMethod); + + deleteMethod); } dsl.connection(c -> - CWMS_RATING_PACKAGE.call_DELETE_SPECS( - getDslContext(c,office).configuration(), - ratingSpecId, - deleteAction, - office) + CWMS_RATING_PACKAGE.call_DELETE_SPECS( + getDslContext(c, office).configuration(), + ratingSpecId, + deleteAction, + office) ); } + public void create(RatingSpec spec, boolean failIfExists) { + String xml = RatingSpecXmlUtils.toPlSqlXml(spec); + create(xml, failIfExists); + } + + // In my tests this method wouldn't fail if the input was + // mostly right, it just wouldn't create anything. public void create(String xml, boolean failIfExists) { final String office = RatingDao.extractOfficeFromXml(xml); dsl.connection(c -> - CWMS_RATING_PACKAGE.call_STORE_SPECS__3( - getDslContext(c,office).configuration(), - xml, - formatBool(failIfExists)) + CWMS_RATING_PACKAGE.call_STORE_SPECS__3( + getDslContext(c, office).configuration(), + xml, + formatBool(failIfExists)) ); } @@ -432,21 +443,21 @@ public RatingEffectiveDatesMap retrieveSpecEffectiveDates(String officeIdMask, S //office->spec->dates NavigableMap>> specDateMap = new TreeMap<>(); //instantiate empty Instant list for each office/spec combination so that specs with no effective dates are included in the final result - for(Map.Entry> entry : officeToRatingIdsNoAliasesMap.entrySet()) { + for (Map.Entry> entry : officeToRatingIdsNoAliasesMap.entrySet()) { String officeId = entry.getKey(); List specIds = entry.getValue(); NavigableMap> specMap = specDateMap.computeIfAbsent(officeId, k -> new TreeMap<>()); - for(String specId : specIds) { + for (String specId : specIds) { specMap.put(specId, new TreeSet<>()); } } - try(ResultSet rs = catRatings(conn, officeIdMask, specIdMask, begin, end)) { + try (ResultSet rs = catRatings(conn, officeIdMask, specIdMask, begin, end)) { checkMetaData(rs.getMetaData(), RATINGS_COLUMN_LIST, "Ratings"); - while(rs.next()) { + while (rs.next()) { String officeId = rs.getString(OFFICE_ID); String specId = rs.getString(SPECIFICATION_ID); List ratingIdsNoAliases = officeToRatingIdsNoAliasesMap.get(officeId); - if(ratingIdsNoAliases != null && !ratingIdsNoAliases.contains(specId)) { // skip aliased specs based on queried list of rating ids not including aliases + if (ratingIdsNoAliases != null && !ratingIdsNoAliases.contains(specId)) { // skip aliased specs based on queried list of rating ids not including aliases continue; } Timestamp timestamp = rs.getTimestamp(EFFECTIVE_DATE, GMT_CALENDAR); @@ -463,11 +474,11 @@ public RatingEffectiveDatesMap retrieveSpecEffectiveDates(String officeIdMask, S //package scoped for unit testing static RatingEffectiveDatesMap buildRatingEffectiveDatesMap(NavigableMap>> specDateMap) { Map> officeToSpecDatesMap = new LinkedHashMap<>(specDateMap.size()); - for(Map.Entry>> entry : specDateMap.entrySet()) { + for (Map.Entry>> entry : specDateMap.entrySet()) { String officeId = entry.getKey(); List specEffectiveDatesForOffice = new ArrayList<>(); NavigableMap> specMap = entry.getValue(); - for(Map.Entry> specEntry : specMap.entrySet()) { + for (Map.Entry> specEntry : specMap.entrySet()) { String specId = specEntry.getKey(); NavigableSet dateList = specEntry.getValue(); if (dateList.isEmpty()) { @@ -495,8 +506,7 @@ private ResultSet catRatings(Connection conn, String officeIdMask, String specId CachedRowSet output = RowSetProvider.newFactory() .createCachedRowSet(); - try (CallableStatement statement = conn.prepareCall("{CALL CWMS_20.CWMS_RATING.CAT_RATINGS(?, ?, ?, ?, ?, ?)}")) - { + try (CallableStatement statement = conn.prepareCall("{CALL CWMS_20.CWMS_RATING.CAT_RATINGS(?, ?, ?, ?, ?, ?)}")) { statement.registerOutParameter(1, Types.REF_CURSOR); statement.setString(2, specIdMask); statement.setTimestamp(3, pEffectiveDateStart, GMT_CALENDAR); @@ -527,7 +537,7 @@ private Map> getRatingIds(String office, String templateIdM condition = condition.and(ratingIdLike); } - if(!includeAliases) { + if (!includeAliases) { condition = condition.and(specView.ALIASED_ITEM.isNull()); } diff --git a/cwms-data-api/src/main/java/cwms/cda/data/dao/RatingSpecXmlUtils.java b/cwms-data-api/src/main/java/cwms/cda/data/dao/RatingSpecXmlUtils.java new file mode 100644 index 000000000..058d00743 --- /dev/null +++ b/cwms-data-api/src/main/java/cwms/cda/data/dao/RatingSpecXmlUtils.java @@ -0,0 +1,84 @@ +package cwms.cda.data.dao; + +import com.fasterxml.jackson.annotation.JsonIgnore; +import com.fasterxml.jackson.dataformat.xml.annotation.JacksonXmlElementWrapper; +import com.fasterxml.jackson.dataformat.xml.annotation.JacksonXmlProperty; +import com.fasterxml.jackson.databind.SerializationFeature; +import com.fasterxml.jackson.dataformat.xml.XmlMapper; +import com.fasterxml.jackson.dataformat.xml.ser.ToXmlGenerator; +import cwms.cda.data.dto.rating.IndependentRoundingSpec; +import cwms.cda.data.dto.rating.RatingSpec; +import cwms.cda.formatters.xml.XMLv2; + +import java.time.ZonedDateTime; +import java.util.List; + + +abstract class RatingSpecPlSqlMixin { + @JacksonXmlProperty(isAttribute = true, localName = "office-id") + abstract String getOfficeId(); + + @JacksonXmlProperty(localName = "rating-spec-id") + abstract String getRatingId(); + + @JacksonXmlElementWrapper(localName = "ind-rounding-specs") + @JacksonXmlProperty(localName = "ind-rounding-spec") + abstract IndependentRoundingSpec[] getIndependentRoundingSpecs(); + + @JacksonXmlProperty(localName = "dep-rounding-spec") + abstract String getDependentRoundingSpec(); + + @JsonIgnore + abstract List getEffectiveDates(); +} + + +class RatingSpecXmlUtils { + private static final XmlMapper mapper = buildMapper(); + + private static XmlMapper buildMapper() { + XmlMapper m = XMLv2.buildXmlMapper(); + + // We don't want to globally mess with how RatingSpec is serialized, just when + // it comes thru this class. + m.addMixIn(RatingSpec.class, RatingSpecPlSqlMixin.class); + m.enable(SerializationFeature.INDENT_OUTPUT); + m.configure(ToXmlGenerator.Feature.WRITE_XML_DECLARATION, true); + return m; + } + + + /** + * The pl/sql create call is expecting a very particular format of xml. + * CDA publishes a RatingSpec object based on a DTO class. The CDA RatingSpec class does + * not quite match what the pl/sql wants. This method is meant to take a CDA RatingSpec + * as input and coax it into the format that the pl/sql wants. + * + * @param spec The CDA RatingSpec object. + * @return xml String in the format expected by the pl/sql create call. + */ + public static String toPlSqlXml(RatingSpec spec) { + try { + String xml = mapper.writer() + .withRootName("rating-spec") + .writeValueAsString(spec); + + String namespaces = " xmlns:xsi=\"http://www.w3.org/2001/XMLSchema-instance\" xsi:noNamespaceSchemaLocation=\"http://www.hec.usace.army.mil/xmlSchema/cwms/Ratings.xsd\""; + + // We want to wrap the rating-spec in a ratings element. + // xml currently starts with + // then + + int rootStart = xml.indexOf("" + body + ""; + } + + return xml; + } catch (Exception e) { + throw new RuntimeException(e); + } + } +} diff --git a/cwms-data-api/src/main/java/cwms/cda/formatters/xml/XMLv2.java b/cwms-data-api/src/main/java/cwms/cda/formatters/xml/XMLv2.java index a28dd01fa..fc70b6824 100644 --- a/cwms-data-api/src/main/java/cwms/cda/formatters/xml/XMLv2.java +++ b/cwms-data-api/src/main/java/cwms/cda/formatters/xml/XMLv2.java @@ -82,7 +82,7 @@ public T parseContent(InputStream content, Class type } } - private static @NotNull XmlMapper buildXmlMapper() { + public static @NotNull XmlMapper buildXmlMapper() { XmlMapper retval = new XmlMapper(); retval.findAndRegisterModules(); // Without these two disables an Instant gets written as 3333333.335000000 diff --git a/cwms-data-api/src/test/java/cwms/cda/api/rating/RatingSpecControllerTestIT.java b/cwms-data-api/src/test/java/cwms/cda/api/rating/RatingSpecControllerTestIT.java index 190b6bbb0..88ae21700 100644 --- a/cwms-data-api/src/test/java/cwms/cda/api/rating/RatingSpecControllerTestIT.java +++ b/cwms-data-api/src/test/java/cwms/cda/api/rating/RatingSpecControllerTestIT.java @@ -37,6 +37,7 @@ import cwms.cda.data.dao.JooqDao; import cwms.cda.data.dto.rating.RatingSpec; +import cwms.cda.formatters.ContentType; import cwms.cda.formatters.Formats; import javax.servlet.http.HttpServletResponse; @@ -235,4 +236,112 @@ void test_create_read_delete() throws Exception { .log().ifValidationFails(LogDetail.ALL,true) .statusCode(is(HttpServletResponse.SC_NOT_FOUND)); } + + @Test + void test_create_read_delete_json() throws Exception { + String locationId = "RatingSpecTestJson"; + String officeId = "SPK"; + createLocation(locationId, true, officeId); + String ratingXml = readResourceFile("cwms/cda/api/Zanesville_Stage_Flow_COE_Production.xml"); + RatingSpecContainer specContainer = RatingSpecXmlFactory.ratingSpecContainer(ratingXml); + specContainer.officeId = officeId; + specContainer.specOfficeId = officeId; + specContainer.locationId = locationId; + specContainer.specId = specContainer.specId.replace("Zanesville", locationId); + + String templateXml = RatingSpecXmlFactory.toXml(specContainer, "", 0); + TestAccounts.KeyUser user = TestAccounts.KeyUser.SPK_NORMAL; + + //Create Template (XML) + given() + .log().ifValidationFails(LogDetail.ALL,true) + .accept(Formats.JSONV2) + .contentType(Formats.XMLV2) + .body(templateXml) + .header("Authorization", user.toHeaderValue()) + .queryParam(OFFICE, officeId) + .when() + .redirects().follow(true) + .redirects().max(3) + .post("/ratings/template") + .then() + .assertThat() + .log().ifValidationFails(LogDetail.ALL,true) + .statusCode(is(HttpServletResponse.SC_CREATED)); + + //Create Spec (JSON) + RatingSpec ratingSpec = new RatingSpec.Builder() + .withOfficeId(officeId) + .withRatingId(specContainer.specId) + .withTemplateId(specContainer.templateId) + .withLocationId(locationId) + .withInRangeMethod(specContainer.inRangeMethod) + .withOutRangeLowMethod(specContainer.outRangeLowMethod) + .withOutRangeHighMethod(specContainer.outRangeHighMethod) + .withActive(specContainer.active) + .withAutoUpdate(specContainer.autoUpdate) + .withAutoActivate(specContainer.autoActivate) + .withDescription("JSON Test") + .withIndependentRoundingSpecs(RatingSpec.Builder.buildIndependentRoundingSpecs(specContainer.indRoundingSpecs)) + .withDependentRoundingSpec(specContainer.depRoundingSpec) + .withVersion("Production") // pl/sql error if version isn't specified. + .build(); + + ContentType contentType = Formats.parseHeader(Formats.JSONV2, RatingSpec.class); + String specJson = Formats.format(contentType, ratingSpec); + + given() + .log().ifValidationFails(LogDetail.ALL,true) + .accept(Formats.JSONV2) + .contentType(Formats.JSONV2) + .body(specJson) + .header("Authorization", user.toHeaderValue()) + .queryParam(OFFICE, officeId) + .when() + .redirects().follow(true) + .redirects().max(3) + .post("/ratings/spec") + .then() + .assertThat() + .log().ifValidationFails(LogDetail.ALL,true) + .statusCode(is(HttpServletResponse.SC_CREATED)); + + //Read + given() + .log().ifValidationFails(LogDetail.ALL,true) + .accept(Formats.JSONV2) + .contentType(Formats.JSONV2) + .queryParam("office", officeId) + .when() + .redirects().follow(true) + .redirects().max(3) + .get("/ratings/spec/" + specContainer.specId) + .then() + .assertThat() + .log().ifValidationFails(LogDetail.ALL,true) + .statusCode(is(HttpServletResponse.SC_OK)) + .body("rating-id", equalTo(specContainer.specId)) + .body("office-id", equalTo(specContainer.officeId)) + .body("template-id", equalTo(specContainer.templateId)) + .body("in-range-method", equalTo(specContainer.inRangeMethod)) + .body("out-range-low-method", equalTo(specContainer.outRangeLowMethod)) + .body("out-range-high-method", equalTo(specContainer.outRangeHighMethod)); + + //Delete + given() + .log().ifValidationFails(LogDetail.ALL,true) + .accept(Formats.JSONV2) + .contentType(Formats.JSONV2) + .header("Authorization", user.toHeaderValue()) + .queryParam(OFFICE, officeId) + .queryParam(METHOD, JooqDao.DeleteMethod.DELETE_ALL) + .when() + .redirects().follow(true) + .redirects().max(3) + .delete("/ratings/spec/" + specContainer.specId) + .then() + .assertThat() + .log().ifValidationFails(LogDetail.ALL,true) + .statusCode(is(HttpServletResponse.SC_NO_CONTENT)); + } } diff --git a/cwms-data-api/src/test/java/cwms/cda/data/dao/RatingSpecXmlUtilsTest.java b/cwms-data-api/src/test/java/cwms/cda/data/dao/RatingSpecXmlUtilsTest.java new file mode 100644 index 000000000..302dd2289 --- /dev/null +++ b/cwms-data-api/src/test/java/cwms/cda/data/dao/RatingSpecXmlUtilsTest.java @@ -0,0 +1,37 @@ +package cwms.cda.data.dao; + +import cwms.cda.data.dto.rating.RatingSpec; +import cwms.cda.data.dto.rating.RatingSpecTest; +import org.junit.jupiter.api.Test; + +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertTrue; + +public class RatingSpecXmlUtilsTest { + + @Test + void testToPlSqlXml() { + RatingSpec spec = RatingSpecTest.buildRatingSpec("SWT", "ARBU.Elev;Stor.Linear.Production"); + String xml = RatingSpecXmlUtils.toPlSqlXml(spec); + System.out.println("Debug:" + xml); + + assertTrue(xml.contains("version='1.0'") || xml.contains("version=\"1.0\"")); + assertTrue(xml.contains("encoding='UTF-8'") || xml.contains("encoding=\"UTF-8\"")); + assertTrue(xml.contains(""); + assertTrue(xml.contains(""), "office-id should be an attribute of "); + + assertTrue(xml.contains("ARBU.Elev;Stor.Linear.Production")); + assertTrue(xml.contains("")); + assertTrue(xml.contains("")); + + assertFalse(xml.contains("rating-id>")); + assertFalse(xml.contains("independent-rounding-spec")); + assertFalse(xml.contains("dependent-rounding-spec")); + } +} From 3b8aa6a6cc3197397c02006516ac8bad13030ef2 Mon Sep 17 00:00:00 2001 From: rma-rripken <89810919+rma-rripken@users.noreply.github.com> Date: Thu, 29 Jan 2026 19:18:41 -0800 Subject: [PATCH 07/14] Fixing issue where independent rounding specs were coming out twice in the pl/sql xml. --- .../java/cwms/cda/data/dto/rating/RatingSpec.java | 6 ++++-- .../cwms/cda/data/dao/RatingSpecXmlUtilsTest.java | 2 +- .../cwms/cda/data/dto/rating/RatingSpecTest.java | 13 +++++++------ 3 files changed, 12 insertions(+), 9 deletions(-) diff --git a/cwms-data-api/src/main/java/cwms/cda/data/dto/rating/RatingSpec.java b/cwms-data-api/src/main/java/cwms/cda/data/dto/rating/RatingSpec.java index 476abc92f..235095409 100644 --- a/cwms-data-api/src/main/java/cwms/cda/data/dto/rating/RatingSpec.java +++ b/cwms-data-api/src/main/java/cwms/cda/data/dto/rating/RatingSpec.java @@ -2,6 +2,7 @@ import com.fasterxml.jackson.annotation.JsonFormat; import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.annotation.JsonRootName; import com.fasterxml.jackson.databind.PropertyNamingStrategies; import com.fasterxml.jackson.databind.annotation.JsonDeserialize; import com.fasterxml.jackson.databind.annotation.JsonNaming; @@ -20,6 +21,7 @@ import java.util.List; import org.jetbrains.annotations.NotNull; +@JsonRootName("rating-spec") @JsonDeserialize(builder = RatingSpec.Builder.class) @JsonInclude(JsonInclude.Include.NON_NULL) @JsonNaming(PropertyNamingStrategies.KebabCaseStrategy.class) @@ -40,8 +42,6 @@ public class RatingSpec extends CwmsDTO { private final boolean autoActivate; private final boolean autoMigrateExtension; - @JacksonXmlElementWrapper(localName = "independent-rounding-specs") - @JacksonXmlProperty(localName = "independent-rounding-spec") private final IndependentRoundingSpec[] independentRoundingSpecs; private final String dependentRoundingSpec; private final String description; @@ -118,6 +118,8 @@ public boolean isAutoMigrateExtension() { return autoMigrateExtension; } + @JacksonXmlElementWrapper(localName = "independent-rounding-specs") + @JacksonXmlProperty(localName = "independent-rounding-spec") public IndependentRoundingSpec[] getIndependentRoundingSpecs() { return independentRoundingSpecs; } diff --git a/cwms-data-api/src/test/java/cwms/cda/data/dao/RatingSpecXmlUtilsTest.java b/cwms-data-api/src/test/java/cwms/cda/data/dao/RatingSpecXmlUtilsTest.java index 302dd2289..37f70e8f8 100644 --- a/cwms-data-api/src/test/java/cwms/cda/data/dao/RatingSpecXmlUtilsTest.java +++ b/cwms-data-api/src/test/java/cwms/cda/data/dao/RatingSpecXmlUtilsTest.java @@ -13,7 +13,7 @@ public class RatingSpecXmlUtilsTest { void testToPlSqlXml() { RatingSpec spec = RatingSpecTest.buildRatingSpec("SWT", "ARBU.Elev;Stor.Linear.Production"); String xml = RatingSpecXmlUtils.toPlSqlXml(spec); - System.out.println("Debug:" + xml); +// System.out.println("Debug:" + xml); assertTrue(xml.contains("version='1.0'") || xml.contains("version=\"1.0\"")); assertTrue(xml.contains("encoding='UTF-8'") || xml.contains("encoding=\"UTF-8\"")); diff --git a/cwms-data-api/src/test/java/cwms/cda/data/dto/rating/RatingSpecTest.java b/cwms-data-api/src/test/java/cwms/cda/data/dto/rating/RatingSpecTest.java index a8c4178f1..120e794e5 100644 --- a/cwms-data-api/src/test/java/cwms/cda/data/dto/rating/RatingSpecTest.java +++ b/cwms-data-api/src/test/java/cwms/cda/data/dto/rating/RatingSpecTest.java @@ -1,9 +1,7 @@ package cwms.cda.data.dto.rating; import static cwms.cda.data.dto.rating.RatingSpec.Builder.buildIndependentRoundingSpecs; -import static org.junit.jupiter.api.Assertions.assertEquals; -import static org.junit.jupiter.api.Assertions.assertNotNull; -import static org.junit.jupiter.api.Assertions.assertTrue; +import static org.junit.jupiter.api.Assertions.*; import com.fasterxml.jackson.core.JsonProcessingException; import com.fasterxml.jackson.databind.ObjectMapper; @@ -50,8 +48,8 @@ void testSerialize() throws JsonProcessingException { RatingSpec spec = buildRatingSpec(officeId, ratingId); ObjectMapper om = JsonV2.buildObjectMapper(); - String serializedLocation = om.writeValueAsString(spec); - assertNotNull(serializedLocation); + String serializedSpec = om.writeValueAsString(spec); + assertNotNull(serializedSpec); } @@ -82,8 +80,11 @@ void testRoundtripXML() { String xml = xmlv2.format(spec); assertNotNull(xml); - System.out.println(xml); + assertTrue(xml.contains("ARBU.Elev;Stor.Linear.Production")); + assertTrue(xml.contains("")); + assertFalse(xml.contains("")); + assertFalse(xml.contains("")); RatingSpec spec2 = xmlv2.parseContent(xml, RatingSpec.class); assertNotNull(spec2); From 3ee044faf082e4d7ae167ecce6b4c318ef599836 Mon Sep 17 00:00:00 2001 From: rma-rripken <89810919+rma-rripken@users.noreply.github.com> Date: Fri, 30 Jan 2026 10:27:16 -0800 Subject: [PATCH 08/14] IndependentRoundingSpec has an equals(), this comment wasn't correct. --- .../src/main/java/cwms/cda/data/dto/rating/RatingSpec.java | 1 - 1 file changed, 1 deletion(-) diff --git a/cwms-data-api/src/main/java/cwms/cda/data/dto/rating/RatingSpec.java b/cwms-data-api/src/main/java/cwms/cda/data/dto/rating/RatingSpec.java index 235095409..fbe192318 100644 --- a/cwms-data-api/src/main/java/cwms/cda/data/dto/rating/RatingSpec.java +++ b/cwms-data-api/src/main/java/cwms/cda/data/dto/rating/RatingSpec.java @@ -183,7 +183,6 @@ public boolean equals(Object o) { that.getSourceAgency()) : that.getSourceAgency() != null) { return false; } - // Probably incorrect - comparing Object[] arrays with Arrays.equals if (!Arrays.equals(getIndependentRoundingSpecs(), that.getIndependentRoundingSpecs())) { return false; } From 59f523b5f09d897d187e1ff96363186c2ec81177 Mon Sep 17 00:00:00 2001 From: rma-rripken <89810919+rma-rripken@users.noreply.github.com> Date: Fri, 30 Jan 2026 10:40:44 -0800 Subject: [PATCH 09/14] Added XML support to RatingSpecs and adding tests --- .../java/cwms/cda/data/dao/RatingSpecDao.java | 2 +- .../cwms/cda/data/dto/rating/RatingSpecs.java | 81 ++++++++++++++++--- .../cda/data/dto/rating/RatingSpecTest.java | 30 ++++--- .../cda/data/dto/rating/RatingSpecsTest.java | 67 +++++++++++++++ .../cda/data/dto/rating/rating_specs.json | 47 +++++++++++ .../cwms/cda/data/dto/rating/rating_specs.xml | 43 ++++++++++ 6 files changed, 243 insertions(+), 27 deletions(-) create mode 100644 cwms-data-api/src/test/java/cwms/cda/data/dto/rating/RatingSpecsTest.java create mode 100644 cwms-data-api/src/test/resources/cwms/cda/data/dto/rating/rating_specs.json create mode 100644 cwms-data-api/src/test/resources/cwms/cda/data/dto/rating/rating_specs.xml diff --git a/cwms-data-api/src/main/java/cwms/cda/data/dao/RatingSpecDao.java b/cwms-data-api/src/main/java/cwms/cda/data/dao/RatingSpecDao.java index 5c23885f0..4474a9fc7 100644 --- a/cwms-data-api/src/main/java/cwms/cda/data/dao/RatingSpecDao.java +++ b/cwms-data-api/src/main/java/cwms/cda/data/dao/RatingSpecDao.java @@ -197,7 +197,7 @@ public RatingSpecs retrieveRatingSpecs(String cursor, int pageSize, String offic Set retval = getRatingSpecs(office, specIdMask, offset, pageSize); RatingSpecs.Builder builder = new RatingSpecs.Builder(offset, pageSize, total); - builder.specs(new ArrayList<>(retval)); + builder.withSpecs(new ArrayList<>(retval)); return builder.build(); } diff --git a/cwms-data-api/src/main/java/cwms/cda/data/dto/rating/RatingSpecs.java b/cwms-data-api/src/main/java/cwms/cda/data/dto/rating/RatingSpecs.java index 5a568e0a4..ab1707652 100644 --- a/cwms-data-api/src/main/java/cwms/cda/data/dto/rating/RatingSpecs.java +++ b/cwms-data-api/src/main/java/cwms/cda/data/dto/rating/RatingSpecs.java @@ -4,10 +4,12 @@ import com.fasterxml.jackson.databind.PropertyNamingStrategies; import com.fasterxml.jackson.databind.annotation.JsonDeserialize; import com.fasterxml.jackson.databind.annotation.JsonNaming; +import com.fasterxml.jackson.databind.annotation.JsonPOJOBuilder; import cwms.cda.data.dto.CwmsDTOPaginated; import cwms.cda.formatters.Formats; import cwms.cda.formatters.annotations.FormattableWith; import cwms.cda.formatters.json.JsonV2; +import cwms.cda.formatters.xml.XMLv2; import java.util.ArrayList; import java.util.Collections; @@ -17,39 +19,98 @@ @JsonInclude(JsonInclude.Include.NON_NULL) @JsonNaming(PropertyNamingStrategies.KebabCaseStrategy.class) @FormattableWith(contentType = Formats.JSONV2, formatter = JsonV2.class, aliases = {Formats.DEFAULT, Formats.JSON}) +@FormattableWith(contentType = Formats.XMLV2, formatter = XMLv2.class) public class RatingSpecs extends CwmsDTOPaginated { private List specs; private RatingSpecs() { - } - private int offset; - private RatingSpecs(int offset, int pageSize, Integer total, List specsList) { super(Integer.toString(offset), pageSize, total); specs = new ArrayList<>(specsList); - this.offset = offset; } public List getSpecs() { return Collections.unmodifiableList(specs); } + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + + RatingSpecs that = (RatingSpecs) o; + + if (getPageSize() != that.getPageSize()) return false; + if (getSpecs() != null ? !getSpecs().equals(that.getSpecs()) : that.getSpecs() != null) return false; + if (getPage() != null ? !getPage().equals(that.getPage()) : that.getPage() != null) return false; + if (getNextPage() != null ? !getNextPage().equals(that.getNextPage()) : that.getNextPage() != null) + return false; + return getTotal() != null ? getTotal().equals(that.getTotal()) : that.getTotal() == null; + } + + @Override + public int hashCode() { + int result = getSpecs() != null ? getSpecs().hashCode() : 0; + result = 31 * result + (getPage() != null ? getPage().hashCode() : 0); + result = 31 * result + (getNextPage() != null ? getNextPage().hashCode() : 0); + result = 31 * result + (getTotal() != null ? getTotal().hashCode() : 0); + result = 31 * result + getPageSize(); + return result; + } + + @JsonPOJOBuilder + @JsonNaming(PropertyNamingStrategies.KebabCaseStrategy.class) public static class Builder { - private final int offset; - private final int pageSize; - private final Integer total; + private int offset; + private int pageSize; + private Integer total; private List specs; + public Builder() { + } + + + public Builder withPage(String page) { + String[] parts = decodeCursor(page); + if (parts.length > 0) { + try { + this.offset = Integer.parseInt(parts[0]); + } catch (NumberFormatException e) { + // Try different delimiter if default fails, though decodeCursor should handle it if base64 + } + } + return this; + } + + + public Builder withOffset(int offset) { + this.offset = offset; + return this; + } + + + public Builder withPageSize(int pageSize) { + this.pageSize = pageSize; + return this; + } + + + public Builder withTotal(Integer total) { + this.total = total; + return this; + } + public Builder(int offset, int pageSize, Integer total) { this.offset = offset; this.pageSize = pageSize; this.total = total; } - public Builder specs(List specList) { + + public Builder withSpecs(List specList) { this.specs = specList; return this; } @@ -57,8 +118,8 @@ public Builder specs(List specList) { public RatingSpecs build() { RatingSpecs retval = new RatingSpecs(offset, pageSize, total, specs); - if (this.specs.size() == this.pageSize) { - String cursor = Integer.toString(retval.offset + retval.specs.size()); + if (this.specs != null && this.specs.size() == this.pageSize) { + String cursor = Integer.toString(offset + retval.specs.size()); retval.nextPage = encodeCursor(cursor, retval.pageSize, retval.total); diff --git a/cwms-data-api/src/test/java/cwms/cda/data/dto/rating/RatingSpecTest.java b/cwms-data-api/src/test/java/cwms/cda/data/dto/rating/RatingSpecTest.java index 120e794e5..570c24402 100644 --- a/cwms-data-api/src/test/java/cwms/cda/data/dto/rating/RatingSpecTest.java +++ b/cwms-data-api/src/test/java/cwms/cda/data/dto/rating/RatingSpecTest.java @@ -5,10 +5,14 @@ import com.fasterxml.jackson.core.JsonProcessingException; import com.fasterxml.jackson.databind.ObjectMapper; +import cwms.cda.formatters.ContentType; +import cwms.cda.formatters.Formats; import cwms.cda.formatters.json.JsonV2; import cwms.cda.formatters.xml.XMLv2; import org.apache.commons.io.IOUtils; import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.CsvSource; import java.io.IOException; import java.io.InputStream; @@ -16,30 +20,24 @@ public class RatingSpecTest { - @Test - void testDeserializeJSON() throws IOException { - InputStream resource = getClass().getResourceAsStream("/cwms/cda/data/dto/rating/rating_spec.json"); + @ParameterizedTest + @CsvSource({ + "json, " + Formats.JSONV2, + "xml, " + Formats.XMLV2 + }) + void testDeserialize(String ext, String format) throws IOException { + InputStream resource = getClass().getResourceAsStream("/cwms/cda/data/dto/rating/rating_spec." + ext); assertNotNull(resource); String json = IOUtils.toString(resource, StandardCharsets.UTF_8); - ObjectMapper om = JsonV2.buildObjectMapper(); - RatingSpec spec = om.readValue(json, RatingSpec.class); + ContentType type = Formats.parseHeader(format, RatingSpec.class); + RatingSpec spec = Formats.parseContent(type, json, RatingSpec.class); assertNotNull(spec); - } - - @Test - void testDeserializeXml() throws IOException { - InputStream resource = getClass().getResourceAsStream("/cwms/cda/data/dto/rating/rating_spec.xml"); - assertNotNull(resource); - String xml = IOUtils.toString(resource, StandardCharsets.UTF_8); - - XMLv2 xmlv2 = new XMLv2(); - RatingSpec spec = xmlv2.parseContent(xml, RatingSpec.class); - assertNotNull(spec); } + @Test void testSerialize() throws JsonProcessingException { String officeId = "SWT"; diff --git a/cwms-data-api/src/test/java/cwms/cda/data/dto/rating/RatingSpecsTest.java b/cwms-data-api/src/test/java/cwms/cda/data/dto/rating/RatingSpecsTest.java new file mode 100644 index 000000000..cba0134c3 --- /dev/null +++ b/cwms-data-api/src/test/java/cwms/cda/data/dto/rating/RatingSpecsTest.java @@ -0,0 +1,67 @@ +package cwms.cda.data.dto.rating; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNotNull; + +import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.databind.ObjectMapper; +import cwms.cda.formatters.ContentType; +import cwms.cda.formatters.Formats; +import cwms.cda.formatters.json.JsonV2; +import cwms.cda.formatters.xml.XMLv2; +import java.io.IOException; +import java.io.InputStream; +import java.nio.charset.StandardCharsets; +import java.util.ArrayList; +import java.util.List; +import org.apache.commons.io.IOUtils; +import org.jooq.XML; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.CsvSource; +import org.junit.jupiter.params.provider.ValueSource; + +class RatingSpecsTest { + + @ParameterizedTest + @CsvSource({ + "json, " + Formats.JSONV2, + "xml, " + Formats.XMLV2 + }) + void testDeserialize(String ext, String format) throws IOException { + InputStream resource = getClass().getResourceAsStream("/cwms/cda/data/dto/rating/rating_specs." + ext); + assertNotNull(resource); + String json = IOUtils.toString(resource, StandardCharsets.UTF_8); + + ContentType type = Formats.parseHeader(format, RatingSpecs.class); + RatingSpecs specs = Formats.parseContent(type, json, RatingSpecs.class); + + assertNotNull(specs); + assertEquals(2, specs.getSpecs().size()); + } + + + @ParameterizedTest + @ValueSource(strings = {Formats.JSONV2, Formats.XMLV2, Formats.DEFAULT}) + void testRoundtrip(String format) { + RatingSpecs specs = buildRatingSpecs(); + + ContentType type = Formats.parseHeader(format, RatingSpecs.class); + String xml = Formats.format(type, specs); + assertNotNull(xml); + + RatingSpecs specs2 = Formats.parseContent(type, xml, RatingSpecs.class); + assertNotNull(specs2); + assertEquals(specs, specs2); + } + + private RatingSpecs buildRatingSpecs() { + List specList = new ArrayList<>(); + specList.add(RatingSpecTest.buildRatingSpec("SWT", "ARBU.Elev;Stor.Linear.Production")); + specList.add(RatingSpecTest.buildRatingSpec("SWT", "OBRK.Elev;Stor.Linear.Production")); + + RatingSpecs.Builder builder = new RatingSpecs.Builder(0, 10, 2); + builder.withSpecs(specList); + return builder.build(); + } +} diff --git a/cwms-data-api/src/test/resources/cwms/cda/data/dto/rating/rating_specs.json b/cwms-data-api/src/test/resources/cwms/cda/data/dto/rating/rating_specs.json new file mode 100644 index 000000000..9dc9f5a2c --- /dev/null +++ b/cwms-data-api/src/test/resources/cwms/cda/data/dto/rating/rating_specs.json @@ -0,0 +1,47 @@ +{ + "page": "MHx8MTB8fDI=", + "page-size": 10, + "total": 2, + "specs": [ + { + "office-id": "SWT", + "rating-id": "ARBU.Elev;Stor.Linear.Production", + "template-id": "Elev;Stor.Linear", + "location-id": "ARBU", + "version": "Production", + "in-range-method": "NEAREST", + "out-range-low-method": "LINEAR", + "out-range-high-method": "LOWER", + "active": true, + "auto-update": false, + "auto-activate": false, + "auto-migrate-extension": false, + "independent-rounding-specs": [ + { + "value": "2222233332" + } + ], + "dependent-rounding-spec": "2222233332" + }, + { + "office-id": "SWT", + "rating-id": "OBRK.Elev;Stor.Linear.Production", + "template-id": "Elev;Stor.Linear", + "location-id": "OBRK", + "version": "Production", + "in-range-method": "NEAREST", + "out-range-low-method": "LINEAR", + "out-range-high-method": "LOWER", + "active": true, + "auto-update": false, + "auto-activate": false, + "auto-migrate-extension": false, + "independent-rounding-specs": [ + { + "value": "2222233332" + } + ], + "dependent-rounding-spec": "2222233332" + } + ] +} diff --git a/cwms-data-api/src/test/resources/cwms/cda/data/dto/rating/rating_specs.xml b/cwms-data-api/src/test/resources/cwms/cda/data/dto/rating/rating_specs.xml new file mode 100644 index 000000000..1cffcf81b --- /dev/null +++ b/cwms-data-api/src/test/resources/cwms/cda/data/dto/rating/rating_specs.xml @@ -0,0 +1,43 @@ + + MCwwLDI= + 10 + 2 + + + SWT + ARBU.Elev;Stor.Linear.Production + Elev;Stor.Linear + ARBU + Production + NEAREST + LINEAR + LOWER + true + false + false + false + 2222233332 + + 2222233332 + + + + SWT + OBRK.Elev;Stor.Linear.Production + Elev;Stor.Linear + OBRK + Production + NEAREST + LINEAR + LOWER + true + false + false + false + 2222233332 + + 2222233332 + + + + From 6ed78be395a60f9423c20615f7a21ed0ebe3b915 Mon Sep 17 00:00:00 2001 From: rma-rripken <89810919+rma-rripken@users.noreply.github.com> Date: Fri, 30 Jan 2026 10:50:48 -0800 Subject: [PATCH 10/14] This didn't need to be CsvSource, ValueSource is sufficient --- .../java/cwms/cda/data/dto/location/kind/EmbankmentTest.java | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/cwms-data-api/src/test/java/cwms/cda/data/dto/location/kind/EmbankmentTest.java b/cwms-data-api/src/test/java/cwms/cda/data/dto/location/kind/EmbankmentTest.java index bf17e5cb4..cf34bed47 100644 --- a/cwms-data-api/src/test/java/cwms/cda/data/dto/location/kind/EmbankmentTest.java +++ b/cwms-data-api/src/test/java/cwms/cda/data/dto/location/kind/EmbankmentTest.java @@ -41,12 +41,12 @@ import org.apache.commons.io.IOUtils; import org.junit.jupiter.api.Test; import org.junit.jupiter.params.ParameterizedTest; -import org.junit.jupiter.params.provider.CsvSource; +import org.junit.jupiter.params.provider.ValueSource; final class EmbankmentTest { @ParameterizedTest - @CsvSource({Formats.JSON, Formats.JSONV1, Formats.DEFAULT}) + @ValueSource(strings = {Formats.JSON, Formats.JSONV1, Formats.DEFAULT}) void testTurbineSerializationRoundTrip(String format) { Embankment embankment = buildTestEmbankment(); String serialized = Formats.format(Formats.parseHeader(format, Embankment.class), embankment); From 97e7da865ce41feb12793f08dc85223e5070c2c2 Mon Sep 17 00:00:00 2001 From: rma-rripken <89810919+rma-rripken@users.noreply.github.com> Date: Fri, 30 Jan 2026 11:47:47 -0800 Subject: [PATCH 11/14] Added some verification that the xml is serializaing in the expected structure. --- .../cwms/cda/data/dto/rating/RatingSpecs.java | 11 +++--- .../cda/data/dto/rating/RatingSpecsTest.java | 36 ++++++++++++++++--- 2 files changed, 37 insertions(+), 10 deletions(-) diff --git a/cwms-data-api/src/main/java/cwms/cda/data/dto/rating/RatingSpecs.java b/cwms-data-api/src/main/java/cwms/cda/data/dto/rating/RatingSpecs.java index ab1707652..1e5e27b40 100644 --- a/cwms-data-api/src/main/java/cwms/cda/data/dto/rating/RatingSpecs.java +++ b/cwms-data-api/src/main/java/cwms/cda/data/dto/rating/RatingSpecs.java @@ -1,10 +1,13 @@ package cwms.cda.data.dto.rating; import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.annotation.JsonRootName; import com.fasterxml.jackson.databind.PropertyNamingStrategies; import com.fasterxml.jackson.databind.annotation.JsonDeserialize; import com.fasterxml.jackson.databind.annotation.JsonNaming; import com.fasterxml.jackson.databind.annotation.JsonPOJOBuilder; +import com.fasterxml.jackson.dataformat.xml.annotation.JacksonXmlElementWrapper; +import com.fasterxml.jackson.dataformat.xml.annotation.JacksonXmlProperty; import cwms.cda.data.dto.CwmsDTOPaginated; import cwms.cda.formatters.Formats; import cwms.cda.formatters.annotations.FormattableWith; @@ -15,6 +18,7 @@ import java.util.Collections; import java.util.List; +@JsonRootName("rating-specs") @JsonDeserialize(builder = RatingSpecs.Builder.class) @JsonInclude(JsonInclude.Include.NON_NULL) @JsonNaming(PropertyNamingStrategies.KebabCaseStrategy.class) @@ -32,6 +36,8 @@ private RatingSpecs(int offset, int pageSize, Integer total, List sp specs = new ArrayList<>(specsList); } + @JacksonXmlElementWrapper(localName = "specs") + @JacksonXmlProperty(localName = "rating-spec") public List getSpecs() { return Collections.unmodifiableList(specs); } @@ -72,7 +78,6 @@ public static class Builder { public Builder() { } - public Builder withPage(String page) { String[] parts = decodeCursor(page); if (parts.length > 0) { @@ -85,19 +90,16 @@ public Builder withPage(String page) { return this; } - public Builder withOffset(int offset) { this.offset = offset; return this; } - public Builder withPageSize(int pageSize) { this.pageSize = pageSize; return this; } - public Builder withTotal(Integer total) { this.total = total; return this; @@ -109,7 +111,6 @@ public Builder(int offset, int pageSize, Integer total) { this.total = total; } - public Builder withSpecs(List specList) { this.specs = specList; return this; diff --git a/cwms-data-api/src/test/java/cwms/cda/data/dto/rating/RatingSpecsTest.java b/cwms-data-api/src/test/java/cwms/cda/data/dto/rating/RatingSpecsTest.java index cba0134c3..51778d4f9 100644 --- a/cwms-data-api/src/test/java/cwms/cda/data/dto/rating/RatingSpecsTest.java +++ b/cwms-data-api/src/test/java/cwms/cda/data/dto/rating/RatingSpecsTest.java @@ -1,25 +1,30 @@ package cwms.cda.data.dto.rating; +import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.Matchers.hasXPath; +import static org.hamcrest.Matchers.is; import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertNotNull; -import com.fasterxml.jackson.core.JsonProcessingException; -import com.fasterxml.jackson.databind.ObjectMapper; import cwms.cda.formatters.ContentType; import cwms.cda.formatters.Formats; -import cwms.cda.formatters.json.JsonV2; -import cwms.cda.formatters.xml.XMLv2; import java.io.IOException; import java.io.InputStream; +import java.io.StringReader; import java.nio.charset.StandardCharsets; import java.util.ArrayList; import java.util.List; +import javax.xml.parsers.DocumentBuilder; +import javax.xml.parsers.DocumentBuilderFactory; +import javax.xml.parsers.ParserConfigurationException; import org.apache.commons.io.IOUtils; -import org.jooq.XML; import org.junit.jupiter.api.Test; import org.junit.jupiter.params.ParameterizedTest; import org.junit.jupiter.params.provider.CsvSource; import org.junit.jupiter.params.provider.ValueSource; +import org.w3c.dom.Document; +import org.xml.sax.InputSource; +import org.xml.sax.SAXException; class RatingSpecsTest { @@ -39,6 +44,27 @@ void testDeserialize(String ext, String format) throws IOException { assertNotNull(specs); assertEquals(2, specs.getSpecs().size()); } + + @Test + void testXmlSerialize() throws ParserConfigurationException, SAXException, IOException { + RatingSpecs specs = buildRatingSpecs(); + String xml = Formats.format(Formats.parseHeader(Formats.XMLV2, RatingSpecs.class), specs); + assertNotNull(xml); + System.out.println("[DEBUG_LOG] xml: " + xml); + + DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance(); + DocumentBuilder builder = factory.newDocumentBuilder(); + Document doc = builder.parse(new InputSource(new StringReader(xml))); + + assertThat(doc, hasXPath("/rating-specs/page-size", is("10"))); + assertThat(doc, hasXPath("/rating-specs/total", is("2"))); + + assertThat(doc, hasXPath("/rating-specs/specs/rating-spec[1]/office-id", is("SWT"))); + assertThat(doc, hasXPath("/rating-specs/specs/rating-spec[1]/rating-id", is("ARBU.Elev;Stor.Linear.Production"))); + + assertThat(doc, hasXPath("/rating-specs/specs/rating-spec[2]/office-id", is("SWT"))); + assertThat(doc, hasXPath("/rating-specs/specs/rating-spec[2]/rating-id", is("OBRK.Elev;Stor.Linear.Production"))); + } @ParameterizedTest From 7ebd54165f841e4a25667375bc109a6b3bbd282d Mon Sep 17 00:00:00 2001 From: rma-rripken <89810919+rma-rripken@users.noreply.github.com> Date: Fri, 30 Jan 2026 11:58:02 -0800 Subject: [PATCH 12/14] Added XML to getOne and getAll --- .../cda/api/rating/RatingSpecController.java | 51 ++---- .../rating/RatingSpecControllerTestIT.java | 171 ++++++++++++++++++ 2 files changed, 190 insertions(+), 32 deletions(-) diff --git a/cwms-data-api/src/main/java/cwms/cda/api/rating/RatingSpecController.java b/cwms-data-api/src/main/java/cwms/cda/api/rating/RatingSpecController.java index d91cd9747..965a84b86 100644 --- a/cwms-data-api/src/main/java/cwms/cda/api/rating/RatingSpecController.java +++ b/cwms-data-api/src/main/java/cwms/cda/api/rating/RatingSpecController.java @@ -30,8 +30,6 @@ import com.codahale.metrics.Histogram; import com.codahale.metrics.MetricRegistry; import com.codahale.metrics.Timer; -import com.fasterxml.jackson.core.JacksonException; -import com.fasterxml.jackson.databind.ObjectMapper; import cwms.cda.api.Controllers; import cwms.cda.api.errors.CdaError; import cwms.cda.data.dao.JooqDao; @@ -40,8 +38,7 @@ import cwms.cda.data.dto.rating.RatingSpecs; import cwms.cda.formatters.ContentType; import cwms.cda.formatters.Formats; -import cwms.cda.formatters.json.JsonV2; -import cwms.cda.formatters.xml.XMLv2; +import cwms.cda.formatters.FormattingException; import io.javalin.apibuilder.CrudHandler; import io.javalin.core.util.Header; import io.javalin.http.Context; @@ -111,7 +108,8 @@ private Timer.Context markAndTime(String subject) { responses = { @OpenApiResponse(status = STATUS_200, content = { - @OpenApiContent(type = Formats.JSONV2, from = RatingSpecs.class) + @OpenApiContent(type = Formats.JSONV2, from = RatingSpecs.class), + @OpenApiContent(type = Formats.XMLV2, from = RatingSpecs.class) } )}, tags = {TAG} @@ -164,6 +162,7 @@ public void getAll(Context ctx) { @OpenApiResponse(status = STATUS_200, content = { @OpenApiContent(from = RatingSpec.class, type = Formats.JSONV2), + @OpenApiContent(from = RatingSpec.class, type = Formats.XMLV2) } ) }, @@ -225,40 +224,28 @@ public void create(Context ctx) { try (final Timer.Context ignored = markAndTime(CREATE)) { DSLContext dsl = getDslContext(ctx); - String reqContentType = ctx.req.getContentType(); - String formatHeader = reqContentType != null ? reqContentType : Formats.XMLV2; - boolean failIfExists = ctx.queryParamAsClass(FAIL_IF_EXISTS, Boolean.class).getOrDefault(false); + String contentTypeHeader = ctx.req.getContentType(); String body = ctx.body(); + ContentType contentType = Formats.parseHeader(contentTypeHeader, RatingSpec.class); + + boolean failIfExists = ctx.queryParamAsClass(FAIL_IF_EXISTS, Boolean.class).getOrDefault(false); RatingSpecDao dao = new RatingSpecDao(dsl); - // First we are going to try and build a CDA RatingSpec from whatever the user gave us. - RatingSpec spec = null; try { - if (formatHeader.contains(Formats.XMLV2)) { - XMLv2 xmlv2 = new XMLv2(); - spec = xmlv2.parseContent(body, RatingSpec.class); - } else if (formatHeader.contains(Formats.JSONV2)) { - ObjectMapper jsonMapper = JsonV2.buildObjectMapper(); - spec = jsonMapper.readValue(body, RatingSpec.class); - } - } catch (JacksonException e) { - logger.atInfo().withCause(e).log("Unable to parse Rating Spec from request body"); - } - - if (spec != null) { - // If we were able to parse the RatingSpec from the request body, then we - // should call the dao method that takes a CDA object and the dao will sort it out. + RatingSpec spec = Formats.parseContent(contentType, body, RatingSpec.class); + // If we can parse it into our CDA RatingSpec object have the DAO use it. dao.create(spec, failIfExists); ctx.status(HttpServletResponse.SC_CREATED); - } else if (formatHeader.contains(Formats.XMLV2)) { - // This branch is if the user said its xml and it doesn't parse into the CDA RatingSpec - // object. We'll let the dao try passing it thru to the pl/sql. - dao.create(body, failIfExists); - ctx.status(HttpServletResponse.SC_CREATED); - } else { - throw new IllegalArgumentException("Could not parse body with format:" + formatHeader); + } catch (FormattingException e) { + if (contentType.getType().contains(Formats.XML)) { + // The user said its xml but it doesn't parse into our CDA RatingSpec object. + // We'll let the dao try doing a string pass-thru to the pl/sql. + dao.create(body, failIfExists); + ctx.status(HttpServletResponse.SC_CREATED); + return; + } + throw e; } - } } diff --git a/cwms-data-api/src/test/java/cwms/cda/api/rating/RatingSpecControllerTestIT.java b/cwms-data-api/src/test/java/cwms/cda/api/rating/RatingSpecControllerTestIT.java index 88ae21700..10267792e 100644 --- a/cwms-data-api/src/test/java/cwms/cda/api/rating/RatingSpecControllerTestIT.java +++ b/cwms-data-api/src/test/java/cwms/cda/api/rating/RatingSpecControllerTestIT.java @@ -344,4 +344,175 @@ void test_create_read_delete_json() throws Exception { .log().ifValidationFails(LogDetail.ALL,true) .statusCode(is(HttpServletResponse.SC_NO_CONTENT)); } + + @Test + void test_getOne_xml() throws Exception { + String locationId = "RatingSpecTestXml"; + String officeId = "SPK"; + createLocation(locationId, true, officeId); + String ratingXml = readResourceFile("cwms/cda/api/Zanesville_Stage_Flow_COE_Production.xml"); + RatingSpecContainer specContainer = RatingSpecXmlFactory.ratingSpecContainer(ratingXml); + specContainer.officeId = officeId; + specContainer.specOfficeId = officeId; + specContainer.locationId = locationId; + specContainer.specId = specContainer.specId.replace("Zanesville", locationId); + String specXml = RatingSpecXmlFactory.toXml(specContainer, "", 0, true); + String templateXml = RatingSpecXmlFactory.toXml(specContainer, "", 0); + TestAccounts.KeyUser user = TestAccounts.KeyUser.SPK_NORMAL; + + // Create Template + given() + .log().ifValidationFails(LogDetail.ALL, true) + .accept(Formats.JSONV2) + .contentType(Formats.XMLV2) + .body(templateXml) + .header("Authorization", user.toHeaderValue()) + .queryParam(OFFICE, officeId) + .when() + .redirects().follow(true) + .redirects().max(3) + .post("/ratings/template") + .then() + .assertThat() + .log().ifValidationFails(LogDetail.ALL, true) + .statusCode(is(HttpServletResponse.SC_CREATED)); + + // Create Spec + given() + .log().ifValidationFails(LogDetail.ALL, true) + .accept(Formats.JSONV2) + .contentType(Formats.XMLV2) + .body(specXml) + .header("Authorization", user.toHeaderValue()) + .queryParam(OFFICE, officeId) + .when() + .redirects().follow(true) + .redirects().max(3) + .post("/ratings/spec") + .then() + .assertThat() + .log().ifValidationFails(LogDetail.ALL, true) + .statusCode(is(HttpServletResponse.SC_CREATED)); + + // Read XML + given() + .log().ifValidationFails(LogDetail.ALL, true) + .accept(Formats.XMLV2) + .queryParam(OFFICE, officeId) + .when() + .redirects().follow(true) + .redirects().max(3) + .get("/ratings/spec/" + specContainer.specId) + .then() + .assertThat() + .log().ifValidationFails(LogDetail.ALL, true) + .statusCode(is(HttpServletResponse.SC_OK)) + .contentType(Formats.XMLV2) + .body("rating-spec.rating-id", equalTo(specContainer.specId)) + .body("rating-spec.office-id", equalTo(specContainer.officeId)) + .body("rating-spec.template-id", equalTo(specContainer.templateId)); + + // Delete + given() + .log().ifValidationFails(LogDetail.ALL, true) + .accept(Formats.JSONV2) + .contentType(Formats.JSONV2) + .header("Authorization", user.toHeaderValue()) + .queryParam(OFFICE, officeId) + .queryParam(METHOD, JooqDao.DeleteMethod.DELETE_ALL) + .when() + .redirects().follow(true) + .redirects().max(3) + .delete("/ratings/spec/" + specContainer.specId) + .then() + .assertThat() + .log().ifValidationFails(LogDetail.ALL, true) + .statusCode(is(HttpServletResponse.SC_NO_CONTENT)); + } + + @Test + void test_getAll_xml() throws Exception { + String locationId = "RatingSpecGetAllXml"; + String officeId = "SPK"; + createLocation(locationId, true, officeId); + String ratingXml = readResourceFile("cwms/cda/api/Zanesville_Stage_Flow_COE_Production.xml"); + RatingSpecContainer specContainer = RatingSpecXmlFactory.ratingSpecContainer(ratingXml); + specContainer.officeId = officeId; + specContainer.specOfficeId = officeId; + specContainer.locationId = locationId; + specContainer.specId = specContainer.specId.replace("Zanesville", locationId); + String specXml = RatingSpecXmlFactory.toXml(specContainer, "", 0, true); + String templateXml = RatingSpecXmlFactory.toXml(specContainer, "", 0); + TestAccounts.KeyUser user = TestAccounts.KeyUser.SPK_NORMAL; + + // Create Template + given() + .log().ifValidationFails(LogDetail.ALL, true) + .accept(Formats.JSONV2) + .contentType(Formats.XMLV2) + .body(templateXml) + .header("Authorization", user.toHeaderValue()) + .queryParam(OFFICE, officeId) + .when() + .redirects().follow(true) + .redirects().max(3) + .post("/ratings/template") + .then() + .assertThat() + .log().ifValidationFails(LogDetail.ALL, true) + .statusCode(is(HttpServletResponse.SC_CREATED)); + + // Create Spec + given() + .log().ifValidationFails(LogDetail.ALL, true) + .accept(Formats.JSONV2) + .contentType(Formats.XMLV2) + .body(specXml) + .header("Authorization", user.toHeaderValue()) + .queryParam(OFFICE, officeId) + .when() + .redirects().follow(true) + .redirects().max(3) + .post("/ratings/spec") + .then() + .assertThat() + .log().ifValidationFails(LogDetail.ALL, true) + .statusCode(is(HttpServletResponse.SC_CREATED)); + + // Read XML via getAll + given() + .log().ifValidationFails(LogDetail.ALL, true) + .accept(Formats.XMLV2) + .queryParam(OFFICE, officeId) + .queryParam("rating-id-mask", specContainer.specId) + .when() + .redirects().follow(true) + .redirects().max(3) + .get("/ratings/spec") + .then() + .assertThat() + .log().ifValidationFails(LogDetail.ALL, true) + .statusCode(is(HttpServletResponse.SC_OK)) + .contentType(Formats.XMLV2) + .body("rating-specs.specs.rating-spec.rating-id", equalTo(specContainer.specId)) + .body("rating-specs.specs.rating-spec.office-id", equalTo(specContainer.officeId)) + .body("rating-specs.specs.rating-spec.template-id", equalTo(specContainer.templateId)); + + // Delete + given() + .log().ifValidationFails(LogDetail.ALL, true) + .accept(Formats.JSONV2) + .contentType(Formats.JSONV2) + .header("Authorization", user.toHeaderValue()) + .queryParam(OFFICE, officeId) + .queryParam(METHOD, JooqDao.DeleteMethod.DELETE_ALL) + .when() + .redirects().follow(true) + .redirects().max(3) + .delete("/ratings/spec/" + specContainer.specId) + .then() + .assertThat() + .log().ifValidationFails(LogDetail.ALL, true) + .statusCode(is(HttpServletResponse.SC_NO_CONTENT)); + } } From c033071336f31f5b91c6758d6b9e459e350cf2fa Mon Sep 17 00:00:00 2001 From: rma-rripken <89810919+rma-rripken@users.noreply.github.com> Date: Tue, 3 Feb 2026 10:19:12 -0800 Subject: [PATCH 13/14] Use a wrapper class instead of manual xml manipulation. --- .../java/cwms/cda/data/dao/RatingSpecDao.java | 16 +++++- .../cwms/cda/data/dao/RatingSpecXmlUtils.java | 50 ++++++++++--------- 2 files changed, 41 insertions(+), 25 deletions(-) diff --git a/cwms-data-api/src/main/java/cwms/cda/data/dao/RatingSpecDao.java b/cwms-data-api/src/main/java/cwms/cda/data/dao/RatingSpecDao.java index 4474a9fc7..1524daf22 100644 --- a/cwms-data-api/src/main/java/cwms/cda/data/dao/RatingSpecDao.java +++ b/cwms-data-api/src/main/java/cwms/cda/data/dao/RatingSpecDao.java @@ -29,6 +29,7 @@ import static cwms.cda.data.dto.rating.RatingSpec.Builder.buildIndependentRoundingSpecs; import static java.util.stream.Collectors.toList; +import com.fasterxml.jackson.core.JsonProcessingException; import cwms.cda.data.dto.CwmsDTOPaginated; import cwms.cda.data.dto.rating.RatingEffectiveDatesMap; import cwms.cda.data.dto.rating.RatingSpec; @@ -67,6 +68,7 @@ import javax.sql.rowset.CachedRowSet; import javax.sql.rowset.RowSetProvider; +import cwms.cda.formatters.FormattingException; import org.jetbrains.annotations.NotNull; import org.jooq.Condition; import org.jooq.DSLContext; @@ -416,8 +418,18 @@ public void delete(String office, DeleteMethod deleteMethod, String ratingSpecId } public void create(RatingSpec spec, boolean failIfExists) { - String xml = RatingSpecXmlUtils.toPlSqlXml(spec); - create(xml, failIfExists); + String xml = null; + try { + xml = RatingSpecXmlUtils.toPlSqlXml(spec); + create(xml, failIfExists); + } catch (JsonProcessingException ex) { + String msg = spec != null ? + "Error rendering '" + spec + "' to XML" + : + "Null element passed to formatter"; + logger.atWarning().withCause(ex).log(msg); + throw new FormattingException(msg, ex); + } } // In my tests this method wouldn't fail if the input was diff --git a/cwms-data-api/src/main/java/cwms/cda/data/dao/RatingSpecXmlUtils.java b/cwms-data-api/src/main/java/cwms/cda/data/dao/RatingSpecXmlUtils.java index 058d00743..af2a8514f 100644 --- a/cwms-data-api/src/main/java/cwms/cda/data/dao/RatingSpecXmlUtils.java +++ b/cwms-data-api/src/main/java/cwms/cda/data/dao/RatingSpecXmlUtils.java @@ -1,11 +1,14 @@ package cwms.cda.data.dao; import com.fasterxml.jackson.annotation.JsonIgnore; +import com.fasterxml.jackson.core.JsonProcessingException; import com.fasterxml.jackson.dataformat.xml.annotation.JacksonXmlElementWrapper; import com.fasterxml.jackson.dataformat.xml.annotation.JacksonXmlProperty; +import com.fasterxml.jackson.dataformat.xml.annotation.JacksonXmlRootElement; import com.fasterxml.jackson.databind.SerializationFeature; import com.fasterxml.jackson.dataformat.xml.XmlMapper; import com.fasterxml.jackson.dataformat.xml.ser.ToXmlGenerator; +import com.google.common.flogger.FluentLogger; import cwms.cda.data.dto.rating.IndependentRoundingSpec; import cwms.cda.data.dto.rating.RatingSpec; import cwms.cda.formatters.xml.XMLv2; @@ -33,7 +36,29 @@ abstract class RatingSpecPlSqlMixin { } +@JacksonXmlRootElement(localName = "ratings") +class RatingSpecWrapper { + @JacksonXmlProperty(isAttribute = true, localName = "xmlns:xsi") + final String xsi = "http://www.w3.org/2001/XMLSchema-instance"; + + @JacksonXmlProperty(isAttribute = true, localName = "xsi:noNamespaceSchemaLocation") + final String schemaLocation = "http://www.hec.usace.army.mil/xmlSchema/cwms/Ratings.xsd"; + + @JacksonXmlProperty(localName = "rating-spec") + private final RatingSpec ratingSpec; + + public RatingSpecWrapper(RatingSpec ratingSpec) { + this.ratingSpec = ratingSpec; + } + + public RatingSpec getRatingSpec() { + return ratingSpec; + } +} + + class RatingSpecXmlUtils { + private static final FluentLogger logger = FluentLogger.forEnclosingClass(); private static final XmlMapper mapper = buildMapper(); private static XmlMapper buildMapper() { @@ -57,28 +82,7 @@ private static XmlMapper buildMapper() { * @param spec The CDA RatingSpec object. * @return xml String in the format expected by the pl/sql create call. */ - public static String toPlSqlXml(RatingSpec spec) { - try { - String xml = mapper.writer() - .withRootName("rating-spec") - .writeValueAsString(spec); - - String namespaces = " xmlns:xsi=\"http://www.w3.org/2001/XMLSchema-instance\" xsi:noNamespaceSchemaLocation=\"http://www.hec.usace.army.mil/xmlSchema/cwms/Ratings.xsd\""; - - // We want to wrap the rating-spec in a ratings element. - // xml currently starts with - // then - - int rootStart = xml.indexOf("" + body + ""; - } - - return xml; - } catch (Exception e) { - throw new RuntimeException(e); - } + public static String toPlSqlXml(RatingSpec spec) throws JsonProcessingException { + return mapper.writeValueAsString(new RatingSpecWrapper(spec)); } } From 4eb4401c47c1090d6b1c5333fc7250cc2839ed64 Mon Sep 17 00:00:00 2001 From: rma-rripken <89810919+rma-rripken@users.noreply.github.com> Date: Tue, 3 Feb 2026 10:25:41 -0800 Subject: [PATCH 14/14] Use a wrapper class instead of manual xml manipulation. --- .../test/java/cwms/cda/data/dao/RatingSpecXmlUtilsTest.java | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/cwms-data-api/src/test/java/cwms/cda/data/dao/RatingSpecXmlUtilsTest.java b/cwms-data-api/src/test/java/cwms/cda/data/dao/RatingSpecXmlUtilsTest.java index 37f70e8f8..29bc7c5aa 100644 --- a/cwms-data-api/src/test/java/cwms/cda/data/dao/RatingSpecXmlUtilsTest.java +++ b/cwms-data-api/src/test/java/cwms/cda/data/dao/RatingSpecXmlUtilsTest.java @@ -1,5 +1,6 @@ package cwms.cda.data.dao; +import com.fasterxml.jackson.core.JsonProcessingException; import cwms.cda.data.dto.rating.RatingSpec; import cwms.cda.data.dto.rating.RatingSpecTest; import org.junit.jupiter.api.Test; @@ -10,7 +11,7 @@ public class RatingSpecXmlUtilsTest { @Test - void testToPlSqlXml() { + void testToPlSqlXml() throws JsonProcessingException { RatingSpec spec = RatingSpecTest.buildRatingSpec("SWT", "ARBU.Elev;Stor.Linear.Production"); String xml = RatingSpecXmlUtils.toPlSqlXml(spec); // System.out.println("Debug:" + xml);