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..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 @@ -33,12 +33,12 @@ 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.FormattingException; import io.javalin.apibuilder.CrudHandler; import io.javalin.core.util.Header; import io.javalin.http.Context; @@ -48,11 +48,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,13 +102,14 @@ 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 = { @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} @@ -122,7 +125,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); @@ -159,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) } ) }, @@ -171,7 +175,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,58 +205,50 @@ 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; + String contentTypeHeader = ctx.req.getContentType(); 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); - } - } + ContentType contentType = Formats.parseHeader(contentTypeHeader, RatingSpec.class); - private static String translateToXml(String body, String contentType) { - String retval; + boolean failIfExists = ctx.queryParamAsClass(FAIL_IF_EXISTS, Boolean.class).getOrDefault(false); + RatingSpecDao dao = new RatingSpecDao(dsl); - if (contentType.contains(Formats.XMLV2)) { - retval = body; - } else if (contentType.contains(Formats.JSONV2)) { - retval = translateJsonToXml(body); - } else { - throw new IllegalArgumentException("Unexpected contentType format:" + contentType); + try { + 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); + } 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; + } } - - return retval; } - 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 @@ -261,22 +257,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..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,11 +29,13 @@ 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; 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 +60,15 @@ 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 cwms.cda.formatters.FormattingException; import org.jetbrains.annotations.NotNull; import org.jooq.Condition; import org.jooq.DSLContext; @@ -193,7 +199,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(); } @@ -388,7 +394,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 +406,41 @@ 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 = 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 + // 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 +455,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 +486,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 +518,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 +549,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..af2a8514f --- /dev/null +++ b/cwms-data-api/src/main/java/cwms/cda/data/dao/RatingSpecXmlUtils.java @@ -0,0 +1,88 @@ +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; + +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(); +} + + +@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() { + 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) throws JsonProcessingException { + return mapper.writeValueAsString(new RatingSpecWrapper(spec)); + } +} 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/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..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 @@ -2,15 +2,18 @@ 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; 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; @@ -18,10 +21,12 @@ 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) @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 +41,7 @@ public class RatingSpec extends CwmsDTO { private final boolean autoUpdate; private final boolean autoActivate; private final boolean autoMigrateExtension; + private final IndependentRoundingSpec[] independentRoundingSpecs; private final String dependentRoundingSpec; private final String description; @@ -112,6 +118,8 @@ public boolean isAutoMigrateExtension() { return autoMigrateExtension; } + @JacksonXmlElementWrapper(localName = "independent-rounding-specs") + @JacksonXmlProperty(localName = "independent-rounding-spec") public IndependentRoundingSpec[] getIndependentRoundingSpecs() { return independentRoundingSpecs; } @@ -175,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; } @@ -319,6 +326,7 @@ public Builder withIndependentRoundingSpecs(IndependentRoundingSpec[] indRoundin return this; } + public static IndependentRoundingSpec[] buildIndependentRoundingSpecs( String indRoundingSpecsStr) { IndependentRoundingSpec[] retval = null; @@ -330,7 +338,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/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..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,55 +1,117 @@ 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; import cwms.cda.formatters.json.JsonV2; +import cwms.cda.formatters.xml.XMLv2; import java.util.ArrayList; 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) @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; } + @JacksonXmlElementWrapper(localName = "specs") + @JacksonXmlProperty(localName = "rating-spec") 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 +119,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/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..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 @@ -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,283 @@ 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)); + } + + @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)); + } } 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..29bc7c5aa --- /dev/null +++ b/cwms-data-api/src/test/java/cwms/cda/data/dao/RatingSpecXmlUtilsTest.java @@ -0,0 +1,38 @@ +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; + +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertTrue; + +public class RatingSpecXmlUtilsTest { + + @Test + void testToPlSqlXml() throws JsonProcessingException { + 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")); + } +} 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); 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..50866dd06 --- /dev/null +++ b/cwms-data-api/src/test/java/cwms/cda/data/dto/rating/IndependentRoundingSpecTest.java @@ -0,0 +1,92 @@ +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); + } + + @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/java/cwms/cda/data/dto/rating/RatingSpecTest.java b/cwms-data-api/src/test/java/cwms/cda/data/dto/rating/RatingSpecTest.java index cf9a50b96..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 @@ -1,101 +1,144 @@ 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.*; 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; +import java.nio.charset.StandardCharsets; + +public class RatingSpecTest { + + @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); + + ContentType type = Formats.parseHeader(format, RatingSpec.class); + RatingSpec spec = Formats.parseContent(type, json, RatingSpec.class); + + assertNotNull(spec); + + } + + + @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 serializedSpec = om.writeValueAsString(spec); + assertNotNull(serializedSpec); + + } -public class RatingSpecTest -{ + @Test + void testRoundtripJSON() throws JsonProcessingException { + String officeId = "SWT"; + String ratingId = "ARBU.Elev;Stor.Linear.Production"; - @Test - void testSerialize() 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 serializedSpec = om.writeValueAsString(spec); + assertNotNull(serializedSpec); - ObjectMapper om = JsonV2.buildObjectMapper(); - String serializedLocation = om.writeValueAsString(spec); - assertNotNull(serializedLocation); + RatingSpec spec2 = om.readValue(serializedSpec, RatingSpec.class); + assertNotNull(spec2); + assertEquals(spec, spec2); + } - } + @Test + void testRoundtripXML() { + 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); + XMLv2 xmlv2 = new XMLv2(); - ObjectMapper om = JsonV2.buildObjectMapper(); - String serializedLocation = om.writeValueAsString(spec); - assertNotNull(serializedLocation); + String xml = xmlv2.format(spec); + assertNotNull(xml); - RatingSpec spec2 = om.readValue(serializedLocation, RatingSpec.class); - assertNotNull(spec2); + 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); + 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 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..51778d4f9 --- /dev/null +++ b/cwms-data-api/src/test/java/cwms/cda/data/dto/rating/RatingSpecsTest.java @@ -0,0 +1,93 @@ +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 cwms.cda.formatters.ContentType; +import cwms.cda.formatters.Formats; +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.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 { + + @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()); + } + + @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 + @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/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" +} 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 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 + + 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 + + + +