diff --git a/cwms-data-api/src/main/java/cwms/cda/data/dao/JooqDao.java b/cwms-data-api/src/main/java/cwms/cda/data/dao/JooqDao.java index 28004b35e..2c862083c 100644 --- a/cwms-data-api/src/main/java/cwms/cda/data/dao/JooqDao.java +++ b/cwms-data-api/src/main/java/cwms/cda/data/dao/JooqDao.java @@ -36,6 +36,7 @@ import cwms.cda.datasource.DelegatingConnectionPreparer; import cwms.cda.helpers.DatabaseHelpers.SCHEMA_VERSION; import cwms.cda.security.CwmsAuthException; +import io.javalin.http.BadRequestResponse; import io.javalin.http.Context; import io.javalin.http.HandlerType; import java.math.BigDecimal; @@ -50,11 +51,15 @@ import java.util.ArrayList; import java.util.Arrays; import java.util.Collections; +import java.util.HashMap; import java.util.List; +import java.util.Map; import java.util.Objects; import java.util.Optional; import java.util.regex.Matcher; import java.util.regex.Pattern; +import java.util.stream.Collectors; +import java.util.stream.IntStream; import javax.servlet.http.HttpServletResponse; import javax.sql.DataSource; import org.jetbrains.annotations.NotNull; @@ -98,6 +103,7 @@ public abstract class JooqDao extends Dao { "ORA-12899: value too large for column \".+\"\\.\".+\"\\.\"(.+)\" " + "\\(actual: (\\d+), maximum: (\\d+)\\)"); private static final Pattern REGEX_META_CHARS_EXCEPT_DOT = Pattern.compile("[\\\\^$|?+()\\[\\]{}]"); + private static final Pattern ORA_CODE = Pattern.compile("^ORA-\\d+: ERROR: .*"); public enum DeleteMethod { DELETE_ALL(DeleteRule.DELETE_ALL), @@ -339,6 +345,8 @@ public static RuntimeException wrapException(RuntimeException input) { retVal = buildFieldLengthExceededException(input); } else if (isTSIDInvalidIntervalException(input)) { retVal = buildInvalidTSIDIntervalException(input); + } else if (isBadRequest(input)) { + retVal = buildBadRequest(input); } return retVal; @@ -381,6 +389,45 @@ private static boolean hasCodeAndMessage(SQLException sqlException, // See link for a more complete list of CWMS Error codes: // https://bitbucket.hecdev.net/projects/CWMS/repos/cwms_database_origin_teamcity_work/browse/src/buildSqlScripts.py#4866 + public static boolean isBadRequest(RuntimeException input) { + boolean retVal = false; + + Optional optional = getSqlException(input); + if (optional.isPresent()) { + SQLException sqlException = optional.get(); + if (!sqlException.getLocalizedMessage().contains("CAN_NOT_DELETE")) { + List codes = IntStream.range(20000, 20999).boxed().collect(Collectors.toList()); + + retVal = hasCodeOrMessage(sqlException, codes, new ArrayList<>()); + } + } + return retVal; + } + + public static BadRequestResponse buildBadRequest(RuntimeException input) { + Throwable cause = input; + if (input instanceof DataAccessException) { + DataAccessException dae = (DataAccessException) input; + cause = dae.getCause(); + } + + String localizedMessage = cause.getLocalizedMessage(); + + if (localizedMessage != null) { + String[] parts = localizedMessage.split("\n"); + String errorMessage = parts[0].replace("'", ""); + if (ORA_CODE.matcher(errorMessage).matches()) { + errorMessage = errorMessage.substring(errorMessage.indexOf("ERROR: ") + 7); + } + errorMessage = sanitizeOrNull(errorMessage); + Map errorDetails = new HashMap<>(); + errorDetails.put("message", errorMessage); + return new BadRequestResponse("", errorDetails); + } + return new BadRequestResponse(); + } + + public static boolean isNotFound(RuntimeException input) { boolean retVal = false; diff --git a/cwms-data-api/src/test/java/cwms/cda/api/LocationGroupControllerTestIT.java b/cwms-data-api/src/test/java/cwms/cda/api/LocationGroupControllerTestIT.java index 53d707087..140f9ddea 100644 --- a/cwms-data-api/src/test/java/cwms/cda/api/LocationGroupControllerTestIT.java +++ b/cwms-data-api/src/test/java/cwms/cda/api/LocationGroupControllerTestIT.java @@ -766,6 +766,175 @@ void test_create_read_delete_same_names_different_offices(String format) throws .statusCode(is(HttpServletResponse.SC_NOT_FOUND)); } + + @Test + void test_create_read_delete_same_aliases_different_names() throws Exception { + // Create two location groups of the same name with an agency alias category + String officeId = user.getOperatingOffice(); + String locationId = "LocGrpTestN1"; + String locationId2 = "LocGrpTestN2"; + createLocation(locationId, true, officeId); + createLocation(locationId2, true, officeId); + LocationCategory cat = new LocationCategory(officeId, "AliasTestCategory", "AliasIntegrationTesting"); + AssignedLocation assignLoc = new AssignedLocation(locationId, officeId, "AliasedId", 1, locationId); + AssignedLocation assignLoc2 = new AssignedLocation(locationId2, officeId, "AliasedId", 1, locationId); + LocationGroup group = new LocationGroup(new LocationGroup(cat, officeId, "LocationGroupTestIT", "IntegrationTesting", + "sharedLocAliasId", locationId, 123), Collections.singletonList(assignLoc)); + LocationGroup group2 = new LocationGroup(new LocationGroup(cat, officeId, "LocationGroupTestIT1", "IntegrationTesting1", + "sharedLocAliasId1", locationId, 123), Collections.singletonList(assignLoc2)); + ContentType contentType = Formats.parseHeader(Formats.JSON, LocationCategory.class); + String groupXml = Formats.format(contentType, group); + groupsToCleanup.add(group); + String categoryXml = Formats.format(contentType, cat); + categoriesToCleanup.add(cat); + registerCategory(cat); + //Create Category + given() + .log().ifValidationFails(LogDetail.ALL,true) + .accept(Formats.JSON) + .contentType(Formats.JSON) + .body(categoryXml) + .header("Authorization", user.toHeaderValue()) + .when() + .redirects().follow(true) + .redirects().max(3) + .post("/location/category") + .then() + .log().ifValidationFails(LogDetail.ALL,true) + .assertThat() + .statusCode(is(HttpServletResponse.SC_CREATED)); + + //Create Group + given() + .log().ifValidationFails(LogDetail.ALL,true) + .accept(Formats.JSON) + .contentType(Formats.JSON) + .body(groupXml) + .header("Authorization", user.toHeaderValue()) + .when() + .redirects().follow(true) + .redirects().max(3) + .post("/location/group") + .then() + .log().ifValidationFails(LogDetail.ALL,true) + .assertThat() + .statusCode(is(HttpServletResponse.SC_CREATED)); + //Create Group 2 + groupXml = Formats.format(contentType, group2); + given() + .log().ifValidationFails(LogDetail.ALL,true) + .accept(Formats.JSON) + .contentType(Formats.JSON) + .body(groupXml) + .header("Authorization", user.toHeaderValue()) + .when() + .redirects().follow(true) + .redirects().max(3) + .post("/location/group") + .then() + .log().ifValidationFails(LogDetail.ALL,true) + .assertThat() + .statusCode(is(HttpServletResponse.SC_BAD_REQUEST)) + .body("message", equalTo("Bad Request")) + .body("details.message", equalTo("Alias (AliasedId) would reference multiple locations. " + + "If you want to allow this, set the CWMSDB/Allow_multiple_locations_for_alias " + + "property to T for office id SPK. Note that this action will eliminate the " + + "ability to look up a location using the alias or any others that reference multiple locations.")); + //Read + given() + .log().ifValidationFails(LogDetail.ALL,true) + .accept(Formats.JSON) + .contentType(Formats.JSON) + .queryParam(OFFICE, officeId) + .queryParam(CATEGORY_ID, group.getLocationCategory().getId()) + .queryParam(CATEGORY_OFFICE_ID, CWMS_OFFICE) + .queryParam(GROUP_OFFICE_ID, officeId) + .when() + .redirects().follow(true) + .redirects().max(3) + .get("/location/group/" + group.getId()) + .then() + .log().ifValidationFails(LogDetail.ALL,true) + .assertThat() + .statusCode(is(HttpServletResponse.SC_OK)) + .body("office-id", equalTo(group.getOfficeId())) + .body("id", equalTo(group.getId())) + .body("description", equalTo(group.getDescription())) + .body("assigned-locations[0].location-id", equalTo(locationId)) + .body("assigned-locations[0].alias-id", equalTo("AliasedId")) + .body("assigned-locations[0].ref-location-id", equalTo(locationId)); + //Read + given() + .log().ifValidationFails(LogDetail.ALL,true) + .accept(Formats.JSON) + .contentType(Formats.JSON) + .queryParam(OFFICE, officeId) + .queryParam(CATEGORY_ID, group2.getLocationCategory().getId()) + .queryParam(CATEGORY_OFFICE_ID, officeId) + .queryParam(GROUP_OFFICE_ID, officeId) + .when() + .redirects().follow(true) + .redirects().max(3) + .get("/location/group/" + group2.getId()) + .then() + .log().ifValidationFails(LogDetail.ALL,true) + .assertThat() + .statusCode(is(HttpServletResponse.SC_NOT_FOUND)); + + //Delete Group + given() + .log().ifValidationFails(LogDetail.ALL,true) + .accept(Formats.JSON) + .contentType(Formats.JSON) + .header("Authorization", user.toHeaderValue()) + .queryParam(OFFICE, officeId) + .queryParam(CATEGORY_ID, cat.getId()) + .queryParam(CASCADE_DELETE, "true") + .when() + .redirects().follow(true) + .redirects().max(3) + .delete("/location/group/" + group.getId()) + .then() + .log().ifValidationFails(LogDetail.ALL,true) + .assertThat() + .statusCode(is(HttpServletResponse.SC_NO_CONTENT)); + + //Read Empty + given() + .log().ifValidationFails(LogDetail.ALL,true) + .accept(Formats.JSON) + .contentType(Formats.JSON) + .queryParam(OFFICE, officeId) + .queryParam(CATEGORY_ID, group.getLocationCategory().getId()) + .queryParam(CATEGORY_OFFICE_ID, officeId) + .queryParam(GROUP_OFFICE_ID, officeId) + .when() + .redirects().follow(true) + .redirects().max(3) + .get("/location/group/" + group.getId()) + .then() + .log().ifValidationFails(LogDetail.ALL,true) + .assertThat() + .statusCode(is(HttpServletResponse.SC_NOT_FOUND)); + //Read Empty + given() + .log().ifValidationFails(LogDetail.ALL,true) + .accept(Formats.JSON) + .contentType(Formats.JSON) + .queryParam(OFFICE, officeId) + .queryParam(CATEGORY_ID, group2.getLocationCategory().getId()) + .queryParam(CATEGORY_OFFICE_ID, officeId) + .queryParam(GROUP_OFFICE_ID, officeId) + .when() + .redirects().follow(true) + .redirects().max(3) + .get("/location/group/" + group2.getId()) + .then() + .log().ifValidationFails(LogDetail.ALL,true) + .assertThat() + .statusCode(is(HttpServletResponse.SC_NOT_FOUND)); + } + @ParameterizedTest @ValueSource(strings = {Formats.JSON, Formats.DEFAULT}) void test_create_read_delete_office_combinations(String format) throws Exception {