Skip to content

Commit

Permalink
Merge pull request #890 from USACE/feature/CWDB-225_Measurement_contr…
Browse files Browse the repository at this point in the history
…oller

CWDB-225 - Implemented Measurement Controller
  • Loading branch information
rma-bryson authored Oct 19, 2024
2 parents 336d4df + 8aa8f87 commit e113280
Show file tree
Hide file tree
Showing 12 changed files with 909 additions and 278 deletions.
5 changes: 5 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 @@ -26,6 +26,7 @@


import static cwms.cda.api.Controllers.CONTRACT_NAME;
import static cwms.cda.api.Controllers.LOCATION_ID;
import static cwms.cda.api.Controllers.NAME;
import static cwms.cda.api.Controllers.OFFICE;
import static cwms.cda.api.Controllers.PROJECT_ID;
Expand Down Expand Up @@ -214,6 +215,7 @@
"/streams/*",
"/stream-locations/*",
"/stream-reaches/*",
"/measurements/*",
"/blobs/*",
"/clobs/*",
"/pools/*",
Expand Down Expand Up @@ -538,6 +540,9 @@ protected void configureRoutes() {
new StreamLocationController(metrics), requiredRoles,5, TimeUnit.MINUTES);
cdaCrudCache(format("/stream-reaches/{%s}", NAME),
new StreamReachController(metrics), requiredRoles,1, TimeUnit.DAYS);
String measurements = "/measurements/";
cdaCrudCache(format(measurements + "{%s}", LOCATION_ID),
new cwms.cda.api.MeasurementController(metrics), requiredRoles,5, TimeUnit.MINUTES);
cdaCrudCache("/blobs/{blob-id}",
new BlobController(metrics), requiredRoles,5, TimeUnit.MINUTES);
cdaCrudCache("/clobs/{clob-id}",
Expand Down
19 changes: 19 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 @@ -134,6 +134,15 @@ public final class Controllers {
public static final String ISSUE_DATE = "issue-date";
public static final String LOCATION_KIND_LIKE = "location-kind-like";
public static final String LOCATION_TYPE_LIKE = "location-type-like";
public static final String MIN_NUMBER = "min-number";
public static final String MAX_NUMBER = "max-number";
public static final String MIN_HEIGHT = "min-height";
public static final String MAX_HEIGHT = "max-height";
public static final String MIN_FLOW = "min-flow";
public static final String MAX_FLOW = "max-flow";
public static final String AGENCY = "agency";
public static final String QUALITY = "quality";


public static final String GROUP_ID = "group-id";
public static final String REPLACE_ASSIGNED_LOCS = "replace-assigned-locs";
Expand Down Expand Up @@ -372,6 +381,16 @@ public static <T> T requiredParamAs(io.javalin.http.Context ctx, String name, Cl
.getOrThrow(e -> new RequiredQueryParameterException(name));
}

@Nullable
public static Double queryParamAsDouble(Context ctx, String param) {
Double retVal = null;
String numberStr = ctx.queryParam(param);
if (numberStr != null) {
retVal = Double.parseDouble(numberStr);
}
return retVal;
}

@Nullable
public static ZonedDateTime queryParamAsZdt(Context ctx, String param, String timezone) {
ZonedDateTime beginZdt = null;
Expand Down
267 changes: 267 additions & 0 deletions cwms-data-api/src/main/java/cwms/cda/api/MeasurementController.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,267 @@
/*
* 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.AGENCY;
import static cwms.cda.api.Controllers.BEGIN;
import static cwms.cda.api.Controllers.CREATE;
import static cwms.cda.api.Controllers.DATE_FORMAT;
import static cwms.cda.api.Controllers.DELETE;
import static cwms.cda.api.Controllers.EXAMPLE_DATE;
import static cwms.cda.api.Controllers.FAIL_IF_EXISTS;
import static cwms.cda.api.Controllers.GET_ALL;
import static cwms.cda.api.Controllers.GET_ONE;
import static cwms.cda.api.Controllers.ID_MASK;
import static cwms.cda.api.Controllers.LOCATION_ID;
import static cwms.cda.api.Controllers.END;
import static cwms.cda.api.Controllers.MAX_FLOW;
import static cwms.cda.api.Controllers.MAX_HEIGHT;
import static cwms.cda.api.Controllers.MIN_FLOW;
import static cwms.cda.api.Controllers.MIN_HEIGHT;
import static cwms.cda.api.Controllers.NOT_SUPPORTED_YET;
import static cwms.cda.api.Controllers.MIN_NUMBER;
import static cwms.cda.api.Controllers.MAX_NUMBER;
import static cwms.cda.api.Controllers.OFFICE;
import static cwms.cda.api.Controllers.OFFICE_MASK;
import static cwms.cda.api.Controllers.QUALITY;
import static cwms.cda.api.Controllers.TIMEZONE;
import static cwms.cda.api.Controllers.UNIT_SYSTEM;
import static cwms.cda.api.Controllers.queryParamAsDouble;
import static cwms.cda.api.Controllers.queryParamAsInstant;
import static cwms.cda.api.Controllers.requiredParam;
import cwms.cda.api.enums.UnitSystem;
import cwms.cda.data.dao.MeasurementDao;
import cwms.cda.data.dto.measurement.Measurement;
import cwms.cda.formatters.ContentType;
import cwms.cda.formatters.Formats;
import io.javalin.apibuilder.CrudHandler;
import io.javalin.core.util.Header;
import io.javalin.http.Context;
import io.javalin.plugin.openapi.annotations.HttpMethod;
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.OpenApiRequestBody;
import io.javalin.plugin.openapi.annotations.OpenApiResponse;
import java.time.Instant;
import org.jetbrains.annotations.NotNull;
import org.jooq.DSLContext;

import javax.servlet.http.HttpServletResponse;
import java.util.List;

import static cwms.cda.data.dao.JooqDao.getDslContext;

public final class MeasurementController implements CrudHandler {

static final String TAG = "Measurements";

private final MetricRegistry metrics;
private final Histogram requestResultSize;

public MeasurementController(MetricRegistry metrics) {
this.metrics = metrics;
String className = this.getClass().getName();
requestResultSize = this.metrics.histogram(name(className, "results", "size"));
}

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

@OpenApi(
queryParams = {
@OpenApiParam(name = OFFICE_MASK, description = "Office id mask for filtering measurements. Use null to retrieve measurements for all offices."),
@OpenApiParam(name = ID_MASK, description = "Location id mask for filtering measurements. Use null to retrieve measurements for all locations."),
@OpenApiParam(name = MIN_NUMBER, description = "Minimum measurement number-id for filtering measurements."),
@OpenApiParam(name = MAX_NUMBER, description = "Maximum measurement number-id for filtering measurements."),
@OpenApiParam(name = BEGIN, description = "The start of the time "
+ "window to delete. The format for this field is ISO 8601 extended, with "
+ "optional offset and timezone, i.e., '" + DATE_FORMAT + "', e.g., '"
+ EXAMPLE_DATE + "'. A null value is treated as an unbounded start."),
@OpenApiParam(name = END, description = "The end of the time "
+ "window to delete.The format for this field is ISO 8601 extended, with "
+ "optional offset and timezone, i.e., '" + DATE_FORMAT + "', e.g., '"
+ EXAMPLE_DATE + "'.A null value is treated as an unbounded end."),
@OpenApiParam(name = TIMEZONE, description = "This field specifies a default timezone "
+ "to be used if the format of the " + BEGIN + "and " + END
+ " parameters do not include offset or time zone information. "
+ "Defaults to UTC."),
@OpenApiParam(name = MIN_HEIGHT, description = "Minimum height for filtering measurements."),
@OpenApiParam(name = MAX_HEIGHT, description = "Maximum height for filtering measurements."),
@OpenApiParam(name = MIN_FLOW, description = "Minimum flow for filtering measurements."),
@OpenApiParam(name = MAX_FLOW, description = "Maximum flow for filtering measurements."),
@OpenApiParam(name = AGENCY, description = "Agencies for filtering measurements."),
@OpenApiParam(name = QUALITY, description = "Quality for filtering measurements."),
@OpenApiParam(name = UNIT_SYSTEM, description = "Specifies the unit system"
+ " of the response. Valid values for the unit field are: "
+ "\n* `EN` Specifies English unit system. Location values will be in the "
+ "default English units for their parameters."
+ "\n* `SI` Specifies the SI unit system. Location values will be in the "
+ "default SI units for their parameters. If not specified, EN is used.")
},
responses = {
@OpenApiResponse(status = "200", content = {
@OpenApiContent(isArray = true, type = Formats.JSONV1, from = Measurement.class),
@OpenApiContent(isArray = true, type = Formats.JSON, from = Measurement.class)
})
},
description = "Returns matching measurement data.",
tags = {TAG}
)
@Override
public void getAll(@NotNull Context ctx) {
String officeId = ctx.queryParam(OFFICE_MASK);
String locationId = ctx.queryParam(ID_MASK);
String unitSystem = ctx.queryParamAsClass(UNIT_SYSTEM, String.class).getOrDefault(UnitSystem.EN.value());
Instant minDate = queryParamAsInstant(ctx, BEGIN);
Instant maxDate = queryParamAsInstant(ctx, END);
String minNum = ctx.queryParam(MIN_NUMBER);
String maxNum = ctx.queryParam(MAX_NUMBER);
Number minHeight = queryParamAsDouble(ctx, MIN_HEIGHT);
Number maxHeight = queryParamAsDouble(ctx, MAX_HEIGHT);
Number minFlow = queryParamAsDouble(ctx, MIN_FLOW);
Number maxFlow = queryParamAsDouble(ctx, MAX_FLOW);
String agency = ctx.queryParam(AGENCY);
String quality = ctx.queryParam(QUALITY);
try (Timer.Context ignored = markAndTime(GET_ALL)) {
DSLContext dsl = getDslContext(ctx);
MeasurementDao dao = new MeasurementDao(dsl);
List<Measurement> measurements = dao.retrieveMeasurements(officeId, locationId, minDate, maxDate, unitSystem,
minHeight, maxHeight, minFlow, maxFlow, minNum, maxNum, agency, quality);
String formatHeader = ctx.header(Header.ACCEPT);
ContentType contentType = Formats.parseHeader(formatHeader, Measurement.class);
ctx.contentType(contentType.toString());
String serialized = Formats.format(contentType, measurements, Measurement.class);
ctx.result(serialized);
ctx.status(HttpServletResponse.SC_OK);
requestResultSize.update(serialized.length());
}
}

@OpenApi(ignore = true)
@Override
public void getOne(@NotNull Context ctx, @NotNull String locationId) {
try (final Timer.Context ignored = markAndTime(GET_ONE)) {
throw new UnsupportedOperationException(NOT_SUPPORTED_YET);
}

}

@OpenApi(
requestBody = @OpenApiRequestBody(
content = {
@OpenApiContent(isArray = true, from = Measurement.class, type = Formats.JSONV1),
@OpenApiContent(isArray = true, from = Measurement.class, type = Formats.JSON)
},
required = true),
queryParams = {
@OpenApiParam(name = FAIL_IF_EXISTS, type = Boolean.class,
description = "Create will fail if provided Measurement(s) already exist. Default: true")
},
description = "Create new measurement(s).",
method = HttpMethod.POST,
tags = {TAG},
responses = {
@OpenApiResponse(status = "204", description = "Measurement(s) successfully stored.")
}
)
@Override
public void create(Context ctx) {

try (Timer.Context ignored = markAndTime(CREATE)) {
String formatHeader = ctx.req.getContentType();
ContentType contentType = Formats.parseHeader(formatHeader, Measurement.class);
List<Measurement> measurements = Formats.parseContentList(contentType, ctx.body(), Measurement.class);
boolean failIfExists = ctx.queryParamAsClass(FAIL_IF_EXISTS, Boolean.class).getOrDefault(true);
DSLContext dsl = getDslContext(ctx);
MeasurementDao dao = new MeasurementDao(dsl);
dao.storeMeasurements(measurements, failIfExists);
String statusMsg = "Created Measurement";
if(measurements.size() > 1)
{
statusMsg += "s";
}
ctx.status(HttpServletResponse.SC_CREATED).json(statusMsg);
}
}

@OpenApi(ignore = true)
@Override
public void update(@NotNull Context ctx, @NotNull String locationId) {
try (final Timer.Context ignored = markAndTime(GET_ONE)) {
throw new UnsupportedOperationException(NOT_SUPPORTED_YET);
}
}

@OpenApi(
pathParams = {
@OpenApiParam(name = LOCATION_ID, description = "Specifies the location-id of "
+ "the measurement(s) to be deleted."),
},
queryParams = {
@OpenApiParam(name = OFFICE, required = true, description = "Specifies the office of the measurements to delete"),
@OpenApiParam(name = BEGIN, required = true, description = "The start of the time "
+ "window to delete. The format for this field is ISO 8601 extended, with "
+ "optional offset and timezone, i.e., '" + DATE_FORMAT + "', e.g., '"
+ EXAMPLE_DATE + "'."),
@OpenApiParam(name = END, required = true, description = "The end of the time "
+ "window to delete.The format for this field is ISO 8601 extended, with "
+ "optional offset and timezone, i.e., '" + DATE_FORMAT + "', e.g., '"
+ EXAMPLE_DATE + "'."),
@OpenApiParam(name = TIMEZONE, description = "This field specifies a default timezone "
+ "to be used if the format of the " + BEGIN + "and " + END
+ " parameters do not include offset or time zone information. "
+ "Defaults to UTC."),
@OpenApiParam(name = MIN_NUMBER, description = "Specifies the min number-id of the measurement to delete."),
@OpenApiParam(name = MAX_NUMBER, description = "Specifies the max number-id of the measurement to delete."),
},
description = "Delete an existing measurement.",
method = HttpMethod.DELETE,
tags = {TAG},
responses = {
@OpenApiResponse(status = "204", description = "Measurement successfully deleted."),
@OpenApiResponse(status = "404", description = "Measurement not found.")
}
)
@Override
public void delete(@NotNull Context ctx, @NotNull String locationId) {
String officeId = requiredParam(ctx, OFFICE);
String minNum = ctx.queryParam(MIN_NUMBER);
String maxNum = ctx.queryParam(MAX_NUMBER);
Instant minDate = queryParamAsInstant(ctx, BEGIN);
Instant maxDate = queryParamAsInstant(ctx, END);
try (Timer.Context ignored = markAndTime(DELETE)) {
DSLContext dsl = getDslContext(ctx);
MeasurementDao dao = new MeasurementDao(dsl);
dao.deleteMeasurements(officeId, locationId, minDate, maxDate,minNum, maxNum);
ctx.status(HttpServletResponse.SC_NO_CONTENT).json( "Measurements for " + locationId + " Deleted");
}
}

}
Loading

0 comments on commit e113280

Please sign in to comment.