diff --git a/cwms-data-api/src/main/java/cwms/cda/api/CatalogController.java b/cwms-data-api/src/main/java/cwms/cda/api/CatalogController.java index af9cc6093..8562d5988 100644 --- a/cwms-data-api/src/main/java/cwms/cda/api/CatalogController.java +++ b/cwms-data-api/src/main/java/cwms/cda/api/CatalogController.java @@ -34,6 +34,7 @@ import org.jetbrains.annotations.NotNull; import org.jooq.DSLContext; import org.owasp.html.PolicyFactory; +import cwms.cda.api.errors.UnsupportedParametersException; public class CatalogController implements CrudHandler { @@ -102,11 +103,13 @@ public void getAll(Context ctx) { ), @OpenApiParam(name = TIMESERIES_CATEGORY_LIKE, description = "Posix regular expression " - + "matching against the timeseries category id" + + "matching against the timeseries category id. Note: This parameter is " + + "unsupported when dataset is Locations." ), @OpenApiParam(name = TIMESERIES_GROUP_LIKE, description = "Posix regular expression " - + "matching against the timeseries group id" + + "matching against the timeseries group id. Note: This parameter is " + + "unsupported when dataset is Locations." ), @OpenApiParam(name = LOCATION_CATEGORY_LIKE, description = "Posix regular expression " @@ -122,7 +125,8 @@ public void getAll(Context ctx) { + "items with no bounding office set will not be present in results."), @OpenApiParam(name = INCLUDE_EXTENTS, type = Boolean.class, description = "Whether the returned catalog entries should include timeseries " - + "extents. Only valid for TIMESERIES. " + + "extents. Only valid for TIMESERIES. Note: This parameter is " + + "unsupported when dataset is Locations." + "Default is " + INCLUDE_EXTENTS_DEFAULT + "."), @OpenApiParam(name = EXCLUDE_EMPTY, type = Boolean.class, description = "Specifies " @@ -131,7 +135,8 @@ public void getAll(Context ctx) { + "'empty' is defined as VERSION_TIME, EARLIEST_TIME, LATEST_TIME " + "and LAST_UPDATE all being null. This parameter does not control " + "whether the extents are returned to the user, only whether matching " - + "timeseries are excluded. Only valid for TIMESERIES. " + + "timeseries are excluded. Only valid for TIMESERIES. Note: This parameter is " + + "unsupported when dataset is Locations." + "Default is " + EXCLUDE_EMPTY_DEFAULT + "."), @OpenApiParam(name = LOCATION_KIND_LIKE, description = "Posix regular expression matching " @@ -299,8 +304,7 @@ private static void warnAboutNotSupported(@NotNull Context ctx, String[] warnAbo notSupported.retainAll(queryParamMap.keySet()); if (!notSupported.isEmpty()) { - throw new IllegalArgumentException("The following parameters are not yet " - + "supported for this method: " + notSupported); + throw new UnsupportedParametersException(List.copyOf(notSupported)); } } diff --git a/cwms-data-api/src/main/java/cwms/cda/api/errors/UnsupportedParametersException.java b/cwms-data-api/src/main/java/cwms/cda/api/errors/UnsupportedParametersException.java new file mode 100644 index 000000000..4935087f7 --- /dev/null +++ b/cwms-data-api/src/main/java/cwms/cda/api/errors/UnsupportedParametersException.java @@ -0,0 +1,59 @@ +package cwms.cda.api.errors; +import java.io.Serializable; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; +import java.util.logging.Level; +import javax.servlet.http.HttpServletResponse; + +/** + * Exception indicating that one or more provided query parameters are not supported + * for the requested operation. Intended for direct user feedback (HTTP 400). + * Default CDA_MESSAGE specific to Locations catalog. + */ +public final class UnsupportedParametersException extends ApplicationException +{ + private static final Level LOG_LEVEL = Level.INFO; + public static final String UNSUPPORTED_QUERY_PARAMETERS = "unsupported query parameters"; + public static final String MESSAGE = "unsupported query parameters present"; + public static final String CDA_MESSAGE = "Unsupported parameter(s) for Locations catalog"; + private final Map details = new LinkedHashMap<>(); + + public UnsupportedParametersException(List params) + { + this(MESSAGE, params); + } + + public UnsupportedParametersException(String message, List params) + { + super(message, USER_INPUT_SOURCE, CDA_MESSAGE, HttpServletResponse.SC_BAD_REQUEST, + LOG_LEVEL, buildDetailsMap(params), null); + details.put(UNSUPPORTED_QUERY_PARAMETERS, String.join(",", params)); + } + + // option for controller-specific CDA messages + public UnsupportedParametersException(String message, String cdaMessage, List params) + { + super(message, USER_INPUT_SOURCE, cdaMessage, HttpServletResponse.SC_BAD_REQUEST, + LOG_LEVEL, buildDetailsMap(params), null); + details.put(UNSUPPORTED_QUERY_PARAMETERS, String.join(",", params)); + } + + public UnsupportedParametersException(String param) + { + this(MESSAGE, List.of(param)); + } + + @Override + public Map getDetails() + { + return details; + } + + private static Map buildDetailsMap(List fields) + { + Map details = new LinkedHashMap<>(); + details.put(UNSUPPORTED_QUERY_PARAMETERS, String.join(",", fields)); + return details; + } +} \ No newline at end of file diff --git a/cwms-data-api/src/test/java/cwms/cda/api/CatalogControllerTestIT.java b/cwms-data-api/src/test/java/cwms/cda/api/CatalogControllerTestIT.java index 55df8eb37..cc10e68b5 100644 --- a/cwms-data-api/src/test/java/cwms/cda/api/CatalogControllerTestIT.java +++ b/cwms-data-api/src/test/java/cwms/cda/api/CatalogControllerTestIT.java @@ -686,4 +686,59 @@ void testFilterLocations() throws Exception{ .statusCode(is(HttpServletResponse.SC_OK)) .body("entries.size()", is(0)); } + + @Test + void test_locations_unsupported_param_single() { + // When requesting the LOCATIONS catalog, certain timeseries params are not supported. + // Verify that supplying a single unsupported parameter results in a 400 with only that parameter listed. + Response resp = + given() + .log().ifValidationFails(LogDetail.ALL, true) + .accept(Formats.JSON) + .queryParam(OFFICE, OFFICE) + .queryParam(INCLUDE_EXTENTS, true) + .when() + .get("/catalog/LOCATIONS") + .then() + .log().ifValidationFails(LogDetail.ALL, true) + .assertThat() + .statusCode(is(HttpServletResponse.SC_BAD_REQUEST)) + .body("message", is("Unsupported parameter(s) for Locations catalog")) + .body("details.'unsupported query parameters'", is(INCLUDE_EXTENTS)) + .extract() + .response(); + + // Ensure only the provided unsupported parameter is mentioned + String details = resp.path("details.'unsupported query parameters'"); + assertEquals(INCLUDE_EXTENTS, details); + } + + @Test + void test_locations_unsupported_params_multiple() { + // Verify that if multiple unsupported params are provided, only those provided are reported. + Response resp = + given() + .log().ifValidationFails(LogDetail.ALL, true) + .accept(Formats.JSON) + .queryParam(OFFICE, OFFICE) + .queryParam(INCLUDE_EXTENTS, true) + .queryParam(EXCLUDE_EMPTY, true) + .when() + .get("/catalog/LOCATIONS") + .then() + .log().ifValidationFails(LogDetail.ALL, true) + .assertThat() + .statusCode(is(HttpServletResponse.SC_BAD_REQUEST)) + .body("message", is("Unsupported parameter(s) for Locations catalog")) + .body("details", hasKey("unsupported query parameters")) + .extract() + .response(); + + String details = resp.path("details.'unsupported query parameters'"); + assertNotNull(details); + String[] parts = details.split(","); + assertEquals(2, parts.length, "Expected exactly two unsupported parameters to be reported"); + // Order of parameters in the message is not guaranteed; verify as a set + assertTrue(List.of(parts).containsAll(List.of(INCLUDE_EXTENTS, EXCLUDE_EMPTY))); + } }