Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
16 commits
Select commit Hold shift + click to select a range
c0e6155
Adding controller integration tests for supporting vertical datum.
RyanM-RMA Oct 10, 2025
69b503c
Adding missing controllers and vertical datum enum
RyanM-RMA Oct 10, 2025
f806465
Refactor `RatingsControllerTestIT` by moving vertical datum tests to …
RyanM-RMA Oct 14, 2025
95079de
Improve vertical datum handling in `VerticalDatum` and update `Rating…
RyanM-RMA Oct 17, 2025
d4ff58b
Trim input string in `VerticalDatum` and expand test cases in `Rating…
RyanM-RMA Oct 20, 2025
528b7a7
Add `DeleteMeTest` for testing vertical datum rating conversions and …
RyanM-RMA Dec 30, 2025
fa76748
CDA-46 - Implements vertical datum support for create, update, and re…
rma-bryson Jan 5, 2026
a8517a0
CDA-46 - Removed unused vertical datum constant
rma-bryson Jan 8, 2026
d6d20ba
CDA-46 - Adds setting of local datum for conversions to work similar …
rma-bryson Jan 8, 2026
ae9b9b3
CDA-46 - Fixed validation error on datum param if its empty
rma-bryson Jan 8, 2026
83c165a
CDA-46 - Updated datum check to handle dashes.
rma-bryson Jan 8, 2026
69c6d02
CDA-46 - updated to check for "unknown" datum first to prevent exception
rma-bryson Jan 8, 2026
8ac297c
CDA-46 - Reverted RatingsControllerTestIT change that uses vertical d…
rma-bryson Jan 9, 2026
4ddc973
CDA-46 - Adds check to ignore vertical datum conversion if no target …
rma-bryson Jan 9, 2026
2aa15ec
CDA-46 - Fixes failing test due to location not existing
rma-bryson Jan 9, 2026
f85abd0
CDA-46 - integrating cwms-rating with matching jooq-codegen version
rma-bryson Jan 22, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
100 changes: 93 additions & 7 deletions cwms-data-api/src/main/java/cwms/cda/api/rating/RatingController.java
Original file line number Diff line number Diff line change
Expand Up @@ -57,13 +57,17 @@
import com.codahale.metrics.Histogram;
import com.codahale.metrics.MetricRegistry;
import com.codahale.metrics.Timer;
import com.fasterxml.jackson.dataformat.xml.XmlMapper;
import cwms.cda.api.Controllers;
import cwms.cda.api.errors.CdaError;
import cwms.cda.data.dao.JsonRatingUtils;
import cwms.cda.data.dao.RatingDao;
import cwms.cda.data.dao.RatingSetDao;
import cwms.cda.data.dao.RatingsVerticalDatumExtractor;
import cwms.cda.data.dao.VerticalDatum;
import cwms.cda.data.dto.CwmsDTOBase;
import cwms.cda.data.dto.StatusResponse;
import cwms.cda.data.dto.VerticalDatumInfo;
import cwms.cda.formatters.ContentType;
import cwms.cda.formatters.Formats;
import cwms.cda.formatters.annotations.FormattableWith;
Expand All @@ -88,7 +92,9 @@
import com.google.common.flogger.FluentLogger;
import javax.servlet.http.HttpServletResponse;
import javax.xml.transform.TransformerException;

