Skip to content

Commit ff14076

Browse files
authored
Merge pull request #323 from librecaptcha/replace-json4s-with-jsoniter-15242701020388036802
Refactor to replace json4s with jsoniter-scala
2 parents 7af0836 + 4ad3129 commit ff14076

5 files changed

Lines changed: 137 additions & 98 deletions

File tree

build.sbt

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,8 @@ lazy val root = (project in file(".")).settings(
1414
name := "LibreCaptcha",
1515
libraryDependencies += "com.sksamuel.scrimage" % "scrimage-core" % "4.3.10",
1616
libraryDependencies += "com.sksamuel.scrimage" % "scrimage-filters" % "4.3.10",
17-
libraryDependencies += "io.github.json4s" %% "json4s-jackson" % "4.1.0"
17+
libraryDependencies += "com.github.plokhotnyuk.jsoniter-scala" %% "jsoniter-scala-core" % "2.38.9",
18+
libraryDependencies += "com.github.plokhotnyuk.jsoniter-scala" %% "jsoniter-scala-macros" % "2.38.9" % "provided"
1819
)
1920

2021
Compile / unmanagedResourceDirectories += { baseDirectory.value / "lib" }

src/main/scala/lc/core/captchaProviders.scala

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -36,7 +36,7 @@ class CaptchaProviders(config: Config) {
3636
if configValue.allowedMedia.contains(param.media)
3737
if configValue.allowedInputType.contains(param.input_type)
3838
if configValue.allowedSizes.contains(param.size)
39-
} yield (configValue.name, configValue.config)
39+
} yield (configValue.name, configValue.config.string)
4040

