diff --git a/src/main/scala/com/newlibertie/pollster/DataAdapter.scala b/src/main/scala/com/newlibertie/pollster/DataAdapter.scala index 4cdd7d0..a29d8c7 100644 --- a/src/main/scala/com/newlibertie/pollster/DataAdapter.scala +++ b/src/main/scala/com/newlibertie/pollster/DataAdapter.scala @@ -1,7 +1,8 @@ package com.newlibertie.pollster -import java.sql.{Connection, DriverManager, ResultSet} +import java.sql.{Connection, DriverManager, ResultSet, SQLException, SQLTimeoutException} +import com.newlibertie.pollster.errorenum.{ApplicationError, DatabaseError} import com.newlibertie.pollster.impl.Poll import com.typesafe.scalalogging.LazyLogging import net.liftweb.json.DefaultFormats @@ -30,6 +31,15 @@ object DataAdapter extends LazyLogging { } } + private def executeUpdateQuery(sql: String): Any = { + try getConnection.createStatement().executeUpdate(sql) + catch { + case _: SQLException => DatabaseError.Access + case _: SQLTimeoutException => DatabaseError.Timeout + case _: Throwable => ApplicationError.ExceptionError + } + } + def createPoll(poll: Poll) = { val query = s""" @@ -54,11 +64,10 @@ object DataAdapter extends LazyLogging { |) """.stripMargin logger.info(query) - val statement = getConnection.createStatement - val numRows = statement.executeUpdate(query) + val numRows = executeUpdateQuery(query) if (numRows != 1) { logger.error("failed to insert " + query) - -1 + DatabaseError.ConstraintViolation } else { logger.info("poll.p.id: " + poll.p.id) @@ -76,11 +85,15 @@ object DataAdapter extends LazyLogging { | WHERE id = '$id' """.stripMargin logger.info(query) - val statement = getConnection.createStatement(ResultSet.TYPE_FORWARD_ONLY, ResultSet.CONCUR_READ_ONLY) - val rs: ResultSet = statement.executeQuery(query) + val rs: ResultSet = + getConnection.createStatement(ResultSet.TYPE_FORWARD_ONLY, ResultSet.CONCUR_READ_ONLY).executeQuery(query) + + // Comvert to Option form + if (!rs.first()) { logger.error("failed to retrieve using: " + query) - -1 + // TODO throw DatabaseError.RecordNotFound + throw new Error("dkfldsfd"); } else { val md = rs.getMetaData @@ -92,7 +105,15 @@ object DataAdapter extends LazyLogging { mutableMap } } - def updatePoll(poll:Poll): Any = { + + /** + * Update the poll. if success then return nothing else throw exception + * + * @param poll + */ + def updatePoll(poll:Poll):Unit = { + + val query = s""" |UPDATE nldb.polls @@ -106,9 +127,17 @@ object DataAdapter extends LazyLogging { |WHERE id = '${poll.p.id.get}' """.stripMargin logger.info(query) - getConnection.createStatement().executeUpdate(query) + val x = getConnection.createStatement().executeUpdate(query) + //x match { + // case Some() + // case None => Throw new ErrorEnum(XXX) + //} } - def deletePoll(id: String): Int = { - getConnection.createStatement().executeUpdate("DELETE FROM polls WHERE id = '" + id + "'") + def deletePoll(id: String):Unit = { + val x = executeUpdateQuery(s"DELETE FROM polls WHERE id = '$id'") + //x match { + // case Some() + // case None => Throw + //} } } diff --git a/src/main/scala/com/newlibertie/pollster/api/v1/PollApi.scala b/src/main/scala/com/newlibertie/pollster/api/v1/PollApi.scala index bcd47d0..d50a900 100644 --- a/src/main/scala/com/newlibertie/pollster/api/v1/PollApi.scala +++ b/src/main/scala/com/newlibertie/pollster/api/v1/PollApi.scala @@ -1,10 +1,13 @@ package com.newlibertie.pollster.api.v1 +import java.sql.{DatabaseMetaData, SQLException, SQLTimeoutException} + import akka.http.scaladsl.model.ContentTypes._ import akka.http.scaladsl.model.headers.`Content-Type` import akka.http.scaladsl.model.{ContentTypes, HttpEntity, HttpResponse, StatusCodes} import akka.http.scaladsl.server.Directives._ import akka.http.scaladsl.server.Route +import com.newlibertie.pollster.errorenum.{ApplicationError, ApplicationErrorEnum, BaseErrorEnum, DatabaseError, DatabaseErrorEnum} import com.newlibertie.pollster.impl.Poll import com.typesafe.scalalogging.LazyLogging @@ -23,9 +26,10 @@ object PollApi extends LazyLogging { get { parameters("id") { id: String => Poll.read(id) match { - case -1 => complete(StatusCodes.NotFound) - case -2 => complete(StatusCodes.BadRequest) case p: Poll => complete(HttpResponse(entity = p.toJsonString)) + case DatabaseError.RecordNotFound => complete(StatusCodes.NotFound) + case ApplicationError.ExceptionError => complete(StatusCodes.InternalServerError) + case _ => complete(StatusCodes.InternalServerError) } } } ~ @@ -70,12 +74,14 @@ object PollApi extends LazyLogging { delete { parameters("id") { id: String => Poll.read(id) match { - case -1 => complete(StatusCodes.NotFound) - case -2 => complete(StatusCodes.BadRequest) + case DatabaseError.RecordNotFound => complete(StatusCodes.NotFound) + case _:DatabaseErrorEnum#AEVal => complete(StatusCodes.BadRequest) + case _:BaseErrorEnum#AEVal => complete(StatusCodes.InternalServerError) case p: Poll => if (p.canDelete()) { p.deletePoll() match { case 1 => complete(StatusCodes.OK) - case _ => complete(StatusCodes.NotFound) + case 0 => complete(StatusCodes.NotFound) + case _ => complete(StatusCodes.InternalServerError) } } else complete(StatusCodes.NotAcceptable) } diff --git a/src/main/scala/com/newlibertie/pollster/errorenum/ApplicationErrorEnum.scala b/src/main/scala/com/newlibertie/pollster/errorenum/ApplicationErrorEnum.scala new file mode 100644 index 0000000..e964a31 --- /dev/null +++ b/src/main/scala/com/newlibertie/pollster/errorenum/ApplicationErrorEnum.scala @@ -0,0 +1,18 @@ +package com.newlibertie.pollster.errorenum + +/** + * Class to define errors related to application logic + * @param errEnumName The unique name of the derived class + * @param initial the id of the first enum variable in the class with name $errEnumName + * @param capacity the number of unique ids for the enum variables in the class with name $errEnumName + */ +class ApplicationErrorEnum (errEnumName: String, initial:Int, capacity:Int) extends BaseErrorEnum(errEnumName, initial, capacity) + +/** + * companion object to define error variables for application errors + */ +object ApplicationError extends ApplicationErrorEnum("ApplicationError", 0, 1000){ + val Unknown = AEVal(0, "Unknown") + val AssertionError = AEVal(1, "AssertionError") + val ExceptionError = AEVal(2, "ExceptionError") +} diff --git a/src/main/scala/com/newlibertie/pollster/errorenum/BaseErrorEnum.scala b/src/main/scala/com/newlibertie/pollster/errorenum/BaseErrorEnum.scala new file mode 100644 index 0000000..7ab3266 --- /dev/null +++ b/src/main/scala/com/newlibertie/pollster/errorenum/BaseErrorEnum.scala @@ -0,0 +1,63 @@ +package com.newlibertie.pollster.errorenum + +import scala.collection.mutable + +/*** + * A class at the root of all error enum derivations + * responsible for validating uniqueness of the name of the errEnumName and non-overlapping id ranges + * registers the EnumClass + * @param errEnumName The unique name of the derived class + * @param initial the id of the first enum variable in the class with name $errEnumName + * @param capacity the number of unique ids for the enum variables in the class with name $errEnumName + */ + +// TODO : Is there a benefit in having class also extend from or implement Exception or Throwable + +class BaseErrorEnum(val errEnumName: String, val initial:Int, val capacity:Int) extends Enumeration { // with Throwable { + require(BaseErrorEnum.register(errEnumName, initial, capacity), "Failed to register: "+errEnumName) + type AppErr = AEVal + import scala.language.implicitConversions + implicit def valueAppErrVal(x: Value): AEVal = x.asInstanceOf[AEVal] + + case class AEVal(i: Int, name: String) extends super.Val(initial+i, name){ + protected def nextId: Int = BaseErrorEnum.super.Value.nextId + } + + protected object AEVal{ + protected def generateName(i: Int): String = {s"$errEnumName.Error#$i"} + protected def generateName(s: String): String = {s"$errEnumName.$s"} + def apply(i:Int):AEVal ={ + val name = AEVal.generateName(i) + require(BaseErrorEnum.super.values.find(_.toString == name) == None, "Duplicate AEVal.name: " + name) + AEVal(i, name) + } + def apply(name: String):AEVal = { + val name2 = generateName(name) + require(BaseErrorEnum.super.values.find(_.toString == name2) == None, "Duplicate AEVal.name: " + name2) + AEVal(nextId-initial, name2) + } + def apply():AEVal = { + AEVal(nextId-initial) + } + } +} + +/** + * Helping Object providing validations and register each EnumClass derived from BaseErrorEnum + */ +private object BaseErrorEnum { + class ErrEnumRange(val initial: Int, val capacity: Int) + private val registry: mutable.Map[String, ErrEnumRange] = new mutable.HashMap + def register(errEnumName: String, initial:Int, capacity:Int):Boolean = { + if (registry.contains(errEnumName) || + (registry.valuesIterator.exists(er => er.initial < (initial + capacity) && + initial < (er.initial + er.capacity)))){ + false + } + else + { + registry(errEnumName) = new ErrEnumRange(initial, capacity) + true + } + } +} diff --git a/src/main/scala/com/newlibertie/pollster/errorenum/DatabaseErrorEnum.scala b/src/main/scala/com/newlibertie/pollster/errorenum/DatabaseErrorEnum.scala new file mode 100644 index 0000000..e2e16c6 --- /dev/null +++ b/src/main/scala/com/newlibertie/pollster/errorenum/DatabaseErrorEnum.scala @@ -0,0 +1,23 @@ +package com.newlibertie.pollster.errorenum + +/** + * Class to define errors related to database + * @param errEnumName The unique name of the derived class + * @param initial the id of the first enum variable in the class with name $errEnumName + * @param capacity the number of unique ids for the enum variables in the class with name $errEnumName + */ +class DatabaseErrorEnum(errEnumName: String, initial:Int, capacity:Int) extends BaseErrorEnum(errEnumName, initial, capacity) + +/** + * companion object to define error variables for database errors + */ +object DatabaseError extends DatabaseErrorEnum("DatabaseError", 1000, 1000){ + val RecordNotFound = AEVal(0, "RecordNotFound") + val ConstraintViolation = AEVal(1, "ConstraintViolation") + val RecordIntegrityError = AEVal(2, "RecordIntegrityError") + val ConnectIdentifierResolution = AEVal(3, "ConnectIdentifierResolution") + val InternalCodeError = AEVal(4, "InternalCodeError") + val DataTypeMisMatch = AEVal(5, "DataTypeMisMatch") + val Access = AEVal(6, "Access") + val Timeout = AEVal(7, "Timeout") +} diff --git a/src/main/scala/com/newlibertie/pollster/impl/Poll.scala b/src/main/scala/com/newlibertie/pollster/impl/Poll.scala index a86f520..a773028 100644 --- a/src/main/scala/com/newlibertie/pollster/impl/Poll.scala +++ b/src/main/scala/com/newlibertie/pollster/impl/Poll.scala @@ -4,6 +4,7 @@ import java.math.BigInteger import java.util.Date import com.newlibertie.pollster.DataAdapter +import com.newlibertie.pollster.errorenum.{ApplicationError, BaseErrorEnum, DatabaseError, DatabaseErrorEnum} import com.typesafe.scalalogging.LazyLogging import net.liftweb.json._ @@ -55,9 +56,10 @@ object Poll extends LazyLogging { catch { case ex: Exception => logger.error(s"Error constructing poll Object from id=${id}: ${ex.getMessage}") - -2 + ApplicationError.ExceptionError } - case _ => -1 + case DatabaseError.RecordNotFound => DatabaseError.RecordNotFound + case _ => ApplicationError.Unknown } } } @@ -79,7 +81,7 @@ class Poll(val p:PollParameters, val cp:CryptographicParameters = new Cryptograp def canOpen():Boolean = { p.opening_ts after new Date() } - def deletePoll(): Int = { + def deletePoll(): Any = { DataAdapter.deletePoll(p.id.get) } def toJsonString: String = { diff --git a/src/test/scala/com/newlibertie/pollster/api/v1/PollApiSpec.scala b/src/test/scala/com/newlibertie/pollster/api/v1/PollApiSpec.scala index dbbfa2c..14094c0 100644 --- a/src/test/scala/com/newlibertie/pollster/api/v1/PollApiSpec.scala +++ b/src/test/scala/com/newlibertie/pollster/api/v1/PollApiSpec.scala @@ -3,7 +3,6 @@ package com.newlibertie.pollster.api.v1 import akka.http.scaladsl.model.{ContentTypes, StatusCodes} import akka.http.scaladsl.server.Route import akka.http.scaladsl.testkit.ScalatestRouteTest -//import com.newlibertie.pollster.DataAdapter.logger import com.newlibertie.pollster.impl.Poll import org.scalatest._ import com.typesafe.scalalogging.LazyLogging diff --git a/src/test/scala/com/newlibertie/pollster/errorenum/ApplicationErrorEnumTest.scala b/src/test/scala/com/newlibertie/pollster/errorenum/ApplicationErrorEnumTest.scala new file mode 100644 index 0000000..ee39a0e --- /dev/null +++ b/src/test/scala/com/newlibertie/pollster/errorenum/ApplicationErrorEnumTest.scala @@ -0,0 +1,68 @@ +package com.newlibertie.pollster.errorenum + +import org.scalatest._ +import com.typesafe.scalalogging.LazyLogging +class ApplicationErrorEnumTest extends WordSpec with Matchers with LazyLogging { + + class TestApplicationErrorEnum (errEnumName: String, initial:Int, capacity:Int) extends BaseErrorEnum(errEnumName, initial, capacity) + object TestApplicationErrorEnum extends TestApplicationErrorEnum("TestApplicationError", 0, 10){ + val Unknown = AEVal(0, "Unknown") + } + + "the enum creation" should { + "detect duplicate enum name" in { + class TestApplicationErrorEnum2 (errEnumName: String, initial:Int, capacity:Int) extends BaseErrorEnum(errEnumName, initial, capacity) + object TestApplicationErrorEnum2 extends TestApplicationErrorEnum2("TestApplicationError", 0, 10){ + val Unknown = AEVal(0, "Unknown") + } + var stat = -1 + try { + var err:AnyRef = TestApplicationErrorEnum.Unknown + err = TestApplicationErrorEnum2.Unknown + } + catch { + case ex: Exception => + logger.info(ex.getMessage) + stat = if (ex.getMessage().equalsIgnoreCase("requirement failed: Failed to register: TestApplicationError")) 0 else -1 + } + stat shouldEqual 0 + } + "detect overlapping id range" in { + var stat = -1 + class TestApplicationErrorEnum3 (errEnumName: String, initial:Int, capacity:Int) extends BaseErrorEnum(errEnumName, initial, capacity) + object TestApplicationErrorEnum3 extends TestApplicationErrorEnum3("TestApplicationError3", 0, 10){ + val Unknown = AEVal(0, "Unknown") + } + try { + var err:AnyRef = TestApplicationErrorEnum.Unknown + err = TestApplicationErrorEnum3.Unknown + } + catch { + case ex: Exception => + logger.info(ex.getMessage) + stat = if (ex.getMessage().equalsIgnoreCase("requirement failed: Failed to register: TestApplicationError3")) 0 else -1 + case t: Throwable => + logger.info(t.getLocalizedMessage) + stat = -1 + } + stat shouldEqual 0 + } + "detect duplicate id" in { + var stat = -1 + class TestApplicationErrorEnum4 (errEnumName: String, initial:Int, capacity:Int) extends BaseErrorEnum(errEnumName, initial, capacity) + try { + object TestApplicationErrorEnum4 extends TestApplicationErrorEnum4("TestApplicationError4", 10, 10){ + val Unknown = AEVal(0, "Unknown") + val Unknown2 = AEVal(0, "Unknown2") + } + TestApplicationErrorEnum4.Unknown + } + catch { + case t: Throwable => + logger.info(t.getMessage) + stat = if (t.getMessage.startsWith("assertion failed: Duplicate id: ")) 0 else -1 + } + stat shouldEqual 0 + } + } +}