mirror of
https://github.com/OpenBankProject/OBP-API.git
synced 2026-02-06 13:07:02 +00:00
feature/json_schema_validation: implement props config schema
one props example:
json.schema.validation.rules=POST|PUT:/obp/v4.0.0|v3.1.0/banks/BANK_ID/fx
/obp/v4.0.0|v3.1.0/banks/BANK_ID/fx={"$schema":"http://json-schema.org/draft-07/schema","description":"The fix","examples":[{"bank_id":"gh.29.uk","from_currency_code":"EUR","to_currency_code":"USD","conversion_value":1.136305,"inverse_conversion_value":0.8800454103431737,"effective_date":"2017-09-19T00:00:00Z"}],"required":["bank_id","from_currency_code","to_currency_code","conversion_value","inverse_conversion_value","effective_date"],"title":"The Fix schema","type":"object","properties":{"bank_id":{"$id":"#/properties/bank_id","type":"string","title":"The bank_id schema","description":"An explanation about the purpose of this instance.","default":"","examples":["gh.29.uk"]},"from_currency_code":{"$id":"#/properties/from_currency_code","default":"","description":"An explanation about the purpose of this instance.","examples":["EUR"],"title":"The from_currency_code schema","enum":["EUR","YUAN"],"type":"string"},"to_currency_code":{"$id":"#/properties/to_currency_code","default":"","description":"An explanation about the purpose of this instance.","examples":["USD"],"title":"The to_currency_code schema","enum":["USD","YUAN"],"type":"string"},"conversion_value":{"$id":"#/properties/conversion_value","type":"number","title":"The conversion_value schema","description":"An explanation about the purpose of this instance.","default":0.0,"examples":[1.136305]},"inverse_conversion_value":{"$id":"#/properties/inverse_conversion_value","type":"number","title":"The inverse_conversion_value schema","description":"An explanation about the purpose of this instance.","default":0.0,"examples":[0.8800454103431737]},"effective_date":{"$id":"#/properties/effective_date","type":"string","title":"The effective_date schema","description":"An explanation about the purpose of this instance.","default":"","examples":["2017-09-19T00:00:00Z"]}},"additionalProperties":true}
This commit is contained in:
parent
2b4a14f856
commit
eb5a5bc824
@ -462,6 +462,13 @@
|
||||
<version>1.7.0</version>
|
||||
</dependency>
|
||||
|
||||
<!--json schema validation start-->
|
||||
<dependency>
|
||||
<groupId>com.networknt</groupId>
|
||||
<artifactId>json-schema-validator</artifactId>
|
||||
<version>1.0.45</version>
|
||||
</dependency>
|
||||
<!--json schema validation end-->
|
||||
</dependencies>
|
||||
|
||||
<build>
|
||||
|
||||
@ -41,17 +41,21 @@ import code.api.v4_0_0.APIMethods400
|
||||
import code.model.dataAccess.AuthUser
|
||||
import code.util.Helper.MdcLoggable
|
||||
import com.alibaba.ttl.TransmittableThreadLocal
|
||||
import com.fasterxml.jackson.databind.{JsonNode, ObjectMapper}
|
||||
import com.networknt.schema.{JsonSchema, JsonSchemaFactory, SpecVersionDetector, ValidationMessage}
|
||||
import com.openbankproject.commons.model.ErrorMessage
|
||||
import com.openbankproject.commons.util.{ApiVersion, ReflectUtils, ScannedApiVersion}
|
||||
import com.openbankproject.commons.util.{ApiVersion, Functions, ReflectUtils, ScannedApiVersion}
|
||||
import net.liftweb.common._
|
||||
import net.liftweb.http.rest.RestHelper
|
||||
import net.liftweb.http.{JsonResponse, LiftResponse, Req, S}
|
||||
import net.liftweb.http.{BadRequestResponse, JsonResponse, LiftResponse, Req, S}
|
||||
import net.liftweb.json.Extraction
|
||||
import net.liftweb.json.JsonAST.JValue
|
||||
import net.liftweb.util.Helpers
|
||||
import org.apache.commons.lang3.StringUtils
|
||||
|
||||
import scala.collection.immutable.List
|
||||
import scala.collection.mutable.ArrayBuffer
|
||||
import scala.jdk.CollectionConverters.iterableAsScalaIterableConverter
|
||||
import scala.math.Ordering
|
||||
|
||||
trait APIFailure{
|
||||
@ -424,12 +428,98 @@ trait OBPRestHelper extends RestHelper with MdcLoggable {
|
||||
serve(obpHandler)
|
||||
}
|
||||
|
||||
case class JsonSchemaValidator(methods: Array[String], url: String, schema: String) {
|
||||
import JsonSchemaValidator._
|
||||
|
||||
private val methodSet: Set[String] = methods.map(_.trim.toUpperCase)
|
||||
.filter(requestMethods.contains(_))
|
||||
.toSet
|
||||
|
||||
assert(methodSet.nonEmpty, s"json schema validation rules contains illegal methods: ${methods.mkString("|")}:$url")
|
||||
|
||||
private val jsonSchema: JsonSchema = {
|
||||
val schemaJson: JsonNode = mapper.readTree(schema)
|
||||
val factory = JsonSchemaFactory.getInstance(SpecVersionDetector.detect(schemaJson))
|
||||
factory.getSchema(schemaJson)
|
||||
}
|
||||
|
||||
private val urlPartMatchers: Array[String => Boolean] = {
|
||||
StringUtils.split(url, '/').map {
|
||||
case v if v.contains('|') => // or style: v3.1.0|v4.0.0
|
||||
val parts = StringUtils.split(v, "|")
|
||||
parts.contains(_:String)
|
||||
case v if v.forall(it => it < 'a' || it > 'z' ) => // placeholder style: BANK_ID
|
||||
Functions.truePredicate[String]
|
||||
case v =>
|
||||
v == _
|
||||
}
|
||||
}
|
||||
|
||||
def isRequestMatches(method: String, requestUrl: List[String]): Boolean = {
|
||||
methodSet.contains(method) &&
|
||||
requestUrl.size == urlPartMatchers.size && {
|
||||
urlPartMatchers.zip(requestUrl).forall(it => {
|
||||
val (fun, v) = it
|
||||
fun(v)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
def validatePayload(jsonContent: Array[Byte]): java.util.Set[ValidationMessage] = {
|
||||
val zson = mapper.readTree(jsonContent)
|
||||
jsonSchema.validate(zson)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
object JsonSchemaValidator {
|
||||
private val mapper = new ObjectMapper
|
||||
// regex, match this format string: "GET|POST|PUTV:/obp/v4.0.0|v3.1.0/banks/BANK_ID/fx"
|
||||
// group 1 is http method names, group 2 is url
|
||||
private val regex = """([|a-zA-Z]+?):([^,]+)""".r
|
||||
private val requestMethods = Set(
|
||||
"POST","PUT","DELETE","GET","HEAD","PATCH","TRACE","OPTIONS","CONNECT"
|
||||
)
|
||||
|
||||
// create JsonSchemaValidator with structured string, style like:
|
||||
// PUT|POST:/obp/v4.0.0|v3.1.0/banks/BANK_ID/fx
|
||||
def apply(toParseRule: String): JsonSchemaValidator = toParseRule match {
|
||||
case regex(methods , url) =>
|
||||
val schemaStr = APIUtil.getPropsValue(url).openOrThrowException(s"key '$url' is not found in props file, it is mandatory for 'json.schema.validation.rules'")
|
||||
JsonSchemaValidator(StringUtils.split(methods, "|"), url, schemaStr)
|
||||
case v => throw new RuntimeException(s"props 'json.schema.validation.rules' value contains illegal part: $v")
|
||||
}
|
||||
}
|
||||
|
||||
private val jsonSchemaRuleMatchers: Array[JsonSchemaValidator] = {
|
||||
APIUtil.getPropsValue("json.schema.validation.rules") match {
|
||||
case Full(v) =>
|
||||
StringUtils.split(v,",").map(JsonSchemaValidator(_))
|
||||
case _ => Array.empty[JsonSchemaValidator]
|
||||
}
|
||||
}
|
||||
|
||||
override protected def serve(handler: PartialFunction[Req, () => Box[LiftResponse]]) : Unit = {
|
||||
val obpHandler : PartialFunction[Req, () => Box[LiftResponse]] = {
|
||||
new PartialFunction[Req, () => Box[LiftResponse]] {
|
||||
def apply(r : Req) = {
|
||||
//Wraps the partial function with some logging
|
||||
handler(r)
|
||||
def apply(r : Req): () => Box[LiftResponse] = {
|
||||
|
||||
val validationError: Box[String] = for {
|
||||
body <- r.body
|
||||
validator <- jsonSchemaRuleMatchers.find(_.isRequestMatches(r.requestType.method, r.path.partPath))
|
||||
errors: Iterable[ValidationMessage] = validator.validatePayload(body).asScala
|
||||
if errors.nonEmpty
|
||||
} yield errors.mkString("; ")
|
||||
|
||||
validationError match {
|
||||
case Full(errorInfo) =>
|
||||
val errorResponse = s"""{"code":400,"message":"${ErrorMessages.InvalidRequestPayload} $errorInfo"}"""
|
||||
() => Full(BadRequestResponse(errorResponse))
|
||||
case _ =>
|
||||
//Wraps the partial function with some logging
|
||||
handler(r)
|
||||
}
|
||||
|
||||
}
|
||||
def isDefinedAt(r : Req) = handler.isDefinedAt(r)
|
||||
}
|
||||
|
||||
@ -56,6 +56,7 @@ object ErrorMessages {
|
||||
val InvalidMyDynamicEndpointUser = "OBP-09011: DynamicEndpoint can only be updated/deleted by the user who created it. Please try `Update/DELETE Dynamic Endpoint` endpoint"
|
||||
|
||||
val InvalidBankIdDynamicEntity = "OBP-09012: This is a bank level dynamic entity. Please specify a valid value for BANK_ID."
|
||||
val InvalidRequestPayload = "OBP-09013: Request body is invalid json structure."
|
||||
|
||||
|
||||
// General messages (OBP-10XXX)
|
||||
|
||||
Loading…
Reference in New Issue
Block a user