Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

CWMVUE-611 - Adding in endpoint to get time range map for measurments #975

Merged
merged 4 commits into from
Dec 13, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
4 changes: 4 additions & 0 deletions cwms-data-api/src/main/java/cwms/cda/ApiServlet.java
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@
import static cwms.cda.api.Controllers.OFFICE;
import static cwms.cda.api.Controllers.PROJECT_ID;
import static cwms.cda.api.Controllers.WATER_USER;
import cwms.cda.api.MeasurementTimeExtentsGetController;
import static io.javalin.apibuilder.ApiBuilder.crud;
import static io.javalin.apibuilder.ApiBuilder.delete;
import static io.javalin.apibuilder.ApiBuilder.get;
Expand Down Expand Up @@ -594,6 +595,9 @@ protected void configureRoutes() {
cdaCrudCache(format("/stream-reaches/{%s}", NAME),
new StreamReachController(metrics), requiredRoles,1, TimeUnit.DAYS);
String measurements = "/measurements/";
String measTimeExtents = measurements + "time-extents";
get(measTimeExtents,new MeasurementTimeExtentsGetController(metrics));
addCacheControl(measTimeExtents, 5, TimeUnit.MINUTES);
cdaCrudCache(format(measurements + "{%s}", LOCATION_ID),
new cwms.cda.api.MeasurementController(metrics), requiredRoles,5, TimeUnit.MINUTES);
cdaCrudCache("/blobs/{blob-id}",
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,93 @@
/*
* MIT License
*
* Copyright (c) 2024 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.api;

import com.codahale.metrics.Histogram;
import com.codahale.metrics.MetricRegistry;
import static com.codahale.metrics.MetricRegistry.name;
import com.codahale.metrics.Timer;
import static cwms.cda.api.Controllers.OFFICE_MASK;
import static cwms.cda.api.Controllers.RESULTS;
import static cwms.cda.api.Controllers.SIZE;
import static cwms.cda.api.Controllers.STATUS_200;
import static cwms.cda.data.dao.JooqDao.getDslContext;
import cwms.cda.data.dao.MeasurementDao;
import cwms.cda.data.dto.CwmsIdTimeExtentsEntry;
import cwms.cda.formatters.ContentType;
import cwms.cda.formatters.Formats;
import io.javalin.core.util.Header;
import io.javalin.http.Context;
import io.javalin.http.Handler;
import io.javalin.plugin.openapi.annotations.OpenApi;
import io.javalin.plugin.openapi.annotations.OpenApiContent;
import io.javalin.plugin.openapi.annotations.OpenApiParam;
import io.javalin.plugin.openapi.annotations.OpenApiResponse;
import java.util.List;
import javax.servlet.http.HttpServletResponse;
import org.jetbrains.annotations.NotNull;
import org.jooq.DSLContext;

public final class MeasurementTimeExtentsGetController implements Handler {
private final MetricRegistry metrics;
private final Histogram requestResultSize;

public MeasurementTimeExtentsGetController(MetricRegistry metrics) {
this.metrics = metrics;
String className = this.getClass().getName();
requestResultSize = this.metrics.histogram(name(className, RESULTS, SIZE));
}

private Timer.Context markAndTime() {
return Controllers.markAndTime(metrics, getClass().getName(), Controllers.GET_ALL);
}

@OpenApi(
queryParams = {
@OpenApiParam(name = OFFICE_MASK, description = "Office Id used to filter the results.")
},
responses = {
@OpenApiResponse(status = STATUS_200, content = {
@OpenApiContent(isArray = true, type = Formats.JSONV1, from = CwmsIdTimeExtentsEntry.class)
})
},
description = "Returns matching downstream stream locations.",
tags = {StreamLocationController.TAG}
)
public void handle(@NotNull Context ctx) throws Exception {
String officeIdMask = ctx.queryParam(OFFICE_MASK);
try (Timer.Context ignored = markAndTime()) {
DSLContext dsl = getDslContext(ctx);
MeasurementDao dao = new MeasurementDao(dsl);
List<CwmsIdTimeExtentsEntry> timeExtents = dao.retrieveMeasurementTimeExtentsMap(officeIdMask);

String formatHeader = ctx.header(Header.ACCEPT);
ContentType contentType = Formats.parseHeader(formatHeader, CwmsIdTimeExtentsEntry.class);
ctx.contentType(contentType.toString());
String serialized = Formats.format(contentType, timeExtents, CwmsIdTimeExtentsEntry.class);
ctx.result(serialized);
ctx.status(HttpServletResponse.SC_OK);
requestResultSize.update(serialized.length());
}
}
}
6 changes: 6 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 @@ -24,6 +24,8 @@

package cwms.cda.data.dao;

import java.sql.Timestamp;
import java.time.Instant;
import static org.jooq.SQLDialect.ORACLE;

import com.google.common.flogger.FluentLogger;
Expand Down Expand Up @@ -128,6 +130,10 @@ public static DSLContext getDslContext(Context ctx) {
return retVal;
}

protected static Timestamp buildTimestamp(Instant date) {
return date != null ? Timestamp.from(date) : null;
}

public static DSLContext getDslContext(Connection connection, String officeId) {
// Because this dsl is constructed with a connection, jOOQ will reuse the provided
// connection and not get new connections from a DataSource. See:
Expand Down
103 changes: 83 additions & 20 deletions cwms-data-api/src/main/java/cwms/cda/data/dao/MeasurementDao.java
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,8 @@
import cwms.cda.api.enums.UnitSystem;
import cwms.cda.api.errors.NotFoundException;
import cwms.cda.data.dto.CwmsId;
import cwms.cda.data.dto.CwmsIdTimeExtentsEntry;
import cwms.cda.data.dto.TimeExtents;
import cwms.cda.data.dto.measurement.Measurement;
import cwms.cda.data.dto.measurement.StreamflowMeasurement;
import cwms.cda.data.dto.measurement.SupplementalStreamflowMeasurement;
Expand All @@ -56,24 +58,28 @@
import java.sql.Timestamp;
import java.time.Instant;
import java.time.OffsetDateTime;
import java.time.ZoneId;
import java.time.ZonedDateTime;
import java.time.format.DateTimeFormatter;
import java.util.ArrayList;
import java.util.Date;
import java.util.TimeZone;
import java.util.Optional;
import mil.army.usace.hec.metadata.location.LocationTemplate;
import org.jooq.DSLContext;

import java.util.List;

import static java.util.stream.Collectors.toList;
import org.jooq.impl.DSL;
import usace.cwms.db.dao.util.OracleTypeMap;
import static org.jooq.impl.DSL.max;
import static org.jooq.impl.DSL.min;
import usace.cwms.db.jooq.codegen.packages.CWMS_STREAM_PACKAGE;
import usace.cwms.db.jooq.codegen.tables.AV_STREAMFLOW_MEAS;
import usace.cwms.db.jooq.codegen.udt.records.STREAMFLOW_MEAS2_T;
import usace.cwms.db.jooq.codegen.udt.records.STREAMFLOW_MEAS2_TAB_T;

public final class MeasurementDao extends JooqDao<Measurement> {

static final String MIN_DATE = "MIN_DATE";
static final String MAX_DATE = "MAX_DATE";
static final XmlMapper XML_MAPPER = buildXmlMapper();

public MeasurementDao(DSLContext dsl) {
Expand All @@ -93,19 +99,16 @@ public List<Measurement> retrieveMeasurements(String officeId, String locationId
String agencies, String qualities) {
return connectionResult(dsl, conn -> {
setOffice(conn, officeId);
Timestamp minTimestamp = OracleTypeMap.buildTimestamp(minDateMask == null ? null : Date.from(minDateMask));
Timestamp maxTimestamp = OracleTypeMap.buildTimestamp(maxDateMask == null ? null : Date.from(maxDateMask));
TimeZone timeZone = OracleTypeMap.GMT_TIME_ZONE;
return retrieveMeasurementsJooq(conn, officeId, locationId, unitSystem, minHeight, maxHeight, minFlow, maxFlow, minNum, maxNum, agencies, qualities, minTimestamp, maxTimestamp, timeZone);
Timestamp minTimestamp = buildTimestamp(minDateMask);
Timestamp maxTimestamp = buildTimestamp(maxDateMask);
return retrieveMeasurementsJooq(conn, officeId, locationId, unitSystem, minHeight, maxHeight, minFlow, maxFlow, minNum, maxNum, agencies, qualities, minTimestamp, maxTimestamp);
});
}

private static List<Measurement> retrieveMeasurementsJooq(Connection conn, String officeId, String locationId, String unitSystem, Number minHeight, Number maxHeight, Number minFlow, Number maxFlow, String minNum, String maxNum, String agencies, String qualities, Timestamp minTimestamp, Timestamp maxTimestamp, TimeZone timeZone) {
STREAMFLOW_MEAS2_TAB_T retrieved = CWMS_STREAM_PACKAGE.call_RETRIEVE_MEAS_OBJS(DSL.using(conn).configuration(), locationId, unitSystem, minTimestamp, maxTimestamp,
minHeight, maxHeight, minFlow, maxFlow, minNum, maxNum, agencies, qualities, timeZone.getID(), officeId);
List<Measurement> retVal = retrieved.stream()
.map(MeasurementDao::fromJooqMeasurementRecord)
.collect(toList());
private static List<Measurement> retrieveMeasurementsJooq(Connection conn, String officeId, String locationId, String unitSystem, Number minHeight, Number maxHeight, Number minFlow, Number maxFlow, String minNum, String maxNum, String agencies, String qualities, Timestamp minTimestamp, Timestamp maxTimestamp) throws JsonProcessingException {
String xml = CWMS_STREAM_PACKAGE.call_RETRIEVE_MEAS_XML(DSL.using(conn).configuration(), locationId, unitSystem, minTimestamp, maxTimestamp,
minHeight, maxHeight, minFlow, maxFlow, minNum, maxNum, agencies, qualities, "UTC", officeId);
List<Measurement> retVal = fromDbXml(xml);
if(retVal.isEmpty()) {
throw new NotFoundException("No measurements found.");
}
Expand Down Expand Up @@ -142,24 +145,55 @@ public void deleteMeasurements(String officeId, String locationId, Instant minDa
String maxNum) {
connection(dsl, conn -> {
setOffice(conn, officeId);
Timestamp minTimestamp = OracleTypeMap.buildTimestamp(minDateMask == null ? null : Date.from(minDateMask));
Timestamp maxTimestamp = OracleTypeMap.buildTimestamp(maxDateMask == null ? null : Date.from(maxDateMask));
TimeZone timeZone = OracleTypeMap.GMT_TIME_ZONE;
String timeZoneId = timeZone.getID();
Timestamp minTimestamp = buildTimestamp(minDateMask);
Timestamp maxTimestamp = buildTimestamp(maxDateMask);
String timeZoneId = "UTC";
verifyMeasurementsExists(conn, officeId, locationId, maxNum, maxNum);
CWMS_STREAM_PACKAGE.call_DELETE_STREAMFLOW_MEAS(DSL.using(conn).configuration(), locationId, minNum, minTimestamp, maxTimestamp,
null, null, null, null, maxNum, maxNum, null, null, timeZoneId, officeId);
});
}

private void verifyMeasurementsExists(Connection conn, String officeId, String locationId, String minNum, String maxNum) {
public List<CwmsIdTimeExtentsEntry> retrieveMeasurementTimeExtentsMap(String officeIdMask)
{
AV_STREAMFLOW_MEAS view = AV_STREAMFLOW_MEAS.AV_STREAMFLOW_MEAS;
return dsl.select(
view.OFFICE_ID,
view.LOCATION_ID,
min(view.DATE_TIME_UTC).as(MIN_DATE).convertFrom(Timestamp::toInstant),
max(view.DATE_TIME_UTC).as(MAX_DATE).convertFrom(Timestamp::toInstant)
)
.from(view)
.where(Optional.ofNullable(officeIdMask)
.map(view.OFFICE_ID::eq)
.orElse(DSL.trueCondition()))
.groupBy(view.OFFICE_ID, view.LOCATION_ID)
.fetch()
.map(record -> new CwmsIdTimeExtentsEntry.Builder()
.withId( new CwmsId.Builder()
.withOfficeId(record.get(view.OFFICE_ID))
.withName(record.get(view.LOCATION_ID))
.build())
.withTimeExtents(new TimeExtents.Builder()
.withEarliestTime(record.value3().atZone(ZoneId.of("UTC")))
.withLatestTime(record.value4().atZone(ZoneId.of("UTC")))
.build())
.build());
}

private void verifyMeasurementsExists(Connection conn, String officeId, String locationId, String minNum, String maxNum) throws JsonProcessingException {
List<Measurement> measurements = retrieveMeasurementsJooq(conn, officeId, locationId, UnitSystem.EN.toString(),
null, null, null, null, minNum, maxNum, null, null, null, null, OracleTypeMap.GMT_TIME_ZONE);
null, null, null, null, minNum, maxNum, null, null, null, null);
if (measurements.isEmpty()) {
throw new NotFoundException("Could not find measurements for " + locationId + " in office " + officeId + ".");
}
}

static List<Measurement> fromDbXml(String xml) throws JsonProcessingException {
MeasurementsXmlDto xmlDto = XML_MAPPER.readValue(xml, MeasurementsXmlDto.class);
return convertMeasurementsFromXmlDto(xmlDto);
}

static String toDbXml(List<Measurement> measurements) throws JsonProcessingException {
MeasurementsXmlDto xmlDto = convertMeasurementsToXmlDto(measurements);
return XML_MAPPER.writeValueAsString(xmlDto);
Expand Down Expand Up @@ -249,6 +283,35 @@ static MeasurementXmlDto convertMeasurementToXmlDto(Measurement meas)
.build();
}

private static List<Measurement> convertMeasurementsFromXmlDto(MeasurementsXmlDto xmlDto) {
return xmlDto.getMeasurements().stream()
.map(MeasurementDao::convertMeasurementFromXmlDto)
.collect(toList());
}

static Measurement convertMeasurementFromXmlDto(MeasurementXmlDto dto) {
return new Measurement.Builder()
.withAgency(dto.getAgency())
.withAreaUnit(dto.getAreaUnit())
.withFlowUnit(dto.getFlowUnit())
.withHeightUnit(dto.getHeightUnit())
.withInstant(dto.getInstant())
.withId(new CwmsId.Builder()
.withName(dto.getLocationId())
.withOfficeId(dto.getOfficeId())
.build())
.withNumber(dto.getNumber())
.withParty(dto.getParty())
.withTempUnit(dto.getTempUnit())
.withUsed(dto.isUsed())
.withVelocityUnit(dto.getVelocityUnit())
.withStreamflowMeasurement(dto.getStreamflowMeasurement())
.withSupplementalStreamflowMeasurement(dto.getSupplementalStreamflowMeasurement())
.withUsgsMeasurement(dto.getUsgsMeasurement())
.withWmComments(dto.getWmComments())
.build();
}

private static XmlMapper buildXmlMapper() {
XmlMapper retVal = new XmlMapper();
retVal.registerModule(new JavaTimeModule());
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
package cwms.cda.data.dao;

import cwms.cda.helpers.DateUtils;
import static org.jooq.impl.DSL.asterisk;
import static org.jooq.impl.DSL.countDistinct;
import static org.jooq.impl.DSL.field;
Expand Down Expand Up @@ -557,12 +558,12 @@ public Catalog getTimeSeriesCatalog(String page, int pageSize, CatalogRequestPar
}

if (params.isIncludeExtents()) {
TimeSeriesExtents extents =
new TimeSeriesExtents(row.get(AV_TS_EXTENTS_UTC.VERSION_TIME),
row.get(AV_TS_EXTENTS_UTC.EARLIEST_TIME),
row.get(AV_TS_EXTENTS_UTC.LATEST_TIME),
row.get(AV_TS_EXTENTS_UTC.LAST_UPDATE)
);
TimeSeriesExtents extents = new TimeSeriesExtents.Builder()
.withEarliestTime(DateUtils.toZdt(row.get(AV_TS_EXTENTS_UTC.EARLIEST_TIME)))
.withLatestTime(DateUtils.toZdt(row.get(AV_TS_EXTENTS_UTC.LATEST_TIME)))
.withLastUpdate(DateUtils.toZdt(row.get(AV_TS_EXTENTS_UTC.LAST_UPDATE)))
.withVersionTime(DateUtils.toZdt(row.get(AV_TS_EXTENTS_UTC.VERSION_TIME)))
.build();
tsIdExtentMap.get(officeTsId).withExtent(extents);
}
});
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
/*
* MIT License
*
* Copyright (c) 2024 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.JsonInclude;
import com.fasterxml.jackson.databind.PropertyNamingStrategies;
import com.fasterxml.jackson.databind.annotation.JsonDeserialize;
import com.fasterxml.jackson.databind.annotation.JsonNaming;
import cwms.cda.formatters.Formats;
import cwms.cda.formatters.annotations.FormattableWith;
import cwms.cda.formatters.json.JsonV1;

@FormattableWith(contentType = Formats.JSONV1, formatter = JsonV1.class, aliases = {Formats.DEFAULT, Formats.JSON})
@JsonDeserialize(builder = CwmsIdTimeExtentsEntry.Builder.class)
@JsonInclude(JsonInclude.Include.NON_NULL)
@JsonNaming(PropertyNamingStrategies.KebabCaseStrategy.class)
public final class CwmsIdTimeExtentsEntry extends CwmsDTOBase{
private final CwmsId id;
private final TimeExtents timeExtents;

public CwmsIdTimeExtentsEntry(Builder builder) {
this.id = builder.id;
this.timeExtents = builder.timeExtents;
}

public CwmsId getId() {
return id;
}

public TimeExtents getTimeExtents() {
return timeExtents;
}

public static final class Builder {
private CwmsId id;
private TimeExtents timeExtents;

public Builder withId(CwmsId id) {
this.id = id;
return this;
}

public Builder withTimeExtents(TimeExtents timeExtents) {
this.timeExtents = timeExtents;
return this;
}

public CwmsIdTimeExtentsEntry build() {
return new CwmsIdTimeExtentsEntry(this);
}
}
}
Loading
Loading