Skip to content
3 changes: 2 additions & 1 deletion cwms-data-api/src/main/java/cwms/cda/ApiServlet.java
Original file line number Diff line number Diff line change
Expand Up @@ -91,9 +91,9 @@
import cwms.cda.api.TimeSeriesController;
import cwms.cda.api.TimeSeriesFilteredController;
import cwms.cda.api.TimeSeriesGroupController;
import cwms.cda.api.TimeSeriesVersionsController;
import cwms.cda.api.TimeSeriesIdentifierDescriptorController;
import cwms.cda.api.TimeSeriesRecentController;
import cwms.cda.api.TimeSeriesVersionsController;
import cwms.cda.api.TimeZoneController;
import cwms.cda.api.TurbineChangesDeleteController;
import cwms.cda.api.TurbineChangesGetController;
Expand Down Expand Up @@ -488,6 +488,7 @@ protected void configureRoutes() {

VerticalDatumController vdiController = new VerticalDatumController(metrics);
String vdiPath = format("/location/{%s}/vertical-datum", Controllers.LOCATION_ID);
get("/location/vertical-datum", vdiController::getAll);
get(vdiPath, ctx -> vdiController.getOne(ctx, ctx.pathParam(Controllers.LOCATION_ID)));
addCacheControl(vdiPath, 5, TimeUnit.MINUTES);
post(vdiPath, vdiController::create, requiredRoles);
Expand Down
2 changes: 2 additions & 0 deletions cwms-data-api/src/main/java/cwms/cda/api/Controllers.java
Original file line number Diff line number Diff line change
Expand Up @@ -73,6 +73,8 @@ public final class Controllers {

public static final String LIKE = "like";

public static final String OVERWRITE = "overwrite";

public static final String UNIT_SYSTEM = "unit-system";

public static final String TIMESERIES_CATEGORY_LIKE = "timeseries-category-like";
Expand Down
107 changes: 84 additions & 23 deletions cwms-data-api/src/main/java/cwms/cda/api/VerticalDatumController.java
Original file line number Diff line number Diff line change
Expand Up @@ -29,10 +29,13 @@
import static cwms.cda.api.Controllers.DELETE;
import static cwms.cda.api.Controllers.GET_ONE;
import static cwms.cda.api.Controllers.LOCATION_ID;
import static cwms.cda.api.Controllers.LOCATION_MASK;
import static cwms.cda.api.Controllers.OFFICE;
import static cwms.cda.api.Controllers.OVERWRITE;
import static cwms.cda.api.Controllers.RESULTS;
import static cwms.cda.api.Controllers.SIZE;
import static cwms.cda.api.Controllers.UNIT;
import static cwms.cda.api.Controllers.UNIT_SYSTEM;
import static cwms.cda.api.Controllers.UPDATE;
import static cwms.cda.api.Controllers.requiredParam;
import static cwms.cda.api.LocationController.LOCATIONS_TAG;
Expand All @@ -41,12 +44,16 @@
import com.codahale.metrics.Histogram;
import com.codahale.metrics.MetricRegistry;
import com.codahale.metrics.Timer;
import com.fasterxml.jackson.dataformat.xml.annotation.JacksonXmlElementWrapper;
import com.google.common.flogger.FluentLogger;
import cwms.cda.api.enums.UnitSystem;
import cwms.cda.api.errors.AlreadyExists;
import cwms.cda.api.errors.CdaError;
import cwms.cda.api.errors.ExceptionTraceSupport;
import cwms.cda.data.dao.VerticalDatumDao;
import cwms.cda.data.dto.StatusResponse;
import cwms.cda.data.dto.VerticalDatumInfo;
import cwms.cda.data.dto.VerticalDatumInfoList;
import cwms.cda.formatters.ContentType;
import cwms.cda.formatters.Formats;
import io.javalin.apibuilder.CrudHandler;
Expand All @@ -59,6 +66,7 @@
import io.javalin.plugin.openapi.annotations.OpenApiRequestBody;
import io.javalin.plugin.openapi.annotations.OpenApiResponse;
import java.io.IOException;
import java.util.List;
import javax.servlet.http.HttpServletResponse;
import org.jetbrains.annotations.NotNull;
import org.jooq.DSLContext;
Expand All @@ -68,6 +76,8 @@ public final class VerticalDatumController implements CrudHandler {
private static final FluentLogger logger = FluentLogger.forEnclosingClass();
// NOTE: manually expanded due to limits of OpenApi Annotations.
private static final String VDI_PATH = "/location/{location-id}/vertical-datum";
private static final String VDI_ALL_PATH = "/location/vertical-datum";
private static final String ERROR_MESSAGE = "Failed to process request to retrieve Vertical Datum Info";
private final MetricRegistry metrics;
private final Histogram requestResultSize;

Expand All @@ -81,9 +91,49 @@ private Timer.Context markAndTime(String subject) {
return Controllers.markAndTime(metrics, getClass().getName(), subject);
}

@OpenApi(
queryParams = {
@OpenApiParam(name = LOCATION_MASK, description = "Filters on the location ID."),
@OpenApiParam(name = OFFICE, required = true, description = "Specifies the owning office."),
@OpenApiParam(name = UNIT_SYSTEM,
description = "Specifies the unit system of measure for elevation/offsets (SI or EN). Default is EN.")
},
responses = {
@OpenApiResponse(status = Controllers.STATUS_200,
content = {@OpenApiContent(type = Formats.JSONV1, from = VerticalDatumInfoList.class),
@OpenApiContent(type = Formats.JSON, from = VerticalDatumInfoList.class),
@OpenApiContent(type = Formats.XMLV1, from = VerticalDatumInfoList.class),
@OpenApiContent(type = Formats.XML, from = VerticalDatumInfoList.class)})
},
description = "Returns Vertical Datum Info for all locations.",
path = VDI_ALL_PATH,
tags = {LOCATIONS_TAG}
)
@Override
public void getAll(@NotNull Context ctx) {
ctx.status(HttpServletResponse.SC_NOT_IMPLEMENTED).json(CdaError.notImplemented());
String office = requiredParam(ctx, OFFICE);
String unitSystem = ctx.queryParamAsClass(UNIT_SYSTEM, String.class).getOrDefault(UnitSystem.SI.getValue());
String locationMask = ctx.queryParamAsClass(LOCATION_MASK, String.class).getOrDefault(null);
try (Timer.Context ignored = markAndTime(GET_ONE)) {
DSLContext dsl = getDslContext(ctx);
VerticalDatumDao dao = new VerticalDatumDao(dsl);
VerticalDatumInfoList vdiList = dao.retrieveVerticalDatumInfoList(office, locationMask, unitSystem);
String formatHeader = ctx.header(Header.ACCEPT);
ContentType contentType = Formats.parseHeader(formatHeader, VerticalDatumInfoList.class);
ctx.contentType(contentType.toString());
String serialized = Formats.format(contentType, vdiList);
requestResultSize.update(serialized.length());
ctx.status(HttpServletResponse.SC_OK);

byte[] bytes = serialized.getBytes();
ctx.header(Header.CONTENT_LENGTH, String.valueOf(bytes.length));
ctx.res.getOutputStream().write(bytes);
} catch (IOException ex) {
CdaError error = ExceptionTraceSupport.buildError(ctx,
ERROR_MESSAGE, ex);
logger.atSevere().withCause(ex).log(ERROR_MESSAGE);
ctx.status(HttpServletResponse.SC_INTERNAL_SERVER_ERROR).json(error);
}
}

@OpenApi(
Expand Down Expand Up @@ -117,7 +167,6 @@ public void getOne(@NotNull Context ctx, @NotNull String locationId) {
ContentType contentType = Formats.parseHeader(formatHeader, VerticalDatumInfo.class);
ctx.contentType(contentType.toString());
String serialized = Formats.format(contentType, info);
ctx.status(HttpServletResponse.SC_OK);
requestResultSize.update(serialized.length());
ctx.status(HttpServletResponse.SC_OK);

Expand All @@ -126,49 +175,61 @@ public void getOne(@NotNull Context ctx, @NotNull String locationId) {
ctx.res.getOutputStream().write(bytes);
} catch (IOException ex) {
CdaError error = ExceptionTraceSupport.buildError(ctx,
"Failed to process request to retrieve Vertical Datum Info", ex);
logger.atSevere().withCause(ex).log("Failed to process request to retrieve Vertical Datum Info");
ERROR_MESSAGE, ex);
logger.atSevere().withCause(ex).log(ERROR_MESSAGE);
ctx.status(HttpServletResponse.SC_INTERNAL_SERVER_ERROR).json(error);
}
}

@OpenApi(
requestBody = @OpenApiRequestBody(
content = {
@OpenApiContent(from = VerticalDatumInfo.class, type = Formats.JSONV1),
@OpenApiContent(from = VerticalDatumInfo.class, type = Formats.XMLV1)
},
required = true),
queryParams = {
@OpenApiParam(name = LOCATION_ID, required = true, description = "Specifies the location id for this vertical-datum-info."),
@OpenApiParam(name = OFFICE, required = true, description = "Specifies the owning office.")
requestBody = @OpenApiRequestBody(
content = {
@OpenApiContent(from = VerticalDatumInfo.class, type = Formats.JSONV1),
@OpenApiContent(from = VerticalDatumInfo.class, type = Formats.XMLV1)
},
description = "Create Vertical Datum Info for a Location",
method = HttpMethod.POST,
path = VDI_PATH,
tags = {LOCATIONS_TAG},
responses = {
@OpenApiResponse(status = Controllers.STATUS_201, description = "Vertical Datum Info successfully stored to CWMS.")
}
required = true),
queryParams = {
@OpenApiParam(name = LOCATION_ID, required = true, description = "Specifies the location id for this vertical-datum-info."),
@OpenApiParam(name = OFFICE, required = true, description = "Specifies the owning office."),
@OpenApiParam(name = OVERWRITE, type = Boolean.class, description = "If true, will overwrite any existing "
+ "vertical-datum-info for the specified location. Default is false.")
},
description = "Create Vertical Datum Info for a Location",
method = HttpMethod.POST,
path = VDI_PATH,
tags = {LOCATIONS_TAG},
responses = {
@OpenApiResponse(status = Controllers.STATUS_201,
description = "Vertical Datum Info successfully stored to CWMS.")
}
)
@Override
public void create(@NotNull Context ctx) {
try (Timer.Context ignored = markAndTime(CREATE)) {
String formatHeader = ctx.req.getContentType();
boolean overwrite = ctx.queryParamAsClass(OVERWRITE, Boolean.class).getOrDefault(false);
ContentType contentType = Formats.parseHeader(formatHeader, VerticalDatumInfo.class);
VerticalDatumInfo info = Formats.parseContent(contentType, ctx.body(), VerticalDatumInfo.class);
//allow locationId and office to be specified in either the body or as query params, but require them to be present in one of those places
String locationId = info.getLocation();
String office = info.getOffice();
if(locationId == null || locationId.isBlank()) {
if (locationId == null || locationId.isBlank()) {
locationId = requiredParam(ctx, LOCATION_ID);
}
if(office == null || office.isBlank()) {
if (office == null || office.isBlank()) {
office = requiredParam(ctx, OFFICE);
}
DSLContext dsl = getDslContext(ctx);
VerticalDatumDao dao = new VerticalDatumDao(dsl);
dao.createVerticalDatumInfo(office, locationId, info);
try {
dao.createVerticalDatumInfo(office, locationId, info);
} catch (AlreadyExists ex) {
if (overwrite) {
dao.updateVerticalDatumInfo(office, locationId, info);
} else {
throw ex;
}
}
StatusResponse re = new StatusResponse(office,
"Vertical Datum Info successfully stored to CWMS.", locationId);
ctx.status(HttpServletResponse.SC_CREATED).json(re);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -26,11 +26,16 @@
import cwms.cda.api.errors.AlreadyExists;
import cwms.cda.api.errors.NotFoundException;
import cwms.cda.data.dto.VerticalDatumInfo;
import cwms.cda.data.dto.VerticalDatumInfoList;
import cwms.cda.formatters.xml.XMLv1;
import java.sql.Clob;
import java.util.ArrayList;
import java.util.List;
import org.jooq.DSLContext;
import org.jooq.Record1;
import usace.cwms.db.jooq.codegen.packages.CWMS_LOC_PACKAGE;
import usace.cwms.db.jooq.codegen.tables.AV_VERT_DATUM_OFFSET;
import usace.cwms.db.jooq.codegen_latest.udt.records.CLOB_TAB_T;

/**
* DAO responsible for CRUD operations on Vertical Datum Info for a Location.
Expand All @@ -51,6 +56,22 @@ public VerticalDatumInfo retrieveVerticalDatumInfo(String officeId, String locat
});
}

public VerticalDatumInfoList retrieveVerticalDatumInfoList(String officeId, String locMask, String units) {
List<VerticalDatumInfo> resultList = new ArrayList<>();
connection(dsl, conn -> {
DSLContext ctx = getDslContext(conn, officeId);
String mask = locMask == null ? "%" : locMask;
CLOB_TAB_T datumInfo = usace.cwms.db.jooq.codegen_latest.packages.CWMS_LOC_PACKAGE
.call_GET_VERTICAL_DATUM_INFO_LIST(ctx.configuration(), officeId, mask, units);
for (Object info : datumInfo) {
Clob clob = (Clob) info;
VerticalDatumInfo vdi = new XMLv1().parseContent(clob.getAsciiStream(), VerticalDatumInfo.class);
resultList.add(vdi);
}
});
return new VerticalDatumInfoList(resultList);
}

public void createVerticalDatumInfo(String officeId, String locationId, VerticalDatumInfo vdi) {
connection(dsl, conn -> {
DSLContext ctx = getDslContext(conn, officeId);
Expand Down Expand Up @@ -98,7 +119,7 @@ private void verifyVerticalDatumInfoExists(DSLContext ctx, String officeId, Stri
.where(AV_VERT_DATUM_OFFSET.AV_VERT_DATUM_OFFSET.LOCATION_ID.eq(locationId))
.and(AV_VERT_DATUM_OFFSET.AV_VERT_DATUM_OFFSET.OFFICE_ID.eq(officeId))
.fetchOne();
if(result == null) {
if (result == null) {
throw new NotFoundException("No vertical datum info found for location " + locationId + " in office " + officeId);
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.Objects;
import java.util.Set;
import java.util.stream.Collectors;

Expand Down Expand Up @@ -141,6 +142,24 @@ private VerticalDatumInfo.Offset[] buildConvertedOffsets(VerticalDatum convertTo
return newOffsets.toArray(new VerticalDatumInfo.Offset[]{});
}

@Override
public boolean equals(Object o) {
if (o == null || getClass() != o.getClass()) {
return false;
}
VerticalDatumInfo that = (VerticalDatumInfo) o;
return Objects.equals(office, that.office) && Objects.equals(unit, that.unit)
&& Objects.equals(location, that.location) && Objects.equals(nativeDatum, that.nativeDatum)
&& Objects.equals(elevation, that.elevation)
&& Objects.equals(localDatumName, that.localDatumName)
&& Objects.deepEquals(offsets, that.offsets);
}

@Override
public int hashCode() {
return Objects.hash(office, unit, location, nativeDatum, elevation, localDatumName, Arrays.hashCode(offsets));
}

@JsonNaming(PropertyNamingStrategies.KebabCaseStrategy.class)
public static class Offset {
@JacksonXmlProperty(isAttribute = true)
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
/*
*
* MIT License
*
* Copyright (c) 2026 Hydrologic Engineering Center
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in all
* copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
* DEALINGS IN THE
* SOFTWARE.
*/

package cwms.cda.data.dto;

import com.fasterxml.jackson.annotation.JsonCreator;
import com.fasterxml.jackson.annotation.JsonProperty;
import com.fasterxml.jackson.annotation.JsonRootName;
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 cwms.cda.formatters.Formats;
import cwms.cda.formatters.annotations.FormattableWith;
import cwms.cda.formatters.json.JsonV1;
import cwms.cda.formatters.json.JsonV2;
import cwms.cda.formatters.xml.XMLv1;
import cwms.cda.formatters.xml.XMLv2;
import java.util.List;
import java.util.Objects;

@JsonRootName("vertical-data")
@JacksonXmlRootElement(localName = "vertical-data")
@FormattableWith(contentType = Formats.XMLV1, formatter = XMLv1.class)
@FormattableWith(contentType = Formats.XMLV2, formatter = XMLv2.class, aliases = {Formats.XML})
@FormattableWith(contentType = Formats.JSONV2, formatter = JsonV2.class, aliases = {Formats.DEFAULT, Formats.JSON})
@FormattableWith(contentType = Formats.JSONV1, formatter = JsonV1.class)
public final class VerticalDatumInfoList extends CwmsDTOBase {
@JacksonXmlElementWrapper(useWrapping = false)
@JacksonXmlProperty(localName = "vertical-datum-info")
@JsonProperty("vertical-datum-info")
private final List<VerticalDatumInfo> datumList;

@JsonCreator
public VerticalDatumInfoList(@JsonProperty("vertical-datum-info") List<VerticalDatumInfo> verticalDatumInfoList) {
this.datumList = verticalDatumInfoList;
}

public List<VerticalDatumInfo> getDatumList() {
return datumList;
}

@Override
public boolean equals(Object o) {
if (o == null || getClass() != o.getClass()) {
return false;
}
VerticalDatumInfoList that = (VerticalDatumInfoList) o;
return Objects.equals(datumList, that.datumList);
}

@Override
public int hashCode() {
return Objects.hashCode(datumList);
}
}
Loading
Loading