package com.restapi.controllers import com.fasterxml.jackson.core.JsonParser import com.fasterxml.jackson.databind.DeserializationContext import com.fasterxml.jackson.databind.JsonDeserializer import com.fasterxml.jackson.databind.JsonNode import com.fasterxml.jackson.databind.annotation.JsonDeserialize import com.restapi.domain.* import com.restapi.domain.Session.currentRoles import com.restapi.domain.Session.currentUser import com.restapi.domain.Session.database import com.restapi.domain.Session.findDataModelByEntityAndUniqId import com.restapi.domain.Session.objectMapper import com.restapi.integ.Scripting import io.ebean.RawSqlBuilder import io.javalin.http.BadRequestResponse import io.javalin.http.Context import io.javalin.http.HttpStatus import io.javalin.http.bodyAsClass import org.slf4j.LoggerFactory import java.time.LocalDate import java.time.LocalDateTime import java.time.LocalTime import java.time.format.DateTimeFormatter import java.time.format.DateTimeParseException enum class QueryParamType { STRING, NUMBER, DATETIME, DATE } @JsonDeserialize(using = QueryParamDeSerializer::class) sealed class QueryParam { data class StrParam(val str: String) : QueryParam() data class NumberParam(val nbr: Number) : QueryParam() data class BooleanParam(val bool: Boolean) : QueryParam() data class ComplexParam(val type: QueryParamType, val value: String) : QueryParam() { fun getValueComplex(): Any { return when (type) { QueryParamType.STRING -> value QueryParamType.NUMBER -> (if (value.matches(Regex("\\d+"))) value.toLongOrNull() else value.toDoubleOrNull()) ?: throw IllegalArgumentException("$value is not a number") QueryParamType.DATETIME -> try { LocalDateTime.parse( value, DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss") ) } catch (e: DateTimeParseException) { throw IllegalArgumentException("unable to parse $value as datetime") } QueryParamType.DATE -> try { LocalDate.parse(value, DateTimeFormatter.ofPattern("yyyy-MM-dd")) } catch (e: DateTimeParseException) { throw IllegalArgumentException("unable to parse $value as date") } } } } fun getValue(): Any { return when (this) { is ComplexParam -> getValueComplex() is StrParam -> str is NumberParam -> nbr is BooleanParam -> bool } } } class QueryParamDeSerializer : JsonDeserializer() { override fun deserialize(p: JsonParser, ctxt: DeserializationContext?): QueryParam { val node = p.readValueAsTree() return if (node.isTextual) { QueryParam.StrParam(node.asText()) } else if (node.isNumber) { QueryParam.NumberParam(node.numberValue()) } else if (node.isBoolean) { QueryParam.BooleanParam(node.booleanValue()) } else if (node.isObject) { QueryParam.ComplexParam( QueryParamType.valueOf(node.get("type").textValue()), node.get("value").textValue(), ) } else { throw BadRequestResponse("unable to find valid query param for $node") } } } object Entities { private val logger = LoggerFactory.getLogger("Entities") private val OK = mapOf("status" to true) fun delete(ctx: Context) { val e = database.findDataModelByEntityAndUniqId(ctx.pathParam("entity"), ctx.pathParam("id")) e.deletedBy = Session.currentUser() e.deletedOn = LocalDateTime.now() e.update() e.delete() ctx.json(OK) } fun patch(ctx: Context) { val e = database.findDataModelByEntityAndUniqId(ctx.pathParam("entity"), ctx.pathParam("id")) val pv = ctx.bodyAsClass>() verifyKeys(pv) pv.forEach { (key, value) -> e.data[key] = value; } e.update() ctx.json(OK) } fun update(ctx: Context) { val purgeExisting = ctx.queryParam("purge")?.toBooleanStrictOrNull() == true val e = database.findDataModelByEntityAndUniqId(ctx.pathParam("entity"), ctx.pathParam("id")) val newData = ctx.bodyAsClass>() verifyKeys(newData) if (purgeExisting) { e.data.clear(); } e.data.putAll(newData) e.update() ctx.json(OK) } private fun verifyKeys(newData: Map) { newData.keys.forEach { key -> if (!SafeStringDeserializer.isSafe(key)) throw IllegalArgumentException("$key is invalid from $newData ") } } fun search(ctx: Context) { val sql = ctx.bodyAsClass() verifyKeys(sql.params) val entity = ctx.pathParam("entity").lowercase() val noCreatedFilter = currentRoles().contains("ROLE_ADMIN") || sql.createdBy.isNullOrEmpty() val createdFilter = if (noCreatedFilter) "" else "and created_by = :cBy" val searchJsonMap = sql.params.map { e -> Pair(e.key, e.value.getValue()) } .filter { val second = it.second if (second is String) { second.isNotEmpty() } else { true } } .toMap() logger.warn("convert ${sql.params} to $searchJsonMap") val fl = database.find(DataModel::class.java) .setRawSql( RawSqlBuilder.parse( """ select sys_pk, deleted_on, current_approval_level, required_approval_levels, deleted, version, created_at, modified_at, deleted_by, approval_status, tags, comments, tenant_id, unique_identifier, entity_name, data, created_by, modified_by from data_model where entity_name = :e and created_at between :from and :to and data @> cast(:search as jsonb) and deleted = false $createdFilter order by ${sql.orderBy} """.trimIndent() ).create() ) .setParameter("from", sql.dateRange.first()) .setParameter("to", sql.dateRange.last().plusDays(1)) .setParameter("e", entity) .setParameter("search", objectMapper.writeValueAsString(searchJsonMap)) .apply { if (!noCreatedFilter) { logger.warn("Set Created By Filter to ${currentUser()}") setParameter("cBy", currentUser()) } } .findList() logger.warn("Search jsonMap [$searchJsonMap] => ${fl.size} entries") ctx.json(fl) } fun getAll(ctx: Context) { val entity = ctx.pathParam("entity").uppercase() val pageNo = ctx.queryParam("pageNo")?.toInt() ?: 1 val perPage = ctx.queryParam("perPage")?.toInt() ?: 100 val cnt = database.find(DataModel::class.java) .where() .eq("entityName", entity.lowercase()) .setFirstRow((pageNo - 1) * perPage) .setMaxRows(perPage) .orderBy("sysPk desc") .findPagedList() ctx.json(cnt) } fun getNextSeqNo(ctx: Context) { val entity = ctx.pathParam("entity").uppercase() val prefix = "$entity/" val plantId = ctx.queryParam("plantId") ?: throw BadRequestResponse("plantId not sent") val plant = database.find(Plant::class.java) .where() .eq("plantId", plantId) .findOne() ?: throw BadRequestResponse("plant missing for $plantId") val inventoryPrefix = plant.prefixes?.get(entity) ?: prefix val cnt = (database.find(DataModel::class.java) .where() .eq("entityName", entity.lowercase()) .eq("data->>'plantId'", plantId) .findCount() + 1) .toString() .padStart(6, '0') val seq = SequenceNumber(inventoryPrefix + cnt) ctx.json(seq).status(HttpStatus.OK) } fun view(it: Context) { database.save( AuditLog().apply { auditType = AuditType.VIEW entity = it.pathParam("entity") uniqueIdentifier = it.pathParam("id") } ) it.json( database.findDataModelByEntityAndUniqId(it.pathParam("entity"), it.pathParam("id")) ) } data class Execute(val script: String, val fn: String, val params: Map) fun execute(ctx: Context) { val entity = ctx.pathParam("entity") val body = ctx.bodyAsClass() //may be approval flow is configured? val setupEntity = database.find(EntityModel::class.java) .where() .eq("name", entity) .findOne() ctx.json( Scripting.execute(body.script, body.fn, body.params, setupEntity) ) } fun create(ctx: Context) { val entity = ctx.pathParam("entity") //may be approval flow is configured? val setupEntity = database.find(EntityModel::class.java) .where() .eq("name", entity) .findOne() Session.creatSeq(entity) val dataModel = ctx.bodyAsClass().apply { this.entityName = entity if (this.uniqueIdentifier.isEmpty()) { this.uniqueIdentifier = Session.nextUniqId(entity) } this.approvalStatus = ApprovalStatus.APPROVED } verifyKeys(dataModel.data) database.save( dataModel.apply { if (setupEntity != null) { val allowedFields = setupEntity.allowedFields.map { it.lowercase() } if (allowedFields.isNotEmpty()) { val moreFields = dataModel.data.keys.map { it.lowercase() }.filter { !allowedFields.contains(it) } if (moreFields.isNotEmpty()) { logger.warn("Data Keys = ${dataModel.data.keys} is more than $allowedFields, extra fields = $moreFields") throw BadRequestResponse("data contains more fields than allowed") } setupEntity.allowedFieldTypes.forEach { (key, expectedType) -> val valueFromUser = dataModel.data[key] ?: return@forEach val isDate = expectedType.equals("date", ignoreCase = true) val isDateTime = expectedType.equals("datetime", ignoreCase = true) val isTime = expectedType.equals("time", ignoreCase = true) if (isDate || isDateTime || isTime) { //this should be a string of a particular format if (valueFromUser !is String) { throw BadRequestResponse("field $key, is of type ${valueFromUser.javaClass.simpleName} expected $expectedType") } else { val dtPattern = Regex("^\\d{4}-\\d{2}-\\d{2}$") val dtmPattern = Regex("^\\d{4}-\\d{2}-\\d{2}\\s\\d{2}:\\d{2}$") val timePattern = Regex("^\\d{2}:\\d{2}$") if (isDate && !dtPattern.matches(valueFromUser) && !isValidDate(valueFromUser) ) { throw BadRequestResponse("field $key, is of type ${valueFromUser.javaClass.simpleName} expected $expectedType") } if (isDateTime && !dtmPattern.matches(valueFromUser) && !isValidDateTime(valueFromUser) ) { throw BadRequestResponse("field $key, is of type ${valueFromUser.javaClass.simpleName} expected $expectedType") } if (isTime && !timePattern.matches(valueFromUser) && !isValidTime(valueFromUser) ) { throw BadRequestResponse("field $key, is of type ${valueFromUser.javaClass.simpleName} expected $expectedType") } } } if (valueFromUser.javaClass.simpleName != expectedType) { throw BadRequestResponse("field $key, is of type ${valueFromUser.javaClass.simpleName} expected $expectedType") } } } } } ) if (setupEntity != null && !setupEntity.postSaveScript.isNullOrEmpty()) { Scripting.execute(setupEntity.postSaveScript!!, "postSave", dataModel, setupEntity) } database.save( AuditLog().apply { this.auditType = AuditType.CREATE this.entity = entity this.uniqueIdentifier = dataModel.uniqueIdentifier this.data = dataModel.data } ) ctx.json(OK) } private fun isValidDate(f: String) = try { LocalDate.parse(f, DateTimeFormatter.ofPattern("yyyy-MM-dd")) true } catch (e: Exception) { false } private fun isValidDateTime(f: String) = try { LocalDateTime.parse(f, DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm")) true } catch (e: Exception) { false } private fun isValidTime(f: String) = try { LocalTime.parse(f, DateTimeFormatter.ofPattern("HH:mm")) true } catch (e: Exception) { false } } data class SearchParams( val params: Map = mapOf(), val createdBy: String?, val dateRange: List = listOf(LocalDate.now().minusDays(7), LocalDate.now()), val orderBy: String = "sysPk asc" ) data class SequenceNumber(val number: String)