diff --git a/cwms-data-api/src/main/java/cwms/cda/api/BasinController.java b/cwms-data-api/src/main/java/cwms/cda/api/BasinController.java index 65631c358..c8bdf4f4a 100644 --- a/cwms-data-api/src/main/java/cwms/cda/api/BasinController.java +++ b/cwms-data-api/src/main/java/cwms/cda/api/BasinController.java @@ -193,7 +193,7 @@ public void getOne(@NotNull Context ctx, @NotNull String name) { String units = ctx.queryParamAsClass(UNIT, String.class).getOrDefault(UnitSystem.EN.value()); - String office = ctx.queryParam(OFFICE); + String office = requiredParam(ctx, OFFICE); String formatHeader = ctx.header(Header.ACCEPT); ContentType contentType = Formats.parseHeader(formatHeader, Basin.class); ctx.contentType(contentType.toString()); @@ -303,7 +303,7 @@ public void delete(@NotNull Context ctx, @NotNull String name) { cwms.cda.data.dao.basin.BasinDao basinDao = new cwms.cda.data.dao.basin.BasinDao(dsl); CwmsId basinId = new CwmsId.Builder() .withName(name) - .withOfficeId(ctx.queryParam(OFFICE)) + .withOfficeId(requiredParam(ctx, OFFICE)) .build(); basinDao.deleteBasin(basinId, deleteMethod.getRule()); StatusResponse re = new StatusResponse(basinId.getOfficeId(), "Deleted CWMS Basin", basinId.getName()); diff --git a/cwms-data-api/src/main/java/cwms/cda/api/BinaryTimeSeriesValueController.java b/cwms-data-api/src/main/java/cwms/cda/api/BinaryTimeSeriesValueController.java index cffac4407..798bb5806 100644 --- a/cwms-data-api/src/main/java/cwms/cda/api/BinaryTimeSeriesValueController.java +++ b/cwms-data-api/src/main/java/cwms/cda/api/BinaryTimeSeriesValueController.java @@ -64,7 +64,8 @@ public BinaryTimeSeriesValueController(MetricRegistry metrics) { @OpenApiParam(name = OFFICE, required = true, description = "Specifies the owning office of " + "the Binary TimeSeries whose data is to be included in the response."), @OpenApiParam(name = BLOB_ID, description = "Will be removed in a schema update. " + - "This is a placeholder for integration testing with schema 23.3.16", deprecated = true) + "This is a placeholder for integration testing with schema 23.3.16", deprecated = true, + required = true) }, responses = { @OpenApiResponse(status = STATUS_200, diff --git a/cwms-data-api/src/main/java/cwms/cda/api/EmbankmentController.java b/cwms-data-api/src/main/java/cwms/cda/api/EmbankmentController.java index 1db14e616..12da35431 100644 --- a/cwms-data-api/src/main/java/cwms/cda/api/EmbankmentController.java +++ b/cwms-data-api/src/main/java/cwms/cda/api/EmbankmentController.java @@ -90,7 +90,7 @@ private Timer.Context markAndTime(String subject) { @Override public void getAll(Context ctx) { String office = ctx.queryParam(OFFICE); - String projectId = ctx.queryParam(PROJECT_ID); + String projectId = requiredParam(ctx, PROJECT_ID); try (Timer.Context ignored = markAndTime(GET_ALL)) { DSLContext dsl = getDslContext(ctx); EmbankmentDao dao = new EmbankmentDao(dsl); diff --git a/cwms-data-api/src/main/java/cwms/cda/api/LevelsController.java b/cwms-data-api/src/main/java/cwms/cda/api/LevelsController.java index 5ca14bfa4..f6ec6f48b 100644 --- a/cwms-data-api/src/main/java/cwms/cda/api/LevelsController.java +++ b/cwms-data-api/src/main/java/cwms/cda/api/LevelsController.java @@ -55,6 +55,7 @@ import cwms.cda.formatters.FormattingException; import cwms.cda.formatters.UnsupportedFormatException; import cwms.cda.helpers.DateUtils; +import cwms.cda.helpers.annotations.IgnoreRequiredQueryParamMismatch; import io.javalin.apibuilder.CrudHandler; import io.javalin.core.util.Header; import io.javalin.http.Context; @@ -378,6 +379,7 @@ public void getAll(@NotNull Context ctx) { description = "Retrieves requested Location Level", tags = TAG ) + @IgnoreRequiredQueryParamMismatch(parameterNames = {EFFECTIVE_DATE}) @Override public void getOne(@NotNull Context ctx, @NotNull String levelId) { String office = requiredParam(ctx, OFFICE); diff --git a/cwms-data-api/src/main/java/cwms/cda/api/LocationGroupController.java b/cwms-data-api/src/main/java/cwms/cda/api/LocationGroupController.java index 31f2d5815..7d9ffb79b 100644 --- a/cwms-data-api/src/main/java/cwms/cda/api/LocationGroupController.java +++ b/cwms-data-api/src/main/java/cwms/cda/api/LocationGroupController.java @@ -86,10 +86,10 @@ private Timer.Context markAndTime(String subject) { + " the assigned locations in the returned location groups. (default: false)"), @OpenApiParam(name = LOCATION_CATEGORY_LIKE, description = "Posix regular expression " + "matching against the location category id"), - @OpenApiParam(name = CATEGORY_OFFICE_ID, required = true, description = "Specifies the " + @OpenApiParam(name = CATEGORY_OFFICE_ID, description = "Specifies the " + "owning office of the category the location group belongs to " + "whose data is to be included in the response."), - @OpenApiParam(name = LOCATION_OFFICE_ID, required = true, description = "Specifies the " + @OpenApiParam(name = LOCATION_OFFICE_ID, description = "Specifies the " + "owning office of the location assigned to the location group whose data is to be included in the response."), }, responses = { @@ -108,8 +108,8 @@ public void getAll(@NotNull Context ctx) { DSLContext dsl = getDslContext(ctx); LocationGroupDao cdm = new LocationGroupDao(dsl); - String groupOfficeId = requiredParam(ctx, OFFICE); - String categoryOfficeId = requiredParam(ctx, CATEGORY_OFFICE_ID); + String groupOfficeId = ctx.queryParam(OFFICE); + String categoryOfficeId = ctx.queryParam(CATEGORY_OFFICE_ID); String locationOfficeId = ctx.queryParam(LOCATION_OFFICE_ID); boolean includeAssigned = queryParamAsClass(ctx, new String[]{INCLUDE_ASSIGNED}, @@ -152,11 +152,12 @@ Boolean.class, false, metrics, name(LocationGroupController.class.getName(), @OpenApiParam(name = OFFICE, required = true, description = "Specifies the " + "owning office of the location group whose data is to be included " + "in the response."), - @OpenApiParam(name = GROUP_OFFICE_ID, required = true, description = "Specifies the " - + "owning office of the location group whose data is to be included in the response."), - @OpenApiParam(name = CATEGORY_OFFICE_ID, required = true, description = "Specifies the " + @OpenApiParam(name = GROUP_OFFICE_ID, description = "Specifies the " + + "owning office of the location group whose data is to be included in the response. " + + "Required for GEO JSON format."), + @OpenApiParam(name = CATEGORY_OFFICE_ID, description = "Specifies the " + "owning office of the category the location group belongs to " - + "whose data is to be included in the response."), + + "whose data is to be included in the response. Required for GEO JSON format."), @OpenApiParam(name = CATEGORY_ID, required = true, description = "Specifies" + " the category containing the location group whose data is to be " + "included in the response."), @@ -325,8 +326,8 @@ public void delete(@NotNull Context ctx, @NotNull String groupId) { DSLContext dsl = getDslContext(ctx); LocationGroupDao dao = new LocationGroupDao(dsl); - String office = ctx.queryParam(OFFICE); - String categoryId = ctx.queryParam(CATEGORY_ID); + String office = requiredParam(ctx, OFFICE); + String categoryId = requiredParam(ctx, CATEGORY_ID); boolean cascadeDelete = ctx.queryParamAsClass(CASCADE_DELETE, Boolean.class).getOrDefault(false); dao.delete(categoryId, groupId, cascadeDelete, office); ctx.status(HttpServletResponse.SC_NO_CONTENT); diff --git a/cwms-data-api/src/main/java/cwms/cda/api/MeasurementController.java b/cwms-data-api/src/main/java/cwms/cda/api/MeasurementController.java index be7ea5b71..92b48e6e9 100644 --- a/cwms-data-api/src/main/java/cwms/cda/api/MeasurementController.java +++ b/cwms-data-api/src/main/java/cwms/cda/api/MeasurementController.java @@ -216,9 +216,9 @@ public void update(@NotNull Context ctx, @NotNull String locationId) { }, 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. " + + @OpenApiParam(name = BEGIN, description = "The start of the time window to delete. " + TIME_FORMAT_DESC), - @OpenApiParam(name = END, required = true, description = "The end of the time window to delete." + + @OpenApiParam(name = END, description = "The end of the time window to delete." + TIME_FORMAT_DESC), @OpenApiParam(name = TIMEZONE, description = "This field specifies a default timezone " + "to be used if the format of the " + BEGIN + "and " + END diff --git a/cwms-data-api/src/main/java/cwms/cda/api/PoolController.java b/cwms-data-api/src/main/java/cwms/cda/api/PoolController.java index 2841cdc2b..6905608bf 100644 --- a/cwms-data-api/src/main/java/cwms/cda/api/PoolController.java +++ b/cwms-data-api/src/main/java/cwms/cda/api/PoolController.java @@ -14,6 +14,7 @@ import static cwms.cda.api.Controllers.OFFICE; import static cwms.cda.api.Controllers.PAGE; import static cwms.cda.api.Controllers.PAGE_SIZE; +import static cwms.cda.api.Controllers.PARAMETER_ID; import static cwms.cda.api.Controllers.POOL_ID; import static cwms.cda.api.Controllers.PROJECT_ID; import static cwms.cda.api.Controllers.RESULTS; @@ -23,6 +24,7 @@ import static cwms.cda.api.Controllers.STATUS_501; import static cwms.cda.api.Controllers.TOP_MASK; import static cwms.cda.api.Controllers.queryParamAsClass; +import static cwms.cda.api.Controllers.requiredParam; import static cwms.cda.data.dao.JooqDao.getDslContext; import com.codahale.metrics.Histogram; @@ -190,8 +192,8 @@ public void getOne(@NotNull Context ctx, @NotNull String poolId) { PoolDao dao = new PoolDao(dsl); // These are required - String office = ctx.queryParam(OFFICE); - String projectId = ctx.queryParam(PROJECT_ID); + String office = requiredParam(ctx, OFFICE);; + String projectId = requiredParam(ctx, PROJECT_ID); // These are optional String bottomMask = diff --git a/cwms-data-api/src/main/java/cwms/cda/api/ProjectController.java b/cwms-data-api/src/main/java/cwms/cda/api/ProjectController.java index 62c92d9e7..c5e592d76 100644 --- a/cwms-data-api/src/main/java/cwms/cda/api/ProjectController.java +++ b/cwms-data-api/src/main/java/cwms/cda/api/ProjectController.java @@ -231,8 +231,8 @@ public void create(@NotNull Context ctx) { @OpenApiParam(name = NAME, description = "The name of the project to be renamed"), }, queryParams = { - @OpenApiParam(name = NAME, description = "The new name of the project"), - @OpenApiParam(name = OFFICE, description = "The office of the project to be renamed"), + @OpenApiParam(name = NAME, required = true, description = "The new name of the project"), + @OpenApiParam(name = OFFICE, required = true, description = "The office of the project to be renamed"), }, requestBody = @OpenApiRequestBody( content = { diff --git a/cwms-data-api/src/main/java/cwms/cda/api/SpecifiedLevelController.java b/cwms-data-api/src/main/java/cwms/cda/api/SpecifiedLevelController.java index 550c9efda..31638a5d4 100644 --- a/cwms-data-api/src/main/java/cwms/cda/api/SpecifiedLevelController.java +++ b/cwms-data-api/src/main/java/cwms/cda/api/SpecifiedLevelController.java @@ -163,7 +163,7 @@ public void create(Context ctx) { queryParams = { @OpenApiParam(name = OFFICE, required = true, description = "Specifies the " + "owning office of the specified level to be renamed"), - @OpenApiParam(name = SPECIFIED_LEVEL_ID, description = "The new specified level id.") + @OpenApiParam(name = SPECIFIED_LEVEL_ID, required = true, description = "The new specified level id.") }, method = HttpMethod.PATCH, tags = {TAG} @@ -174,8 +174,8 @@ public void update(Context ctx, @NotNull String oldSpecifiedLevelId) { DSLContext dsl = getDslContext(ctx); SpecifiedLevelDao dao = getDao(dsl); - String newSpecifiedLevelId = ctx.queryParam(SPECIFIED_LEVEL_ID); - String office = ctx.queryParam(OFFICE); + String newSpecifiedLevelId = requiredParam(ctx, SPECIFIED_LEVEL_ID); + String office = requiredParam(ctx, OFFICE); dao.update(oldSpecifiedLevelId, newSpecifiedLevelId, office); ctx.status(HttpServletResponse.SC_NO_CONTENT); } @@ -201,7 +201,7 @@ public void delete(Context ctx, String specifiedLevelId) { DSLContext dsl = getDslContext(ctx); SpecifiedLevelDao dao = getDao(dsl); - String office = ctx.queryParam(OFFICE); + String office = requiredParam(ctx, OFFICE); dao.delete(specifiedLevelId, office); ctx.status(HttpServletResponse.SC_NO_CONTENT); } diff --git a/cwms-data-api/src/main/java/cwms/cda/api/StandardTextController.java b/cwms-data-api/src/main/java/cwms/cda/api/StandardTextController.java index 0038a39f7..bdf3d737a 100644 --- a/cwms-data-api/src/main/java/cwms/cda/api/StandardTextController.java +++ b/cwms-data-api/src/main/java/cwms/cda/api/StandardTextController.java @@ -133,10 +133,7 @@ public void getAll(Context ctx) { @Override public void getOne(@NotNull Context ctx, @NotNull String stdTextId) { try (Timer.Context ignored = markAndTime(DELETE)) { - String office = ctx.queryParam(OFFICE); - if (office == null) { - throw new IllegalArgumentException(OFFICE + " is a required parameter"); - } + String office = requiredParam(ctx, OFFICE); DSLContext dsl = getDslContext(ctx); StandardTextValue standardTextValue = getDao(dsl).retrieveStandardText(stdTextId, office); @@ -204,12 +201,8 @@ public void update(@NotNull Context ctx, @NotNull String oldTextTimeSeriesId) { @Override public void delete(@NotNull Context ctx, @NotNull String stdTextId) { try (Timer.Context ignored = markAndTime(DELETE)) { - String office = ctx.queryParam(OFFICE); - if (office == null) { - throw new IllegalArgumentException(OFFICE + " is a required parameter"); - } - JooqDao.DeleteMethod deleteMethod = ctx.queryParamAsClass(METHOD, JooqDao.DeleteMethod.class) - .getOrThrow(e -> new IllegalArgumentException(METHOD + " is a required parameter")); + String office = requiredParam(ctx, OFFICE); + JooqDao.DeleteMethod deleteMethod = requiredParamAs(ctx, METHOD, JooqDao.DeleteMethod.class); String deleteAction; switch (deleteMethod) { case DELETE_ALL: diff --git a/cwms-data-api/src/main/java/cwms/cda/api/TextTimeSeriesValueController.java b/cwms-data-api/src/main/java/cwms/cda/api/TextTimeSeriesValueController.java index 5203b105b..1f3cd343c 100644 --- a/cwms-data-api/src/main/java/cwms/cda/api/TextTimeSeriesValueController.java +++ b/cwms-data-api/src/main/java/cwms/cda/api/TextTimeSeriesValueController.java @@ -59,7 +59,8 @@ public TextTimeSeriesValueController(MetricRegistry metrics) { @OpenApiParam(name = OFFICE, required = true, description = "Specifies the owning office of " + "the Text TimeSeries whose data is to be included in the response."), @OpenApiParam(name = CLOB_ID, description = "Will be removed in a schema update. " + - "This is a placeholder for integration testing with schema 23.3.16", deprecated = true) + "This is a placeholder for integration testing with schema 23.3.16", deprecated = true, + required = true) }, responses = { @OpenApiResponse(status = STATUS_200, diff --git a/cwms-data-api/src/main/java/cwms/cda/api/TimeSeriesFilteredController.java b/cwms-data-api/src/main/java/cwms/cda/api/TimeSeriesFilteredController.java index da8f26eef..3393c2389 100644 --- a/cwms-data-api/src/main/java/cwms/cda/api/TimeSeriesFilteredController.java +++ b/cwms-data-api/src/main/java/cwms/cda/api/TimeSeriesFilteredController.java @@ -214,7 +214,7 @@ public void handle(@NotNull Context ctx) { ? DateUtils.parseUserDate(end, timezone) : ZonedDateTime.now(tz); - String office = requiredParam(ctx, OFFICE); + String office = ctx.queryParam(OFFICE); FilteredTimeSeriesParameters ftsParams = FilteredTimeSeriesParameters.Builder.from(ctx) .build(); diff --git a/cwms-data-api/src/main/java/cwms/cda/api/TimeSeriesGroupController.java b/cwms-data-api/src/main/java/cwms/cda/api/TimeSeriesGroupController.java index 708629088..2eccdb7f8 100644 --- a/cwms-data-api/src/main/java/cwms/cda/api/TimeSeriesGroupController.java +++ b/cwms-data-api/src/main/java/cwms/cda/api/TimeSeriesGroupController.java @@ -98,7 +98,7 @@ private Timer.Context markAndTime(String subject) { + "timeseries assigned to the group(s) whose data is to be included in the response. If this " + "field is not specified, group information for all assigned TS offices shall be returned."), @OpenApiParam(name = GROUP_OFFICE_ID, description = "Specifies the owning office of the " - + "timeseries group", required = true), + + "timeseries group"), @OpenApiParam(name = INCLUDE_ASSIGNED, type = Boolean.class, description = "Include" + " the assigned timeseries in the returned timeseries groups. (default: true)"), @OpenApiParam(name = TIMESERIES_CATEGORY_LIKE, description = "Posix regular expression " @@ -163,15 +163,15 @@ Boolean.class, true, metrics, name(TimeSeriesGroupController.class.getName(), + "the timeseries group whose data is to be included in the response") }, queryParams = { - @OpenApiParam(name = OFFICE, required = true, description = "Specifies the " + @OpenApiParam(name = OFFICE, description = "Specifies the " + "owning office of the timeseries assigned to the group whose data is to be included" + " in the response. This will limit the assigned timeseries returned to only those" + " assigned to the specified office."), @OpenApiParam(name = CATEGORY_OFFICE_ID, description = "Specifies the owning office of the " - + "timeseries group category", required = true), + + "timeseries group category"), @OpenApiParam(name = GROUP_OFFICE_ID, description = "Specifies the owning office of the " - + "timeseries group", required = true), - @OpenApiParam(name = CATEGORY_ID, required = true, description = "Specifies" + + "timeseries group"), + @OpenApiParam(name = CATEGORY_ID, description = "Specifies" + " the category containing the timeseries group whose data is to be " + "included in the response."), }, @@ -337,8 +337,8 @@ public void delete(@NotNull Context ctx, @NotNull String groupId) { DSLContext dsl = getDslContext(ctx); TimeSeriesGroupDao dao = new TimeSeriesGroupDao(dsl); - String office = ctx.queryParam(OFFICE); - String categoryId = ctx.queryParam(CATEGORY_ID); + String office = requiredParam(ctx, OFFICE); + String categoryId = requiredParam(ctx, CATEGORY_ID); dao.delete(categoryId, groupId, office); ctx.status(HttpServletResponse.SC_NO_CONTENT); } diff --git a/cwms-data-api/src/main/java/cwms/cda/api/TimeSeriesIdentifierDescriptorController.java b/cwms-data-api/src/main/java/cwms/cda/api/TimeSeriesIdentifierDescriptorController.java index 5081e6ab2..142a430c6 100644 --- a/cwms-data-api/src/main/java/cwms/cda/api/TimeSeriesIdentifierDescriptorController.java +++ b/cwms-data-api/src/main/java/cwms/cda/api/TimeSeriesIdentifierDescriptorController.java @@ -180,7 +180,7 @@ public void getOne(@NotNull Context ctx, @NotNull String timeseriesId) { DSLContext dsl = getDslContext(ctx); TimeSeriesIdentifierDescriptorDao dao = new TimeSeriesIdentifierDescriptorDao(dsl); - String office = ctx.queryParam(OFFICE); + String office = requiredParam(ctx, OFFICE); String formatHeader = ctx.header(Header.ACCEPT); if (Formats.DEFAULT.equals(formatHeader)) { @@ -329,7 +329,7 @@ public void update(@NotNull Context ctx, @NotNull String name) { @Override public void delete(@NotNull Context ctx, @NotNull String timeseriesId) { - JooqDao.DeleteMethod method = ctx.queryParamAsClass(METHOD, JooqDao.DeleteMethod.class).get(); + JooqDao.DeleteMethod method =requiredParamAs(ctx, METHOD, JooqDao.DeleteMethod.class); String office = requiredParam(ctx, OFFICE); diff --git a/cwms-data-api/src/main/java/cwms/cda/api/TurbineChangesDeleteController.java b/cwms-data-api/src/main/java/cwms/cda/api/TurbineChangesDeleteController.java index 597a0ad4f..15cbf4ca9 100644 --- a/cwms-data-api/src/main/java/cwms/cda/api/TurbineChangesDeleteController.java +++ b/cwms-data-api/src/main/java/cwms/cda/api/TurbineChangesDeleteController.java @@ -26,29 +26,22 @@ import static com.codahale.metrics.MetricRegistry.name; import static cwms.cda.api.Controllers.*; -import static cwms.cda.api.Controllers.SINCE; import static cwms.cda.data.dao.JooqDao.getDslContext; import com.codahale.metrics.Histogram; import com.codahale.metrics.MetricRegistry; import com.codahale.metrics.Timer; -import cwms.cda.api.enums.UnitSystem; import cwms.cda.data.dao.location.kind.TurbineDao; import cwms.cda.data.dto.CwmsId; import cwms.cda.data.dto.StatusResponse; -import cwms.cda.data.dto.location.kind.TurbineChange; -import cwms.cda.formatters.ContentType; -import cwms.cda.formatters.Formats; -import io.javalin.core.util.Header; +import cwms.cda.helpers.annotations.IgnoreRequiredQueryParamMismatch; import io.javalin.http.Context; import io.javalin.http.Handler; 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.OpenApiResponse; import java.time.Instant; -import java.util.List; import javax.servlet.http.HttpServletResponse; import org.jetbrains.annotations.NotNull; import org.jooq.DSLContext; @@ -99,6 +92,7 @@ private Timer.Context markAndTime(String subject) { + "inputs provided the project was not found.") } ) + @IgnoreRequiredQueryParamMismatch(parameterNames = {TIMEZONE}) public void handle(@NotNull Context ctx) throws Exception { String projectId = ctx.pathParam(NAME); String office = ctx.pathParam(OFFICE); diff --git a/cwms-data-api/src/main/java/cwms/cda/api/TurbineChangesGetController.java b/cwms-data-api/src/main/java/cwms/cda/api/TurbineChangesGetController.java index e8d3b936d..870608d0e 100644 --- a/cwms-data-api/src/main/java/cwms/cda/api/TurbineChangesGetController.java +++ b/cwms-data-api/src/main/java/cwms/cda/api/TurbineChangesGetController.java @@ -32,22 +32,18 @@ import com.codahale.metrics.MetricRegistry; import com.codahale.metrics.Timer; import cwms.cda.api.enums.UnitSystem; -import cwms.cda.api.errors.CdaError; -import cwms.cda.api.errors.RequiredQueryParameterException; import cwms.cda.data.dao.location.kind.TurbineDao; import cwms.cda.data.dto.CwmsId; import cwms.cda.data.dto.location.kind.TurbineChange; import cwms.cda.formatters.ContentType; import cwms.cda.formatters.Formats; -import io.javalin.apibuilder.CrudHandler; +import cwms.cda.helpers.annotations.IgnoreRequiredQueryParamMismatch; import io.javalin.core.util.Header; import io.javalin.http.Context; import io.javalin.http.Handler; -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 java.util.List; @@ -116,6 +112,7 @@ private Timer.Context markAndTime(String subject) { description = "Returns matching CWMS Turbine Change Data for a Reservoir Project.", tags = {TurbineController.TAG} ) + @IgnoreRequiredQueryParamMismatch(parameterNames = {TIMEZONE}) public void handle(@NotNull Context ctx) throws Exception { String projectId = ctx.pathParam(NAME); String office = ctx.pathParam(OFFICE); diff --git a/cwms-data-api/src/main/java/cwms/cda/api/location/kind/GateChangeDeleteController.java b/cwms-data-api/src/main/java/cwms/cda/api/location/kind/GateChangeDeleteController.java index 9fbf1bf82..b8c793e32 100644 --- a/cwms-data-api/src/main/java/cwms/cda/api/location/kind/GateChangeDeleteController.java +++ b/cwms-data-api/src/main/java/cwms/cda/api/location/kind/GateChangeDeleteController.java @@ -20,24 +20,24 @@ package cwms.cda.api.location.kind; +import static cwms.cda.api.Controllers.*; + import com.codahale.metrics.MetricRegistry; import com.codahale.metrics.Timer; import cwms.cda.api.BaseHandler; import cwms.cda.data.dao.JooqDao; import cwms.cda.data.dao.location.kind.OutletDao; import cwms.cda.data.dto.CwmsId; +import cwms.cda.helpers.annotations.IgnoreRequiredQueryParamMismatch; 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.OpenApiParam; import io.javalin.plugin.openapi.annotations.OpenApiResponse; -import java.sql.Timestamp; import java.time.Instant; import javax.servlet.http.HttpServletResponse; import org.jetbrains.annotations.NotNull; import org.jooq.DSLContext; -import static cwms.cda.api.Controllers.*; -import static cwms.cda.api.Controllers.GET_ALL; public class GateChangeDeleteController extends BaseHandler { @@ -70,6 +70,7 @@ public GateChangeDeleteController(MetricRegistry metrics) { tags = {OutletController.TAG}, method = HttpMethod.DELETE ) + @IgnoreRequiredQueryParamMismatch(parameterNames = {TIMEZONE}) @Override public void handle(@NotNull Context context) throws Exception { String office = context.pathParam(OFFICE); diff --git a/cwms-data-api/src/main/java/cwms/cda/api/location/kind/GateChangeGetAllController.java b/cwms-data-api/src/main/java/cwms/cda/api/location/kind/GateChangeGetAllController.java index e3761f8f6..fdc6cfc97 100644 --- a/cwms-data-api/src/main/java/cwms/cda/api/location/kind/GateChangeGetAllController.java +++ b/cwms-data-api/src/main/java/cwms/cda/api/location/kind/GateChangeGetAllController.java @@ -20,6 +20,8 @@ package cwms.cda.api.location.kind; +import static cwms.cda.api.Controllers.*; + import com.codahale.metrics.MetricRegistry; import com.codahale.metrics.Timer; import cwms.cda.api.BaseHandler; @@ -30,19 +32,18 @@ import cwms.cda.data.dto.location.kind.GateChange; import cwms.cda.formatters.ContentType; import cwms.cda.formatters.Formats; +import cwms.cda.helpers.annotations.IgnoreRequiredQueryParamMismatch; import io.javalin.core.util.Header; import io.javalin.http.Context; 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.sql.Timestamp; import java.time.Instant; import java.util.List; import javax.servlet.http.HttpServletResponse; import org.jetbrains.annotations.NotNull; import org.jooq.DSLContext; -import static cwms.cda.api.Controllers.*; public class GateChangeGetAllController extends BaseHandler { private static final int DEFAULT_PAGE_SIZE = 500; @@ -94,6 +95,7 @@ public GateChangeGetAllController(MetricRegistry metrics) { description = "Returns matching CWMS gate change data for a Reservoir Project.", tags = {OutletController.TAG} ) + @IgnoreRequiredQueryParamMismatch(parameterNames = {TIMEZONE}) @Override public void handle(@NotNull Context context) { String office = context.pathParam(OFFICE); diff --git a/cwms-data-api/src/main/java/cwms/cda/api/location/kind/LockController.java b/cwms-data-api/src/main/java/cwms/cda/api/location/kind/LockController.java index 1acb7e586..547559878 100644 --- a/cwms-data-api/src/main/java/cwms/cda/api/location/kind/LockController.java +++ b/cwms-data-api/src/main/java/cwms/cda/api/location/kind/LockController.java @@ -108,7 +108,7 @@ private Timer.Context markAndTime(String subject) { public void getAll(@NotNull Context ctx) { try (Timer.Context ignored = markAndTime(GET_ALL)) { String office = requiredParam(ctx, OFFICE); - String projectId = ctx.queryParam(PROJECT_ID); + String projectId = requiredParam(ctx, PROJECT_ID); CwmsId project = CwmsId.buildCwmsId(office, projectId); DSLContext dsl = getDslContext(ctx); LockDao dao = new LockDao(dsl); diff --git a/cwms-data-api/src/main/java/cwms/cda/api/project/UpdateLockRevokerRights.java b/cwms-data-api/src/main/java/cwms/cda/api/project/UpdateLockRevokerRights.java index a428d913e..50c58904f 100644 --- a/cwms-data-api/src/main/java/cwms/cda/api/project/UpdateLockRevokerRights.java +++ b/cwms-data-api/src/main/java/cwms/cda/api/project/UpdateLockRevokerRights.java @@ -29,6 +29,7 @@ import static cwms.cda.api.Controllers.PROJECT_MASK; import static cwms.cda.api.Controllers.USER_ID; import static cwms.cda.api.Controllers.requiredParam; +import static cwms.cda.api.Controllers.requiredParamAs; import com.codahale.metrics.MetricRegistry; import com.codahale.metrics.Timer; @@ -82,8 +83,7 @@ public void handle(@NotNull Context ctx) throws Exception { String userId = requiredParam(ctx, USER_ID); String projMask = ctx.queryParamAsClass(PROJECT_MASK, String.class).getOrDefault("*"); String appId = requiredParam(ctx, APPLICATION_ID); - Boolean allow = ctx.queryParamAsClass(Controllers.ALLOW, Boolean.class) - .getOrThrow(e -> new RequiredQueryParameterException(Controllers.ALLOW)); + Boolean allow = requiredParamAs(ctx, Controllers.ALLOW, Boolean.class); try (final Timer.Context ignored = markAndTime("updateRights")) { ProjectLockDao lockDao = new ProjectLockDao(JooqDao.getDslContext(ctx)); diff --git a/cwms-data-api/src/main/java/cwms/cda/api/rating/RatingController.java b/cwms-data-api/src/main/java/cwms/cda/api/rating/RatingController.java index 78d8ed7dd..365816242 100644 --- a/cwms-data-api/src/main/java/cwms/cda/api/rating/RatingController.java +++ b/cwms-data-api/src/main/java/cwms/cda/api/rating/RatingController.java @@ -52,6 +52,7 @@ import static cwms.cda.api.Controllers.UPDATE; import static cwms.cda.api.Controllers.VERSION_DATE; import static cwms.cda.api.Controllers.addDeprecatedContentTypeWarning; +import static cwms.cda.api.Controllers.requiredParam; import static cwms.cda.data.dao.JooqDao.getDslContext; import com.codahale.metrics.Histogram; @@ -245,9 +246,9 @@ public void delete(@NotNull Context ctx, @NotNull String ratingSpecId) { DSLContext dsl = getDslContext(ctx); String timezone = ctx.queryParamAsClass(TIMEZONE, String.class).getOrDefault("UTC"); - Instant startTimeDate = DateUtils.parseUserDate(ctx.queryParam(BEGIN), timezone).toInstant(); - Instant endTimeDate = DateUtils.parseUserDate(ctx.queryParam(END), timezone).toInstant(); - String office = ctx.queryParam(OFFICE); + Instant startTimeDate = DateUtils.parseUserDate(requiredParam(ctx, BEGIN), timezone).toInstant(); + Instant endTimeDate = DateUtils.parseUserDate(requiredParam(ctx, END), timezone).toInstant(); + String office = requiredParam(ctx, OFFICE); RatingDao ratingDao = getRatingDao(dsl); ratingDao.delete(office, ratingSpecId, startTimeDate, endTimeDate); ctx.status(HttpServletResponse.SC_NO_CONTENT); @@ -400,7 +401,7 @@ public void getAll(@NotNull Context ctx) { public void getOne(@NotNull Context ctx, @NotNull String rating) { try (final Timer.Context ignored = markAndTime(GET_ONE)) { - String officeId = ctx.queryParam(OFFICE); + String officeId = requiredParam(ctx, OFFICE); String timezone = ctx.queryParamAsClass(TIMEZONE, String.class).getOrDefault("UTC"); VerticalDatum verticalDatum = VerticalDatum.getVerticalDatum(ctx.queryParam(DATUM)); diff --git a/cwms-data-api/src/main/java/cwms/cda/api/rating/RatingLatestController.java b/cwms-data-api/src/main/java/cwms/cda/api/rating/RatingLatestController.java index c7db67d17..aff922c10 100644 --- a/cwms-data-api/src/main/java/cwms/cda/api/rating/RatingLatestController.java +++ b/cwms-data-api/src/main/java/cwms/cda/api/rating/RatingLatestController.java @@ -28,6 +28,7 @@ import static cwms.cda.api.Controllers.OFFICE; import static cwms.cda.api.Controllers.RATING_ID; import static cwms.cda.api.Controllers.STATUS_200; +import static cwms.cda.api.Controllers.requiredParam; import static cwms.cda.data.dao.JooqDao.getDslContext; import com.codahale.metrics.MetricRegistry; @@ -91,7 +92,7 @@ public void handle(@NotNull Context ctx) throws Exception { ContentType contentType = new ContentType(ctx.contentType() != null ? ctx.contentType() : Formats.JSONV2); - String officeId = ctx.queryParam(OFFICE); + String officeId = requiredParam(ctx, OFFICE); if (!contentType.toString().equals(Formats.JSONV2) && !contentType.toString().equals(Formats.XMLV2)) { ctx.status(HttpCode.UNSUPPORTED_MEDIA_TYPE); 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 b477afdb7..63f157b45 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 @@ -153,7 +153,7 @@ public void getAll(Context ctx) { + "the rating-id of the Rating Spec to be included in the response") }, queryParams = { - @OpenApiParam(name = OFFICE, required = true, description = "Specifies the " + @OpenApiParam(name = OFFICE, description = "Specifies the " + "owning office of the Rating Specs whose data is to be included in " + "the response. If this field is not specified, matching rating " + "information from all offices shall be returned."), @@ -275,9 +275,9 @@ public void delete(Context ctx, @NotNull String ratingSpecId) { try (final Timer.Context ignored = markAndTime(DELETE)) { DSLContext dsl = getDslContext(ctx); - String office = ctx.queryParam(OFFICE); + String office = requiredParam(ctx, OFFICE); RatingSpecDao ratingDao = getRatingSpecDao(dsl); - JooqDao.DeleteMethod method = ctx.queryParamAsClass(METHOD, JooqDao.DeleteMethod.class).get(); + JooqDao.DeleteMethod method = requiredParamAs(ctx, METHOD, JooqDao.DeleteMethod.class); ratingDao.delete(office, method, ratingSpecId); ctx.status(HttpServletResponse.SC_NO_CONTENT); } diff --git a/cwms-data-api/src/main/java/cwms/cda/api/rating/RatingTemplateController.java b/cwms-data-api/src/main/java/cwms/cda/api/rating/RatingTemplateController.java index 5b5a1e49a..bd783d083 100644 --- a/cwms-data-api/src/main/java/cwms/cda/api/rating/RatingTemplateController.java +++ b/cwms-data-api/src/main/java/cwms/cda/api/rating/RatingTemplateController.java @@ -154,7 +154,7 @@ private RatingTemplateDao getRatingTemplateDao(DSLContext dsl) { + " the template whose data is to be included in the response") }, queryParams = { - @OpenApiParam(name = OFFICE, required = true, description = "Specifies the " + @OpenApiParam(name = OFFICE, description = "Specifies the " + "owning office of the Rating Templates whose data is to be included" + " in the response. If this field is not specified, matching rating " + "information from all offices shall be returned."), @@ -280,9 +280,9 @@ public void delete(Context ctx, String ratingTemplateId) { try (final Timer.Context ignored = markAndTime(DELETE)){ DSLContext dsl = getDslContext(ctx); - String office = ctx.queryParam(OFFICE); + String office = requiredParam(ctx, OFFICE); RatingTemplateDao ratingDao = new RatingTemplateDao(dsl); - JooqDao.DeleteMethod method = ctx.queryParamAsClass(METHOD, JooqDao.DeleteMethod.class).get(); + JooqDao.DeleteMethod method = requiredParamAs(ctx, METHOD, JooqDao.DeleteMethod.class); ratingDao.delete(office, method, ratingTemplateId); ctx.status(HttpServletResponse.SC_NO_CONTENT); } diff --git a/cwms-data-api/src/main/java/cwms/cda/api/timeseriesprofile/TimeSeriesProfileController.java b/cwms-data-api/src/main/java/cwms/cda/api/timeseriesprofile/TimeSeriesProfileController.java index 38c2fdfab..6e535b9eb 100644 --- a/cwms-data-api/src/main/java/cwms/cda/api/timeseriesprofile/TimeSeriesProfileController.java +++ b/cwms-data-api/src/main/java/cwms/cda/api/timeseriesprofile/TimeSeriesProfileController.java @@ -60,7 +60,8 @@ public TimeSeriesProfileController(MetricRegistry metrics) { @OpenApi( queryParams = { - @OpenApiParam(name = OFFICE, description = "The office ID associated with the time series profile"), + @OpenApiParam(name = OFFICE, required = true, + description = "The office ID associated with the time series profile"), }, pathParams = { @OpenApiParam(name = PARAMETER_ID, description = "The key parameter ID associated with the time " diff --git a/cwms-data-api/src/main/java/cwms/cda/api/timeseriesprofile/TimeSeriesProfileDeleteController.java b/cwms-data-api/src/main/java/cwms/cda/api/timeseriesprofile/TimeSeriesProfileDeleteController.java index 4a15335b2..04d8a4803 100644 --- a/cwms-data-api/src/main/java/cwms/cda/api/timeseriesprofile/TimeSeriesProfileDeleteController.java +++ b/cwms-data-api/src/main/java/cwms/cda/api/timeseriesprofile/TimeSeriesProfileDeleteController.java @@ -59,7 +59,8 @@ public TimeSeriesProfileDeleteController(MetricRegistry metrics) { @OpenApi( queryParams = { - @OpenApiParam(name = OFFICE, description = "The office associated with the time series profile"), + @OpenApiParam(name = OFFICE, required = true, + description = "The office associated with the time series profile"), }, pathParams = { @OpenApiParam(name = LOCATION_ID, description = "The location ID associated with the time " diff --git a/cwms-data-api/src/main/java/cwms/cda/api/timeseriesprofile/TimeSeriesProfileInstanceCreateController.java b/cwms-data-api/src/main/java/cwms/cda/api/timeseriesprofile/TimeSeriesProfileInstanceCreateController.java index ccdeb5528..2f7682a34 100644 --- a/cwms-data-api/src/main/java/cwms/cda/api/timeseriesprofile/TimeSeriesProfileInstanceCreateController.java +++ b/cwms-data-api/src/main/java/cwms/cda/api/timeseriesprofile/TimeSeriesProfileInstanceCreateController.java @@ -35,6 +35,7 @@ import cwms.cda.data.dao.timeseriesprofile.TimeSeriesProfileInstanceDao; import cwms.cda.data.dto.timeseriesprofile.TimeSeriesProfile; import cwms.cda.formatters.Formats; +import cwms.cda.helpers.annotations.IgnoreRequiredQueryParamMismatch; import io.javalin.http.Context; import io.javalin.http.Handler; import io.javalin.plugin.openapi.annotations.HttpMethod; @@ -82,6 +83,7 @@ public TimeSeriesProfileInstanceCreateController(MetricRegistry metrics) { @OpenApiResponse(status = "409", description = "Time series profile instance already exists") } ) + @IgnoreRequiredQueryParamMismatch(parameterNames = {TIMEZONE}) @Override public void handle(@NotNull Context ctx) { try (final Timer.Context ignored = markAndTime(CREATE)) { diff --git a/cwms-data-api/src/main/java/cwms/cda/api/watersupply/AccountingCatalogController.java b/cwms-data-api/src/main/java/cwms/cda/api/watersupply/AccountingCatalogController.java index 1e10310c5..1b192feaa 100644 --- a/cwms-data-api/src/main/java/cwms/cda/api/watersupply/AccountingCatalogController.java +++ b/cwms-data-api/src/main/java/cwms/cda/api/watersupply/AccountingCatalogController.java @@ -47,6 +47,7 @@ import com.codahale.metrics.MetricRegistry; import com.codahale.metrics.Timer; +import com.google.common.flogger.FluentLogger; import cwms.cda.api.Controllers; import cwms.cda.api.errors.CdaError; import cwms.cda.data.dao.watersupply.WaterContractDao; @@ -57,6 +58,7 @@ import cwms.cda.data.dto.watersupply.WaterUserContract; import cwms.cda.formatters.ContentType; import cwms.cda.formatters.Formats; +import cwms.cda.helpers.annotations.IgnoreRequiredQueryParamMismatch; import io.javalin.core.util.Header; import io.javalin.http.Context; import io.javalin.http.Handler; @@ -67,7 +69,6 @@ import io.javalin.plugin.openapi.annotations.OpenApiResponse; import java.time.Instant; import java.util.List; -import com.google.common.flogger.FluentLogger; import javax.servlet.http.HttpServletResponse; import org.jetbrains.annotations.NotNull; import org.jooq.DSLContext; @@ -141,7 +142,7 @@ protected WaterSupplyAccountingDao getWaterSupplyAccountingDao(DSLContext dsl) { method = HttpMethod.GET, tags = {TAG} ) - + @IgnoreRequiredQueryParamMismatch(parameterNames = {TIMEZONE}) @Override public void handle(Context ctx) { try (Timer.Context ignored = markAndTime(GET_ALL)) { @@ -151,17 +152,15 @@ public void handle(Context ctx) { final String locationId = ctx.pathParam(PROJECT_ID); final Instant startTime = requiredInstant(ctx, START); final Instant endTime = requiredInstant(ctx, END); - final String units = ctx.queryParam(UNIT) != null ? ctx.queryParam(UNIT) : "cms"; - final boolean startInclusive = ctx.queryParam(START_TIME_INCLUSIVE) == null - || Boolean.parseBoolean(ctx.queryParam(START_TIME_INCLUSIVE)); - final boolean endInclusive = ctx.queryParam(END_TIME_INCLUSIVE) == null - || Boolean.parseBoolean(ctx.queryParam(END_TIME_INCLUSIVE)); - final boolean ascending = ctx.queryParam(ASCENDING) == null - || Boolean.parseBoolean(ctx.queryParam(ASCENDING)); - final int rowLimit = ctx.queryParam(ROW_LIMIT) != null ? Integer.parseInt(ctx.queryParam(ROW_LIMIT)) : 0; + final String units = ctx.queryParamAsClass(UNIT, String.class).getOrDefault("cms"); + final boolean startInclusive = ctx.queryParamAsClass(START_TIME_INCLUSIVE, Boolean.class) + .getOrDefault(true); + final boolean endInclusive = ctx.queryParamAsClass(END_TIME_INCLUSIVE, Boolean.class).getOrDefault(true); + final boolean ascending = ctx.queryParamAsClass(ASCENDING, Boolean.class).getOrDefault(true); + final int rowLimit = ctx.queryParamAsClass(ROW_LIMIT, Integer.class).getOrDefault(0); DSLContext dsl = getDslContext(ctx); - String formatHeader = ctx.header(Header.ACCEPT) != null ? ctx.header(Header.ACCEPT) : Formats.JSONV1; + String formatHeader = ctx.headerAsClass(Header.ACCEPT, String.class).getOrDefault(Formats.JSONV1); ContentType contentType = Formats.parseHeader(formatHeader, WaterSupplyAccounting.class); ctx.contentType(contentType.toString()); CwmsId projectLocation = new CwmsId.Builder().withOfficeId(office).withName(locationId).build(); diff --git a/cwms-data-api/src/main/java/cwms/cda/helpers/annotations/IgnoreRequiredQueryParamMismatch.java b/cwms-data-api/src/main/java/cwms/cda/helpers/annotations/IgnoreRequiredQueryParamMismatch.java new file mode 100644 index 000000000..cd06733e1 --- /dev/null +++ b/cwms-data-api/src/main/java/cwms/cda/helpers/annotations/IgnoreRequiredQueryParamMismatch.java @@ -0,0 +1,51 @@ +/* + * + * 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.helpers.annotations; + +import java.lang.annotation.Documented; +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +/* + * Inform the OpenAPI documentation verification process to ignore mismatches + * in required query parameters between the annotated element and the OpenAPI specification. + * This is useful in cases where the implementation intentionally deviates from the specification + * for certain parameters, such as when a parameter is one of a set of mutually exclusive required parameters. + */ +@Documented +@Target({ElementType.METHOD}) +@Retention(RetentionPolicy.RUNTIME) +public @interface IgnoreRequiredQueryParamMismatch { + /** + * Returns the names of the parameters that are being ignored in the required parameter mismatch check. + * + * @return the names of the parameters to be ignored + */ + String[] parameterNames(); +} diff --git a/cwms-data-api/src/test/java/cwms/cda/api/OpenApiDocTest.java b/cwms-data-api/src/test/java/cwms/cda/api/OpenApiDocTest.java index 48322cde5..660b55495 100644 --- a/cwms-data-api/src/test/java/cwms/cda/api/OpenApiDocTest.java +++ b/cwms-data-api/src/test/java/cwms/cda/api/OpenApiDocTest.java @@ -210,7 +210,7 @@ private void testPathParameters(List expectedPathParameters, S assertAll( () -> assertTrue(receivedItems.isEmpty(), "Found used undocumented path parameter: " + extraInfo), () -> assertTrue(missingItems.isEmpty(), "Found documented path parameter that is not used: " + missingInfo), - () -> assertAll(expectedParams.stream().map(expectedParam -> testParamInfo(expectedParam, verifiedUsages))) + () -> assertAll(expectedParams.stream().map(expectedParam -> testParamInfo(expectedParam, verifiedUsages, true))) ); } @@ -246,11 +246,11 @@ private void testQueryParameters(List expectedQueryParameters, .collect(Collectors.joining(", ")); assertAll(() -> assertTrue(receivedItems.isEmpty(), "Found used undocumented query parameter: " + extraInfo), () -> assertTrue(missingItems.isEmpty(), "Found documented query parameter that is not used: " + missingInfo), - () -> assertAll(expectedParams.stream().map(expectedParam -> testParamInfo(expectedParam, verifiedUsages)))); + () -> assertAll(expectedParams.stream().map(expectedParam -> testParamInfo(expectedParam, verifiedUsages, false)))); } private Executable testParamInfo(OpenApiParamInfo expectedParam, - Set receivedQueryParameters) { + Set receivedQueryParameters, boolean pathParam) { OpenApiParamUsageInfo receivedInfo = receivedQueryParameters.stream() .filter(receivedUsageInfo -> receivedUsageInfo.getParamInfo() .getName() @@ -263,7 +263,18 @@ private Executable testParamInfo(OpenApiParamInfo expectedParam, //Real tests return () -> assertAll(() -> assertTrue(receivedInfo.isUsed(), "Unable to find a usage of documented parameter: " + expectedParam.getName()), - () -> assertTrue(receivedInfo.isNullHandled(), "Unable to find a null handled usage of documented parameter: " + expectedParam.getName())); + () -> assertTrue(receivedInfo.isNullHandled(), "Unable to find a null handled usage of documented parameter: " + expectedParam.getName()), + // Disabled type checking due to many parameters being read as strings and then converted, + // which is a valid way to read parameters, but makes it difficult to verify the type is correct. + // We can re-enable this in the future if we want to be more strict about how parameters are read. + //() -> assertEquals(receivedInfo.getParamInfo().getType(), expectedParam.getType(), "Incorrect type for parameter: " + expectedParam.getName()), + () -> assertEquals(receivedInfo.getParamInfo().getName(), expectedParam.getName(), "Incorrect name for parameter: " + expectedParam.getName()), + () -> { + if (!pathParam && !expectedParam.ignoreRequired()) // Path parameters are always required, so we don't need to check that. + { + assertEquals(receivedInfo.getParamInfo().isRequired(), expectedParam.isRequired(), "Incorrect required status for parameter: " + expectedParam.getName()); + } + }); } private OpenApiParamUsage parseParamInfo(CompilationUnit unit, Class clazz, Method method) { @@ -458,7 +469,7 @@ private OpenApiParamUsageInfo readUsageFromCall(CompilationUnit unit, Class c boolean used = true; boolean nullHandled = true; if (!required) { - //Check if null is handled via getOrDefault + //TODO: Check if null is handled via getOrDefault } return new OpenApiParamUsageInfo(new OpenApiParamInfo(paramName, required, paramClass), used, nullHandled); }).orElseGet(() -> { diff --git a/cwms-data-api/src/test/java/helpers/OpenApiParamInfo.java b/cwms-data-api/src/test/java/helpers/OpenApiParamInfo.java index ef85450d6..38e6feae3 100644 --- a/cwms-data-api/src/test/java/helpers/OpenApiParamInfo.java +++ b/cwms-data-api/src/test/java/helpers/OpenApiParamInfo.java @@ -26,6 +26,7 @@ public class OpenApiParamInfo { private String name; private final boolean required; private final Class type; + private boolean ignoreRequired = false; public OpenApiParamInfo(String name, boolean required, Class type) { this.name = name; @@ -38,6 +39,15 @@ public OpenApiParamInfo setName(String name) { return this; } + public OpenApiParamInfo setIgnoreRequired(boolean ignoreRequired) { + this.ignoreRequired = ignoreRequired; + return this; + } + + public boolean ignoreRequired() { + return ignoreRequired; + } + public String getName() { return name; } diff --git a/cwms-data-api/src/test/java/helpers/OpenApiTestHelper.java b/cwms-data-api/src/test/java/helpers/OpenApiTestHelper.java index 919b2abaa..b3e62d21a 100644 --- a/cwms-data-api/src/test/java/helpers/OpenApiTestHelper.java +++ b/cwms-data-api/src/test/java/helpers/OpenApiTestHelper.java @@ -8,6 +8,7 @@ import com.github.javaparser.symbolsolver.resolution.typesolvers.CombinedTypeSolver; import com.github.javaparser.symbolsolver.resolution.typesolvers.JavaParserTypeSolver; import com.github.javaparser.symbolsolver.resolution.typesolvers.ReflectionTypeSolver; +import cwms.cda.helpers.annotations.IgnoreRequiredQueryParamMismatch; import io.javalin.plugin.openapi.annotations.OpenApi; import io.javalin.plugin.openapi.annotations.OpenApiParam; import java.io.IOException; @@ -47,10 +48,20 @@ public static OpenApiDocInfo readDocParams(Method m) { if (oa == null || oa.ignore()) { return new OpenApiDocInfo(m, true); } + String[] ignored = new String[0]; + IgnoreRequiredQueryParamMismatch ignore = m.getAnnotation(IgnoreRequiredQueryParamMismatch.class); + if (ignore != null) { + ignored = ignore.parameterNames(); + } OpenApiDocInfo info = new OpenApiDocInfo(m, false); for (OpenApiParam p : oa.queryParams()) { if (p != null && !p.name().trim().isEmpty()) { OpenApiParamInfo paramObj = new OpenApiParamInfo(p.name(), p.required(), p.type()); + for (String ignoredName : ignored) { + if (p.name().equalsIgnoreCase(ignoredName)) { + paramObj = paramObj.setIgnoreRequired(true); + } + } info.getQueryParameters().add(paramObj); } }