import mil.army.usace.hec.cwms.rating.io.xml.RatingXmlFactory;
import mil.army.usace.hec.metadata.VerticalDatumException;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import org.jooq.DSLContext;
Expand Down Expand Up @@ -136,7 +142,16 @@ protected RatingDao getRatingDao(DSLContext dsl) {
required = true),
queryParams = {
@OpenApiParam(name = STORE_TEMPLATE, type = Boolean.class,
description = "Also store updates to the rating template. Default: true")
description = "Also store updates to the rating template. Default: true"),
@OpenApiParam(name = DATUM, type = VerticalDatum.class, description = "If the provided "
+ "rating-set includes an explicit vertical-datum-info attribute "
+ "then it is assumed that the data is in the datum specified by the vertical-datum-info. "
+ "If the input rating-set does not include vertical-datum-info and "
+ "this parameter is not provided it is assumed that the data is in the as-stored "
+ "datum and no conversion is necessary. "
+ "If the input rating-set does not include vertical-datum-info and "
+ "this parameter is provided it is assumed that the data is in the Datum named by the argument "
+ "and should be converted to the as-stored datum before being saved.")
},
method = HttpMethod.POST, path = "/ratings", tags = {TAG},
responses = {
Expand All @@ -149,7 +164,14 @@ public void create(@NotNull Context ctx) {
RatingDao ratingDao = getRatingDao(dsl);
boolean storeTemplate = ctx.queryParamAsClass(STORE_TEMPLATE, Boolean.class).getOrDefault(true);
String ratingSet = deserializeRatingSet(ctx, storeTemplate);
ratingDao.create(ratingSet, false);
String datum = ctx.queryParam(DATUM);
VerticalDatum vd = null;
if(datum != null) {
vd = ctx.queryParamAsClass(DATUM, VerticalDatum.class)
.getOrDefault(null);
}
vd = RatingsVerticalDatumExtractor.getVerticalDatum(ratingSet).orElse(vd);
ratingDao.create(ratingSet, false, vd);
StatusResponse re = new StatusResponse(RatingDao.extractOfficeFromXml(ratingSet), "Rating Set successfully stored to CWMS.");
ctx.status(HttpServletResponse.SC_CREATED).json(re);
} catch (IOException ex) {
Expand Down Expand Up @@ -265,7 +287,9 @@ public void delete(@NotNull Context ctx, @NotNull String ratingSpecId) {
+ "\n* `NAVD88` The elevation values will in the "
+ "specified or default units above the NAVD-88 datum."
+ "\n* `NGVD29` The elevation values will be in the "
+ "specified or default units above the NGVD-29 datum."),
+ "specified or default units above the NGVD-29 datum."
+ "\n* `NATIVE` The elevation values will be in the "
+ "Location's native datum."),
@OpenApiParam(name = AT, description = "Specifies the "
+ "start of the time window for data to be included in the response. "
+ "If this field is not specified, any required time window begins 24"
Expand Down Expand Up @@ -362,6 +386,16 @@ public void getAll(@NotNull Context ctx) {
@OpenApiParam(name = METHOD, description = "Specifies "
+ "the retrieval method used. If no method is provided EAGER will be used.",
type = RatingSet.DatabaseLoadMethod.class),
@OpenApiParam(name = DATUM, description = "Specifies the "
+ "elevation datum of the response. This field affects only elevation"
+ " Ratings. Valid values for this field are:"
+ "\n* `NAVD88` The elevation values will in the "
+ "specified or default units above the NAVD-88 datum."
+ "\n* `NGVD29` The elevation values will be in the "
+ "specified or default units above the NGVD-29 datum."
+ "\n* `NATIVE` The elevation values will be in the "
+ "Location's native datum.",
type = VerticalDatum.class),
},
responses = {
@OpenApiResponse(status = STATUS_200, content = {
Expand All @@ -377,6 +411,7 @@ public void getOne(@NotNull Context ctx, @NotNull String rating) {
try (final Timer.Context ignored = markAndTime(GET_ONE)) {
String officeId = ctx.queryParam(OFFICE);
String timezone = ctx.queryParamAsClass(TIMEZONE, String.class).getOrDefault("UTC");
VerticalDatum verticalDatum = VerticalDatum.getVerticalDatum(ctx.queryParam(DATUM));

Instant beginInstant = null;
String begin = ctx.queryParam(BEGIN);
Expand All @@ -394,7 +429,7 @@ public void getOne(@NotNull Context ctx, @NotNull String rating) {
RatingSet.DatabaseLoadMethod.class)
.getOrDefault(RatingSet.DatabaseLoadMethod.EAGER);

String body = getRatingSetString(ctx, method, officeId, rating, beginInstant, endInstant);
String body = getRatingSetString(ctx, method, officeId, rating, beginInstant, endInstant, verticalDatum);
if (body != null) {
ctx.result(body);
ctx.status(HttpCode.OK);
Expand All @@ -406,7 +441,7 @@ public void getOne(@NotNull Context ctx, @NotNull String rating) {
@Nullable
private String getRatingSetString(Context ctx, RatingSet.DatabaseLoadMethod method,
String officeId, String rating, Instant begin,
Instant end) {
Instant end, VerticalDatum verticalDatum) {
String retval = null;

try (final Timer.Context ignored = markAndTime("getRatingSetString")) {
Expand All @@ -421,9 +456,44 @@ private String getRatingSetString(Context ctx, RatingSet.DatabaseLoadMethod meth
try {
RatingSet ratingSet = getRatingSet(ctx, method, officeId, rating, begin, end);
if (ratingSet != null) {
//Apply vertical datum conversion if needed
if (verticalDatum != null) {
try {
switch (verticalDatum)
{
case NAVD88:
ratingSet.toNAVD88();
break;
case NGVD29:
ratingSet.toNGVD29();
break;
case NATIVE:
ratingSet.toNativeVerticalDatum();
break;
default:
logger.atSevere().log("Unknown vertical datum: %s", verticalDatum);
break;
}
VerticalDatumInfo vdi = RatingsVerticalDatumExtractor.deserializeVerticalDatumInfoXml(ratingSet.getVerticalDatumInfo());
if(vdi != null && vdi.getOffsetForDatum(verticalDatum) != null) {
VerticalDatumInfo newVdi = vdi.convertedTo(vdi.getOffsetForDatum(verticalDatum));
XmlMapper xmlMapper = new XmlMapper();
String vdiXml = xmlMapper.writeValueAsString(newVdi);
ratingSet.setVerticalDatumInfo(vdiXml);
}
} catch (VerticalDatumException vde) {
logger.atWarning().withCause(vde).log("Failed to convert rating %s to requested vertical datum: %s",
rating, verticalDatum);
}
}
if (isJson) {
retval = JsonRatingUtils.toJson(ratingSet);
} else {
//the toXml method in RatingXmlFactory converts to native-datum which breaks things coming back in the user-requested datum
//setting the current-datum to an unknown value prevents the call to convert to native-datum
if(ratingSet.getVerticalDatumContainer() != null && ratingSet.getVerticalDatumContainer().currentDatum != null) {
ratingSet.getVerticalDatumContainer().currentDatum = "ignoreConversionToNativeDatum";
}
retval = RatingXmlFactory.toXml(ratingSet, " ");
}
} else {
Expand Down Expand Up @@ -483,7 +553,16 @@ private RatingSet getRatingSet(Context ctx, RatingSet.DatabaseLoadMethod method,
@OpenApiParam(name = STORE_TEMPLATE, type = Boolean.class,
description = "Also store updates to the rating template. Default: true"),
@OpenApiParam(name = REPLACE_BASE_CURVE, type = Boolean.class,
description = "Replace the base curve of USGS stream flow rating. Default: false")
description = "Replace the base curve of USGS stream flow rating. Default: false"),
@OpenApiParam(name = DATUM, type = VerticalDatum.class, description = "If the provided "
+ "rating-set includes an explicit vertical-datum-info attribute "
+ "then it is assumed that the data is in the datum specified by the vertical-datum-info. "
+ "If the input rating-set does not include vertical-datum-info and "
+ "this parameter is not provided it is assumed that the data is in the as-stored "
+ "datum and no conversion is necessary. "
+ "If the input rating-set does not include vertical-datum-info and "
+ "this parameter is provided it is assumed that the data is in the Datum named by the argument "
+ "and should be converted to the as-stored datum before being saved.")
},
method = HttpMethod.PATCH, path = "/ratings", tags = {TAG})
public void update(@NotNull Context ctx, @NotNull String ratingId) {
Expand All @@ -498,7 +577,14 @@ public void update(@NotNull Context ctx, @NotNull String ratingId) {
boolean replaceBaseCurve = ctx.queryParamAsClass(REPLACE_BASE_CURVE, Boolean.class)
.getOrDefault(false);
String ratingSet = deserializeRatingSet(ctx, storeTemplate);
ratingDao.store(ratingSet, replaceBaseCurve);
String datum = ctx.queryParam(DATUM);
VerticalDatum vd = null;
if(datum != null) {
vd = ctx.queryParamAsClass(DATUM, VerticalDatum.class)
.getOrDefault(null);
}
vd = RatingsVerticalDatumExtractor.getVerticalDatum(ratingSet).orElse(vd);
ratingDao.store(ratingSet, replaceBaseCurve, vd);
StatusResponse re = new StatusResponse(RatingDao.extractOfficeFromXml(ratingSet), "Updated RatingSet");
ctx.status(HttpServletResponse.SC_OK).json(re);
} catch (IOException ex) {
Expand Down
30 changes: 30 additions & 0 deletions cwms-data-api/src/main/java/cwms/cda/data/dao/JooqDao.java
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,7 @@
import java.util.Arrays;
import java.util.Collections;
import java.util.List;
import java.util.Objects;
import java.util.Optional;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
Expand All @@ -72,6 +73,7 @@
import org.owasp.html.HtmlPolicyBuilder;
import org.owasp.html.PolicyFactory;
import usace.cwms.db.jooq.codegen.packages.CWMS_ENV_PACKAGE;
import usace.cwms.db.jooq.codegen.packages.CWMS_LOC_PACKAGE;
import usace.cwms.db.jooq.codegen.packages.CWMS_UTIL_PACKAGE;


Expand Down Expand Up @@ -215,6 +217,34 @@ protected static Double toDouble(BigDecimal bigDecimal) {
return retVal;
}

/**
* The idea here is that this will check the current default datum,
* possible switch to the specified datum and
* then run the code and
* if the datum was previously switched
* then switch back to the initial datum.
* @param targetDatum The desired ver
* @param dslContext
* @param cr
*/
protected void withDefaultDatum(@Nullable VerticalDatum targetDatum, DSLContext dslContext, ConnectionRunnable cr) {
String defaultVertDatum = CWMS_LOC_PACKAGE.call_GET_DEFAULT_VERTICAL_DATUM(dslContext.configuration());
String targetName = (targetDatum != null) ? targetDatum.toString() : null;
boolean changeDefaultDatum = !Objects.equals(targetName, defaultVertDatum);
try {
if (changeDefaultDatum) {
CWMS_LOC_PACKAGE.call_SET_DEFAULT_VERTICAL_DATUM(dslContext.configuration(), targetName);
}

connection(dslContext, cr);
} finally {
if (changeDefaultDatum) {
// If we changed it we should restore.
CWMS_LOC_PACKAGE.call_SET_DEFAULT_VERTICAL_DATUM(dslContext.configuration(), defaultVertDatum);
}
}
}

/**
* Oracle supports case insensitive regexp search but the syntax for calling it is a
* bit weird. This method lets Dao classes add a case-insensitive regexp search in
Expand Down
12 changes: 10 additions & 2 deletions cwms-data-api/src/main/java/cwms/cda/data/dao/RatingDao.java
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,7 @@ public interface RatingDao {

Pattern officeMatcher = Pattern.compile(".*office-id=\"(.*?)\"");

void create(String ratingSet, boolean replaceBaseCurve) throws IOException, RatingException;
void create(String ratingSet, boolean replaceBaseCurve, VerticalDatum vd) throws IOException, RatingException;

RatingSet retrieve(RatingSet.DatabaseLoadMethod method, String officeId, String specificationId,
Instant start, Instant end) throws IOException, RatingException;
Expand All @@ -46,10 +46,18 @@ String retrieveRatings(String format, String names, String unit, String datum, S
String start, String end, String timezone);


void store(String ratingSet, boolean replaceBaseCurve) throws IOException, RatingException;
void store(String ratingSet, boolean replaceBaseCurve, VerticalDatum vd) throws IOException, RatingException;

void delete(String officeId, String specificationId, Instant start, Instant end);

default void create(String ratingSet, boolean replaceBaseCurve) throws IOException, RatingException {
create(ratingSet, replaceBaseCurve, null);
}

default void store(String ratingSet, boolean replaceBaseCurve) throws IOException, RatingException {
store(ratingSet, replaceBaseCurve, null);
}

static String extractOfficeFromXml(String xml) {
Matcher officeMatch = officeMatcher.matcher(xml);

Expand Down
Loading
Loading