diff --git a/cwms-data-api/src/main/java/cwms/cda/ApiServlet.java b/cwms-data-api/src/main/java/cwms/cda/ApiServlet.java index 87c1b1150..37af31686 100644 --- a/cwms-data-api/src/main/java/cwms/cda/ApiServlet.java +++ b/cwms-data-api/src/main/java/cwms/cda/ApiServlet.java @@ -212,6 +212,7 @@ import javax.sql.DataSource; import org.apache.http.entity.ContentType; import org.jetbrains.annotations.NotNull; +import org.jooq.exception.DataAccessException; import org.owasp.html.HtmlPolicyBuilder; import org.owasp.html.PolicyFactory; @@ -380,6 +381,24 @@ public void init() { CdaError re = new CdaError(e.getMessage()); ctx.status(HttpServletResponse.SC_BAD_REQUEST).json(re); }) + .exception(DataAccessException.class, (e, ctx) -> { + // Whatever Dao is causing this exception to be thrown should be modified. + // The preferred pattern is for the Dao to catch DataAccessExceptions exceptions + // and for the dao to inspect the Oracle error code or error message as necessary + // to transform DataAccessExceptions (and their SQLException causes) + // into specific and appropriate exceptions with + // messages that are helpful and meaningful to end-users. + + // CdaError does not include the Oracle exception message b/c this block catches + // all unhandled DataAccessExceptions and we don't know what is in the message + // it is unknown if the message would be safe/appropriate for users to see. + CdaError errResponse = new CdaError("Database Error"); + logger.atWarning().withCause(e).log("error on request[%s]: %s", + errResponse.getIncidentIdentifier(), ctx.req.getRequestURI()); + ctx.status(500); + ctx.contentType(ContentType.APPLICATION_JSON.toString()); + ctx.json(errResponse); + }) .exception(Exception.class, (e, ctx) -> { CdaError errResponse = new CdaError("System Error"); logger.atWarning().withCause(e).log("error on request[%s]: %s", diff --git a/cwms-data-api/src/main/java/cwms/cda/api/errors/NoDataRateException.java b/cwms-data-api/src/main/java/cwms/cda/api/errors/NoDataRateException.java new file mode 100644 index 000000000..1be35d52e --- /dev/null +++ b/cwms-data-api/src/main/java/cwms/cda/api/errors/NoDataRateException.java @@ -0,0 +1,21 @@ +package cwms.cda.api.errors; + +import java.sql.SQLException; +import java.util.LinkedHashMap; +import java.util.logging.Level; + +public class NoDataRateException extends RateException{ + // 404 is Not Found - this isn't quite right b/c the rate() or reverse-rate() isn't failing b/c + // the ts isn't found its failing b/c a necessary rating table isn't found. + // 424 is Failed Dependency - this is closer to what we want, but it's not quite right either. + + // 422 is Unprocessable Entity. I think this is the closest. + + public static final int HTTP_ERROR_CODE = 422; + + public NoDataRateException(String message, SQLException cause) { + super(message, DATABASE_SOURCE, "Error performing rate function: " + message, + HTTP_ERROR_CODE, Level.INFO, new LinkedHashMap<>(), cause); + } + +} diff --git a/cwms-data-api/src/main/java/cwms/cda/api/errors/RateException.java b/cwms-data-api/src/main/java/cwms/cda/api/errors/RateException.java index 6e0c3f009..69504204c 100644 --- a/cwms-data-api/src/main/java/cwms/cda/api/errors/RateException.java +++ b/cwms-data-api/src/main/java/cwms/cda/api/errors/RateException.java @@ -24,16 +24,23 @@ package cwms.cda.api.errors; +import java.io.Serializable; import java.sql.SQLException; -import java.util.HashMap; +import java.util.LinkedHashMap; +import java.util.Map; import java.util.logging.Level; import javax.servlet.http.HttpServletResponse; -public final class RateException extends ApplicationException { +public class RateException extends ApplicationException { private static final Level LOG_LEVEL = Level.INFO; + public RateException(String message, String source, String cdaErrorMessage, int cdaHttpErrorCode, + Level logLevel, Map details, Throwable cause) { + super(message, source, cdaErrorMessage, cdaHttpErrorCode, logLevel, details, cause); + } + public RateException(String message, SQLException cause) { super(message, DATABASE_SOURCE, "Error performing rate function: " + message, - HttpServletResponse.SC_INTERNAL_SERVER_ERROR, LOG_LEVEL, new HashMap<>(), cause); + HttpServletResponse.SC_INTERNAL_SERVER_ERROR, LOG_LEVEL, new LinkedHashMap<>(), cause); } } diff --git a/cwms-data-api/src/main/java/cwms/cda/data/dao/RateDao.java b/cwms-data-api/src/main/java/cwms/cda/data/dao/RateDao.java index ca22b869c..1b45b8564 100644 --- a/cwms-data-api/src/main/java/cwms/cda/data/dao/RateDao.java +++ b/cwms-data-api/src/main/java/cwms/cda/data/dao/RateDao.java @@ -26,6 +26,7 @@ import static java.util.stream.Collectors.toList; +import cwms.cda.api.errors.NoDataRateException; import cwms.cda.api.errors.RateException; import cwms.cda.data.dto.CwmsId; import cwms.cda.data.dto.TimeSeries; @@ -44,6 +45,7 @@ import org.jooq.ConnectionCallable; import org.jooq.DSLContext; import org.jooq.exception.DataAccessException; +import org.jspecify.annotations.Nullable; import usace.cwms.db.jooq.codegen.packages.CWMS_RATING_PACKAGE; import usace.cwms.db.jooq.codegen.udt.records.DATE_TABLE_TYPE; import usace.cwms.db.jooq.codegen.udt.records.DOUBLE_TAB_T; @@ -72,7 +74,7 @@ public RatedOutput rate(String officeId, String ratingId, RateInputValues input) STR_TAB_T unitsTab = new STR_TAB_T(input.getInputUnits()); unitsTab.add(input.getOutputUnit()); return CWMS_RATING_PACKAGE.call_RATE(context.configuration(), ratingId, - inputValues, unitsTab, formatBool(input.getRound()), ratingDates, null, "UTC", officeId); + inputValues, unitsTab, formatBool(input.getRound()), ratingDates, null, "UTC", officeId); }); return new RatedOutputValues(CwmsId.buildCwmsId(officeId, ratingId), outputValues, input.getOutputUnit()); } @@ -172,15 +174,31 @@ static RuntimeException handleRateDbError(DataAccessException ex) { if (cause instanceof SQLException) { int errorCode = ((SQLException) cause).getErrorCode(); if (errorCode == 20019 || errorCode == 20998) { - String localizedMessage = cause.getLocalizedMessage(); - String[] parts = localizedMessage.split("\n"); - String message = parts[0]; - int index = message.indexOf(":"); - if (index >= 0) { - retval = new RateException(message.substring(index + 1), (SQLException) cause); + String message = getMessage(cause); + if( message != null ) { + retval = new RateException(message, (SQLException) cause); + } + } else if (errorCode == 1403){ + String message = getMessage(cause); + if( message != null ) { + // firstMessage may be like "no data found" + // or "Table rating has no rating points" + retval = new NoDataRateException(message, (SQLException) cause); } } } return retval; } + + private static @Nullable String getMessage(Throwable cause) { + String firstMessage = null; + String localizedMessage = cause.getLocalizedMessage(); + String[] parts = localizedMessage.split("\n"); + String message = parts[0]; + int index = message.indexOf(":"); + if (index >= 0) { + firstMessage = message.substring(index + 1); + } + return firstMessage; + } }