4141
val providerFilter = for {
4242
providerValue <- configFilter
Lines changed: 50 additions & 61 deletions
Original file line numberDiff line numberDiff line change
@@ -1,20 +1,13 @@
11
package lc.core
22

33
import scala.io.Source.fromFile
4-
import org.json4s.{DefaultFormats, JValue, JObject, JField, JString}
5-
import org.json4s.jackson.JsonMethods.{parse, render, pretty}
6-
import org.json4s.JsonDSL._
7-
import org.json4s.StringInput
8-
import org.json4s.jvalue2monadic
9-
import org.json4s.jvalue2extractable
4+
import com.github.plokhotnyuk.jsoniter_scala.core._
105
import java.io.{FileNotFoundException, File, PrintWriter}
116
import java.{util => ju}
127
import lc.misc.HelperFunctions
138

149
class Config(configFilePath: String) {
1510

16-
import Config.formats
17-
1811
private val configString =
1912
try {
2013
val configFile = fromFile(configFilePath)
@@ -40,8 +33,8 @@ class Config(configFilePath: String) {
4033
}
4134
}
4235

43-
private val configJson = parse(configString)
44-
private val configFields: ConfigField = configJson.extract[ConfigField]
36+
private val appConfig = readFromString[AppConfig](configString)
37+
private val configFields: ConfigField = appConfig.toConfigField
4538

4639
val port: Int = configFields.portInt.getOrElse(8888)
4740
val address: String = configFields.address.getOrElse("0.0.0.0")
@@ -53,68 +46,64 @@ class Config(configFilePath: String) {
5346
val corsHeader: String = configFields.corsHeader.getOrElse("")
5447
val maxAttempts: Int = Math.max(1, (configFields.maxAttemptsRatioFloat.getOrElse(0.01f) * bufferCount).toInt)
5548

56-
private val captchaConfigJson = (configJson \ "captchas")
57-
val captchaConfigTransform: JValue = captchaConfigJson transformField { case JField("config", JObject(config)) =>
58-
("config", JString(config.toString))
59-
}
60-
val captchaConfig: List[CaptchaConfig] = captchaConfigTransform.extract[List[CaptchaConfig]]
49+
val captchaConfig: List[CaptchaConfig] = appConfig.captchas
6150
val allowedLevels: Set[String] = captchaConfig.flatMap(_.allowedLevels).toSet
6251
val allowedMedia: Set[String] = captchaConfig.flatMap(_.allowedMedia).toSet
6352
val allowedInputType: Set[String] = captchaConfig.flatMap(_.allowedInputType).toSet
6453

6554
HelperFunctions.setSeed(seed)
6655

6756
private def getDefaultConfig(): String = {
68-
val defaultConfigMap =
69-
(AttributesEnum.RANDOM_SEED.toString -> new ju.Random().nextInt()) ~
70-
(AttributesEnum.PORT.toString -> 8888) ~
71-
(AttributesEnum.ADDRESS.toString -> "0.0.0.0") ~
72-
(AttributesEnum.CAPTCHA_EXPIRY_TIME_LIMIT.toString -> 5) ~
73-
(AttributesEnum.BUFFER_COUNT.toString -> 1000) ~
74-
(AttributesEnum.THREAD_DELAY.toString -> 2) ~
75-
(AttributesEnum.PLAYGROUND_ENABLED.toString -> true) ~
76-
(AttributesEnum.CORS_HEADER.toString -> "") ~
77-
(AttributesEnum.MAX_ATTEMPTS_RATIO.toString -> 0.01f) ~
78-
("captchas" -> List(
79-
(
80-
(AttributesEnum.NAME.toString -> "FilterChallenge") ~
81-
(ParametersEnum.ALLOWEDLEVELS.toString -> List("medium", "hard")) ~
82-
(ParametersEnum.ALLOWEDMEDIA.toString -> List("image/png")) ~
83-
(ParametersEnum.ALLOWEDINPUTTYPE.toString -> List("text")) ~
84-
(ParametersEnum.ALLOWEDSIZES.toString -> List("350x100")) ~
85-
(AttributesEnum.CONFIG.toString -> JObject())
86-
),
87-
(
88-
(AttributesEnum.NAME.toString -> "PoppingCharactersCaptcha") ~
89-
(ParametersEnum.ALLOWEDLEVELS.toString -> List("hard")) ~
90-
(ParametersEnum.ALLOWEDMEDIA.toString -> List("image/gif")) ~
91-
(ParametersEnum.ALLOWEDINPUTTYPE.toString -> List("text")) ~
92-
(ParametersEnum.ALLOWEDSIZES.toString -> List("350x100")) ~
93-
(AttributesEnum.CONFIG.toString -> JObject())
94-
),
95-
(
96-
(AttributesEnum.NAME.toString -> "ShadowTextCaptcha") ~
97-
(ParametersEnum.ALLOWEDLEVELS.toString -> List("easy")) ~
98-
(ParametersEnum.ALLOWEDMEDIA.toString -> List("image/png")) ~
99-
(ParametersEnum.ALLOWEDINPUTTYPE.toString -> List("text")) ~
100-
(ParametersEnum.ALLOWEDSIZES.toString -> List("350x100")) ~
101-
(AttributesEnum.CONFIG.toString -> JObject())
102-
),
103-
(
104-
(AttributesEnum.NAME.toString -> "RainDropsCaptcha") ~
105-
(ParametersEnum.ALLOWEDLEVELS.toString -> List("easy", "medium")) ~
106-
(ParametersEnum.ALLOWEDMEDIA.toString -> List("image/gif")) ~
107-
(ParametersEnum.ALLOWEDINPUTTYPE.toString -> List("text")) ~
108-
(ParametersEnum.ALLOWEDSIZES.toString -> List("350x100")) ~
109-
(AttributesEnum.CONFIG.toString -> JObject())
110-
)
111-
))
57+
val defaultConfig = AppConfig(
58+
seed = Some(new ju.Random().nextInt()),
59+
port = Some(8888),
60+
address = Some("0.0.0.0"),
61+
captchaExpiryTimeLimit = Some(5),
62+
bufferCount = Some(1000),
63+
threadDelay = Some(2),
64+
playgroundEnabled = Some(true),
65+
corsHeader = Some(""),
66+
maxAttemptsRatio = Some(0.01f),
67+
captchas = List(
68+
CaptchaConfig(
69+
name = "FilterChallenge",
70+
allowedLevels = List("medium", "hard"),
71+
allowedMedia = List("image/png"),
72+
allowedInputType = List("text"),
73+
allowedSizes = List("350x100"),
74+
config = JSONString("{}")
75+
),
76+
CaptchaConfig(
77+
name = "PoppingCharactersCaptcha",
78+
allowedLevels = List("hard"),
79+
allowedMedia = List("image/gif"),
80+
allowedInputType = List("text"),
81+
allowedSizes = List("350x100"),
82+
config = JSONString("{}")
83+
),
84+
CaptchaConfig(
85+
name = "ShadowTextCaptcha",
86+
allowedLevels = List("easy"),
87+
allowedMedia = List("image/png"),
88+
allowedInputType = List("text"),
89+
allowedSizes = List("350x100"),
90+
config = JSONString("{}")
91+
),
92+
CaptchaConfig(
93+
name = "RainDropsCaptcha",
94+
allowedLevels = List("easy", "medium"),
95+
allowedMedia = List("image/gif"),
96+
allowedInputType = List("text"),
97+
allowedSizes = List("350x100"),
98+
config = JSONString("{}")
99+
)
100+
)
101+
)
112102

113-
pretty(render(defaultConfigMap))
103+
writeToString(defaultConfig, WriterConfig.withIndentionStep(2))
114104
}
115105

116106
}
117107

118108
object Config {
119-
implicit val formats: DefaultFormats.type = DefaultFormats
120109
}
Lines changed: 81 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -1,47 +1,100 @@
11
package lc.core
22

3-
import org.json4s.jackson.Serialization.write
4-
import lc.core.Config.formats
3+
import com.github.plokhotnyuk.jsoniter_scala.macros._
4+
import com.github.plokhotnyuk.jsoniter_scala.core._
55

66
trait ByteConvert { def toBytes(): Array[Byte] }
77
case class Size(height: Int, width: Int)
8+
89
case class Parameters(level: String, media: String, input_type: String, size: String)
9-
case class Id(id: String) extends ByteConvert { def toBytes(): Array[Byte] = { write(this).getBytes } }
10+
object Parameters {
11+
implicit val codec: JsonValueCodec[Parameters] = JsonCodecMaker.make
12+
}
13+
14+
case class Id(id: String) extends ByteConvert { def toBytes(): Array[Byte] = { writeToArray(this) } }
15+
object Id {
16+
implicit val codec: JsonValueCodec[Id] = JsonCodecMaker.make
17+
}
18+
1019
case class Image(image: Array[Byte]) extends ByteConvert { def toBytes(): Array[Byte] = { image } }
20+
1121
case class Answer(answer: String, id: String)
12-
case class Success(result: String) extends ByteConvert { def toBytes(): Array[Byte] = { write(this).getBytes } }
13-
case class Error(message: String) extends ByteConvert { def toBytes(): Array[Byte] = { write(this).getBytes } }
22+
object Answer {
23+
implicit val codec: JsonValueCodec[Answer] = JsonCodecMaker.make
24+
}
25+
26+
case class Success(result: String) extends ByteConvert { def toBytes(): Array[Byte] = { writeToArray(this) } }
27+
object Success {
28+
implicit val codec: JsonValueCodec[Success] = JsonCodecMaker.make
29+
}
30+
31+
case class Error(message: String) extends ByteConvert { def toBytes(): Array[Byte] = { writeToArray(this) } }
32+
object Error {
33+
implicit val codec: JsonValueCodec[Error] = JsonCodecMaker.make
34+
}
35+
36+
case class JSONString(string: String)
37+
38+
object JSONString {
39+
implicit val codec: JsonValueCodec[JSONString] = new JsonValueCodec[JSONString] {
40+
def decodeValue(in: JsonReader, default: JSONString): JSONString = {
41+
val raw = in.readRawValAsBytes()
42+
JSONString(new String(raw, "UTF-8"))
43+
}
44+
45+
def encodeValue(x: JSONString, out: JsonWriter): Unit = {
46+
out.writeRawVal(x.string.getBytes("UTF-8"))
47+
}
48+
49+
def nullValue: JSONString = null.asInstanceOf[JSONString]
50+
}
51+
}
52+
1453
case class CaptchaConfig(
1554
name: String,
1655
allowedLevels: List[String],
1756
allowedMedia: List[String],
1857
allowedInputType: List[String],
1958
allowedSizes: List[String],
20-
config: String
59+
config: JSONString
2160
)
61+
62+
case class AppConfig(
63+
port: Option[Int] = None,
64+
address: Option[String] = None,
65+
bufferCount: Option[Int] = None,
66+
seed: Option[Int] = None,
67+
captchaExpiryTimeLimit: Option[Int] = None,
68+
threadDelay: Option[Int] = None,
69+
playgroundEnabled: Option[Boolean] = None,
70+
corsHeader: Option[String] = None,
71+
maxAttemptsRatio: Option[Float] = None,
72+
captchas: List[CaptchaConfig] = List.empty
73+
) {
74+
def toConfigField: ConfigField = ConfigField(
75+
port, address, bufferCount, seed, captchaExpiryTimeLimit,
76+
threadDelay, playgroundEnabled, corsHeader, maxAttemptsRatio
77+
)
78+
}
79+
object AppConfig {
80+
implicit val codec: JsonValueCodec[AppConfig] = JsonCodecMaker.make
81+
}
2282
case class ConfigField(
23-
port: Option[Integer],
24-
address: Option[String],
25-
bufferCount: Option[Integer],
26-
seed: Option[Integer],
27-
captchaExpiryTimeLimit: Option[Integer],
28-
threadDelay: Option[Integer],
29-
playgroundEnabled: Option[java.lang.Boolean],
30-
corsHeader: Option[String],
31-
maxAttemptsRatio: Option[java.lang.Float]
83+
port: Option[Int] = None,
84+
address: Option[String] = None,
85+
bufferCount: Option[Int] = None,
86+
seed: Option[Int] = None,
87+
captchaExpiryTimeLimit: Option[Int] = None,
88+
threadDelay: Option[Int] = None,
89+
playgroundEnabled: Option[Boolean] = None,
90+
corsHeader: Option[String] = None,
91+
maxAttemptsRatio: Option[Float] = None
3292
) {
33-
lazy val portInt: Option[Int] = mapInt(port)
34-
lazy val bufferCountInt: Option[Int] = mapInt(bufferCount)
35-
lazy val seedInt: Option[Int] = mapInt(seed)
36-
lazy val captchaExpiryTimeLimitInt: Option[Int] = mapInt(captchaExpiryTimeLimit)
37-
lazy val threadDelayInt: Option[Int] = mapInt(threadDelay)
38-
lazy val maxAttemptsRatioFloat: Option[Float] = mapFloat(maxAttemptsRatio)
93+
lazy val portInt: Option[Int] = port
94+
lazy val bufferCountInt: Option[Int] = bufferCount
95+
lazy val seedInt: Option[Int] = seed
96+
lazy val captchaExpiryTimeLimitInt: Option[Int] = captchaExpiryTimeLimit
97+
lazy val threadDelayInt: Option[Int] = threadDelay
98+
lazy val maxAttemptsRatioFloat: Option[Float] = maxAttemptsRatio
3999
lazy val playgroundEnabledBool: Option[Boolean] = playgroundEnabled.map(_ || false)
40-
41-
private def mapInt(x: Option[Integer]): Option[Int] = {
42-
x.map(_ + 0)
43-
}
44-
private def mapFloat(x: Option[java.lang.Float]): Option[Float] = {
45-
x.map(_ + 0.0f)
46-
}
47100
}

src/main/scala/lc/server/Server.scala

Lines changed: 3 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,9 @@
11
package lc.server
22

3-
import org.json4s.jackson.JsonMethods.parse
4-
import org.json4s.jvalue2extractable
3+
import com.github.plokhotnyuk.jsoniter_scala.core._
54
import lc.core.CaptchaManager
65
import lc.core.ErrorMessageEnum
76
import lc.core.{Answer, ByteConvert, Error, Id, Parameters}
8-
import lc.core.Config.formats
97
import org.limium.picoserve
108
import org.limium.picoserve.Server.{ByteResponse, ServerBuilder, StringResponse}
119
import scala.io.Source
@@ -31,8 +29,7 @@ class Server(
3129
.POST(
3230
"/v2/captcha",
3331
(request) => {
34-
val json = parse(request.getBodyString())
35-
val param = json.extract[Parameters]
32+
val param = readFromString[Parameters](request.getBodyString())
3633
val id = captchaManager.getChallenge(param)
3734
getResponse(id, headerMap)
3835
}
@@ -54,8 +51,7 @@ class Server(
5451
.POST(
5552
"/v2/answer",
5653
(request) => {
57-
val json = parse(request.getBodyString())
58-
val answer = json.extract[Answer]
54+
val answer = readFromString[Answer](request.getBodyString())
5955
val result = captchaManager.checkAnswer(answer)
6056
getResponse(result, headerMap)
6157
}

0 commit comments

Comments
 (0)