I'm writing an app that works with Redis Stream using Spring Data Redis. I'm using spring-data-redis with Lettuce. I can successfully write to the stream as I can validate it directly in Redis via redis-cli, and I see the messages are in Redis. When it comes to reading from the stream using StreamReceiver, it kind of works but my tests fail for the coroutines version.
So, I've implemented two versions for reading with different return types:
Flux<TestData>. I test it using reactor-test classes, similar to what Spring Data Redis team does. It works fine: received items are printed out and test passes.
Flow<TestData>. I test it using FlowTurbine. This tests fails, even though the received items are printed out; FlowTurbine just times out. I tried using directly blocking Flow.toList() instead of Turbine's test, but in this case the call just blocks forever. I'm probably doing something wrong, when I deal with the Flow. What am I doing wrong? And how to fix it?
TestDataRedisRepository.kt content:
import kotlinx.coroutines.FlowPreview
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.flatMapConcat
import kotlinx.coroutines.flow.map
import kotlinx.coroutines.reactive.asFlow
import org.slf4j.LoggerFactory
import org.springframework.data.redis.connection.stream.MapRecord
import org.springframework.data.redis.connection.stream.RecordId
import org.springframework.data.redis.connection.stream.StreamOffset
import org.springframework.data.redis.connection.stream.StreamRecords
import org.springframework.data.redis.core.ReactiveRedisTemplate
import org.springframework.data.redis.core.addAndAwait
import org.springframework.data.redis.core.trimAndAwait
import org.springframework.data.redis.stream.StreamReceiver
import org.springframework.stereotype.Repository
import reactor.core.publisher.Flux
const val STREAM_KEY = "test-data"
#Repository
class TestDataRedisRepository(
val reactiveRedisTemplate: ReactiveRedisTemplate<String, TestData>,
val streamReceiver: StreamReceiver<String, MapRecord<String, String, String>>
) {
private val log = LoggerFactory.getLogger(this::class.java)
#FlowPreview
fun saveAll(entityStream: Flow<TestData>): Flow<RecordId> {
return entityStream
.map { toMapRecord(it) }
.flatMapConcat {
log.info("Saving record: $it")
reactiveRedisTemplate
.opsForStream<String, TestData>()
.add(it)
.asFlow()
}
}
suspend fun save(TestData: TestData): RecordId {
val record = toMapRecord(TestData)
log.info("Saving record: $record")
return reactiveRedisTemplate
.opsForStream<String, TestData>()
.addAndAwait(record)
}
private fun toMapRecord(TestData: TestData): MapRecord<String, String, String> =
StreamRecords.newRecord()
.`in`(STREAM_KEY)
.ofMap(TestData.toMap())
fun readAllAsFlux(): Flux<TestData> {
return streamReceiver
.receive(StreamOffset.fromStart(STREAM_KEY))
.doOnEach { log.info("Received stream record: $it") }
.map { it.value.fromMap() }
}
fun readAllAsFlow(): Flow<TestData> {
return streamReceiver
.receive(StreamOffset.fromStart(STREAM_KEY))
.doOnEach { log.info("Received stream record: $it") }
.map { it.value.fromMap() }
.asFlow()
}
suspend fun deleteAll() {
reactiveRedisTemplate
.opsForStream<String, TestData>()
.trimAndAwait(STREAM_KEY, 0)
}
}
Test class TestDataRedisRepositoryTest.kt content:
import app.cash.turbine.test
import kotlinx.coroutines.FlowPreview
import kotlinx.coroutines.flow.flowOf
import kotlinx.coroutines.flow.toList
import kotlinx.coroutines.runBlocking
import org.assertj.core.api.Assertions.assertThat
import org.junit.jupiter.api.AfterEach
import org.junit.jupiter.api.BeforeAll
import org.junit.jupiter.api.Test
import org.junit.jupiter.api.TestInstance
import org.springframework.beans.factory.annotation.Autowired
import org.springframework.boot.test.context.SpringBootTest
import reactor.test.StepVerifier
import java.time.Duration
import java.time.Instant
import kotlin.time.ExperimentalTime
#FlowPreview
#ExperimentalTime
#TestInstance(TestInstance.Lifecycle.PER_CLASS)
#SpringBootTest
internal class TestDataRedisRepositoryTest #Autowired constructor(
private val testDataRedisRepository: TestDataRedisRepository
) {
private val now: Instant = Instant.now()
#BeforeAll
fun setUp() {
runBlocking { testDataRedisRepository.deleteAll() }
}
#AfterEach
fun afterEach() {
runBlocking {
testDataRedisRepository.deleteAll()
}
}
#Test //passes
fun `test Flux`() {
runBlocking {
testDataRedisRepository.saveAll(
flowOf(
TestData(now.minusSeconds(1), "test2"),
TestData(now, "test3")
)
).toList()
}
testDataRedisRepository.readAllAsFlux().`as`(StepVerifier::create) //
.consumeNextWith {
assertThat(it).isEqualTo(TestData(now.minusSeconds(1), "test2"))
}
.consumeNextWith {
assertThat(it).isEqualTo(TestData(now, "test3"))
}
.thenCancel()
.verify(Duration.ofSeconds(1))
}
#Test //fails
fun `test Flow`() {
runBlocking {
testDataRedisRepository.saveAll(
flowOf(
TestData(now.minusSeconds(1), "test2"),
TestData(now, "test3")
)
).toList()
// val list = testDataRedisRepository.readAllAsFlow().toList() // this call blocks forever
// FlowTurbine just times out
testDataRedisRepository.readAllAsFlow()
.test {
assertThat(expectItem())
.isEqualTo(TestData(now.minusSeconds(1), "test2"))
assertThat(expectItem())
.isEqualTo(TestData(now, "test3"))
expectComplete()
}
}
}
}
My RedisConfig.kt content:
import org.springframework.context.annotation.Bean
import org.springframework.context.annotation.Configuration
import org.springframework.data.redis.connection.ReactiveRedisConnectionFactory
import org.springframework.data.redis.connection.stream.MapRecord
import org.springframework.data.redis.core.ReactiveRedisTemplate
import org.springframework.data.redis.serializer.RedisSerializationContext
import org.springframework.data.redis.serializer.RedisSerializer
import org.springframework.data.redis.stream.StreamReceiver
#Configuration
class RedisConfig {
/**
* For writing to Redis Stream
*/
#Bean
fun reactiveRedisTemplate(
factory: ReactiveRedisConnectionFactory,
serializationContext: RedisSerializationContext<String, TestData>
): ReactiveRedisTemplate<String, TestData> = ReactiveRedisTemplate(
factory,
serializationContext
)
/**
* For reading from Redis Stream
*/
#Bean
fun streamReceiver(
factory: ReactiveRedisConnectionFactory,
serializationContext: RedisSerializationContext<String, TestData>
): StreamReceiver<String, MapRecord<String, String, String>> {
return StreamReceiver.create(
factory,
StreamReceiver.StreamReceiverOptions.builder()
.serializer(serializationContext)
.build()
)
}
#Bean
fun serializationContext(): RedisSerializationContext<String, TestData> =
RedisSerializationContext.newSerializationContext<String, TestData>(
RedisSerializer.string()
).build()
}
TestData.kt
import java.time.Instant
data class TestData(
val instant: Instant,
val content: String
)
const val INSTANT = "instant"
const val CONTENT = "content"
fun TestData.toMap(): Map<String, String> {
return mapOf(
INSTANT to instant.toString(),
CONTENT to content
)
}
fun Map<String, String>.fromMap(): TestData {
return TestData(
Instant.parse(this[INSTANT]),
this[CONTENT]!!
)
}
Redis is running in a Docker container on the default port.
For completeness, here are aplication.yaml and build.gradle.kts:
spring:
redis:
host: localhost
port: 6379
import org.jetbrains.kotlin.gradle.tasks.KotlinCompile
plugins {
id("org.springframework.boot") version "2.5.2"
id("io.spring.dependency-management") version "1.0.11.RELEASE"
kotlin("jvm") version "1.5.20"
kotlin("plugin.spring") version "1.5.20"
}
group = "com.example"
version = "0.0.1-SNAPSHOT"
java.sourceCompatibility = JavaVersion.VERSION_11
repositories {
mavenCentral()
}
dependencies {
implementation("org.springframework.boot:spring-boot-starter-data-redis-reactive")
implementation("io.projectreactor.kotlin:reactor-kotlin-extensions")
implementation("org.jetbrains.kotlin:kotlin-reflect")
implementation("org.jetbrains.kotlin:kotlin-stdlib-jdk8")
implementation("org.jetbrains.kotlinx:kotlinx-coroutines-reactor")
testImplementation("org.springframework.boot:spring-boot-starter-test")
testImplementation("io.projectreactor:reactor-test")
testImplementation("app.cash.turbine:turbine:0.5.2")
}
tasks.withType<KotlinCompile> {
kotlinOptions {
freeCompilerArgs = listOf("-Xjsr305=strict")
jvmTarget = "11"
}
}
tasks.withType<Test> {
useJUnitPlatform()
}
If the test needs to check multiple values then calling toList() causes the flow to wait for the source to emit all its values and then returns those values as a list. Note that this works only for finite data streams.
So, in your case if its a stream that emits infinite values then it will wait forever to collect the values which is why your test is blocked.
A solution can be to take a finite number of items from flow and then do the assertion. For example, you can do something like the code below:
// Take the first item and cancel the flow
val firstItem = testDataRedisRepository.readAllAsFlow().first()
// Take the second item
val secondItem = testDataRedisRepository.readAllAsFlow().drop(1).first()
// Take the first five items
val firstFiveItems = testDataRedisRepository.readAllAsFlow().take(5).toList()
For more scenarios, you can refer to this Android Developers link for testing Kotlin flow.
Related
As part of one of my courses, I am using an API in Android Studio for the first time. For this I use OkHttp. However, I have the following error when I launch my application. I guess it's because the JSON is not parsed well but I can't find a solution.
If anyone could help me that would be great!
Thank you
The error screenshot
MyMarqueRecyclerViewAdapter.kt
import android.content.Intent
import android.os.Bundle
import androidx.fragment.app.Fragment
import androidx.recyclerview.widget.GridLayoutManager
import androidx.recyclerview.widget.LinearLayoutManager
import androidx.recyclerview.widget.RecyclerView
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import android.widget.Toast
import com.google.gson.GsonBuilder
import kotlinx.android.synthetic.main.fragment_marque_list.*
import okhttp3.*
import java.io.IOException
/**
* A fragment representing a list of Items.
*/
class MarqueFragment : Fragment(), OnMarqueClickListener {
private var columnCount = 1
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
arguments?.let {
columnCount = it.getInt(ARG_COLUMN_COUNT)
}
fetchJson()
}
fun fetchJson() {
val url = "https://tp3.infomobile.app/api/v1/brand"
val request = Request.Builder().url(url).build()
val lesmarques = OkHttpClient()
lesmarques.newCall(request).enqueue(object: Callback {
override fun onResponse(call: Call, response: Response) {
val body = response.body?.string()
println(body)
val gson = GsonBuilder().create()
val homeFeed = gson.fromJson(body, HomeFeed::class.java)
println(homeFeed)
activity?.runOnUiThread {
recyclerView_main.adapter = MyMarqueRecyclerViewAdapter(homeFeed)
}
}
override fun onFailure(call: Call, e: IOException) {
println("Failed to execute request")
}
})
}
override fun onCreateView(
inflater: LayoutInflater, container: ViewGroup?,
savedInstanceState: Bundle?
): View? {
val view = inflater.inflate(R.layout.fragment_marque_list, container, false)
// Set the adapter
if (view is RecyclerView) {
with(view) {
layoutManager = when {
columnCount <= 1 -> LinearLayoutManager(context)
else -> GridLayoutManager(context, columnCount)
}
//adapter = MyMarqueRecyclerViewAdapter(homeFeed)
}
}
return view
}
override fun onMarqueItemClicked(position: Int) {
Toast.makeText(this.context, "ça marche", Toast.LENGTH_LONG).show()
val intent = Intent(this#MarqueFragment.requireContext(),MainActivity2::class.java)
startActivity(intent)
}
companion object {
// TODO: Customize parameter argument names
const val ARG_COLUMN_COUNT = "column-count"
// TODO: Customize parameter initialization
#JvmStatic
fun newInstance(columnCount: Int) =
MarqueFragment().apply {
arguments = Bundle().apply {
putInt(ARG_COLUMN_COUNT, columnCount)
}
}
}
}
class HomeFeed(val marques: List<Marque>)
class Marque(val id: Int, val name: String)
MarqueFragment.kt
import android.content.Intent
import androidx.recyclerview.widget.RecyclerView
import android.view.LayoutInflater
import android.view.ViewGroup
import android.widget.TextView
import ca.ulaval.ima.tp3.placeholder.PlaceholderContent.PlaceholderItem
import ca.ulaval.ima.tp3.databinding.FragmentMarqueBinding
/**
* [RecyclerView.Adapter] that can display a [PlaceholderItem].
* TODO: Replace the implementation with code for your data type.
*/
class MyMarqueRecyclerViewAdapter(val homeFeed: HomeFeed?) : RecyclerView.Adapter<MyMarqueRecyclerViewAdapter.ViewHolder>() {
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder {
return ViewHolder(
FragmentMarqueBinding.inflate(
LayoutInflater.from(parent.context),
parent,
false
)
)
}
override fun onBindViewHolder(holder: ViewHolder, position: Int) {
val item = homeFeed?.marques?.get(position)
holder.contentView.text = item.toString()
holder.contentView.setOnClickListener{
val context=holder.contentView.context
val intent = Intent( context, MainActivity2::class.java)
intent.putExtra("marque", item.toString())
context.startActivity(intent)
}
}
override fun getItemCount(): Int {
return homeFeed?.marques!!?.count()
}
inner class ViewHolder(binding: FragmentMarqueBinding) : RecyclerView.ViewHolder(binding.root) {
val contentView: TextView = binding.content
override fun toString(): String {
return super.toString() + " '" + contentView.text + "'"
}
}
}
First of all, you can trace the crash using the log which clearly states that the crash happened at line 42 in MyMarqueRecyclerViewAdapter.kt, in the getItemCount function, which means that this functions is causing it:
override fun getItemCount(): Int {
return homeFeed?.marques!!?.count()
}
What's happening here is that you're forcing kotlin to treat a property that might be null as a non null property, by using the "!!" operator, this tells kotlin that this propery would never be null in any case, and that isn't true in your case since its throwing a NullPointerException, what you actually need to do is allow it to be null and provide an alternative value in case it was null, using the elvis operator, like this:
override fun getItemCount(): Int {
return homeFeed?.marques?.count() ?: 0
}
Another thing is that you're using the count() function, i think you meant to use the size property instead:
override fun getItemCount(): Int {
return homeFeed?.marques?.size ?: 0
}
Second, i noticed your class declaration and compared the content to the json's response content, it seems that the json response conatins a parameter called "content", while the propery in the HomeFeed class is called "marques", which results in GSON not knowing where to get "marques" from, it only knows that there is a parameter called "content" in the json and it doesn't know what to do with it, the best solution would be to annotate the HomeFeed.marques property with a #SerializedName annotation, and provide the corresponding json parameter that should be mapped to this property, like so:
class HomeFeed(
#SerializedName("content")
val marques: List<Marque>
)
class Marque(
#SerializedName("id")
val id: Int,
#SerializedName("name")
val name: String
)
Hope this helps!
Some resources:
Null Safety | Kotlin
Difference between list.count() and list.size
Parsing between Kotlin classes and Json objects using GSON
I am struggling to understand how to parse an empty object {} with the experimental kotlinx.serialization library. The complication arises when in fact an API response can be one of;
{
"id": "ABC1",
"status": "A_STATUS"
}
or
{}
The data structure I have used as my serializer is;
data class Thing(val id: String = "", val status: String = "")
This is annotated with #kotlinx.serialization.Serializable and used within an API client library to marshall between the raw API response and the data model. The default values tell the serialisation library that the field is optional and replaces the #Optional approach of pre-Kotlin 1.3.30.
Finally, the kotlinx.serialization.json.Json parser I am using has the configuration applied by using the nonstrict template.
How do I define a serializer that can parse both an empty object and the expected data type with kotlinx.serialization? Do I need to write my own KSerialiser or is there config I am missing. Ideally, the empty object should be ignored/parsed as a null?
The error I get when parsing an empty object with my Thing data class is;
Field 'id' is required, but it was missing
So this was down to the kotlinCompilerClasspath having a different version kotlin (1.3.21, not 1.3.31).
Interestingly this was owing to advice I followed when configuring my gradle plugin project to not specify a version for the kotlin-dsl plugin.
Explicitly relying on the version I needed fixed the kotlinx.serialisation behavior (no changes to the mainline code)
Yes, ideally null instead of {} is way more convenient to parse but sometimes you just need to consume what backend sends you
There are 2 solutions that come to my mind.
Simpler, specific to your case using map:
import kotlinx.serialization.decodeFromString
import kotlinx.serialization.json.Json
import org.junit.Assert.assertEquals
import org.junit.Assert.assertTrue
import org.junit.Test
class ThingMapSerializerTest {
#Test
fun `should deserialize to non empty map`() {
val thingMap: Map<String, String> =
Json.decodeFromString("""{"id":"ABC1","status":"A_STATUS"}""")
assertTrue(thingMap.isNotEmpty())
assertEquals("ABC1", thingMap["id"])
assertEquals("A_STATUS", thingMap["status"])
}
#Test
fun `should deserialize to empty map`() {
val thingMap: Map<String, String> = Json.decodeFromString("{}")
assertTrue(thingMap.isEmpty())
}
}
More complex but more general that works for any combinations of value types. I recommend sealed class with explicit empty value instead of data class with empty defaults:
import kotlinx.serialization.KSerializer
import kotlinx.serialization.SerializationException
import kotlinx.serialization.descriptors.SerialDescriptor
import kotlinx.serialization.descriptors.buildClassSerialDescriptor
import kotlinx.serialization.descriptors.serialDescriptor
import kotlinx.serialization.encoding.CompositeDecoder
import kotlinx.serialization.encoding.Decoder
import kotlinx.serialization.encoding.Encoder
import kotlinx.serialization.encoding.decodeStructure
import kotlinx.serialization.json.Json
import org.junit.Assert.assertEquals
import org.junit.Test
class ThingSerializerTest {
#Test
fun `should deserialize to thing`() {
val thing: OptionalThing =
Json.decodeFromString(
OptionalThing.ThingSerializer,
"""{"id":"ABC1","status":"A_STATUS"}"""
)
assertEquals(OptionalThing.Thing(id = "ABC1", status = "A_STATUS"), thing)
}
#Test
fun `should deserialize to empty`() {
val thing: OptionalThing =
Json.decodeFromString(OptionalThing.ThingSerializer, "{}")
assertEquals(OptionalThing.Empty, thing)
}
sealed class OptionalThing {
data class Thing(val id: String = "", val status: String = "") : OptionalThing()
object Empty : OptionalThing()
object ThingSerializer : KSerializer<OptionalThing> {
override val descriptor: SerialDescriptor =
buildClassSerialDescriptor("your.app.package.OptionalThing") {
element("id", serialDescriptor<String>(), isOptional = true)
element("status", serialDescriptor<String>(), isOptional = true)
}
override fun deserialize(decoder: Decoder): OptionalThing {
decoder.decodeStructure(descriptor) {
var id: String? = null
var status: String? = null
loop# while (true) {
when (val index = decodeElementIndex(descriptor)) {
CompositeDecoder.DECODE_DONE -> break#loop
0 -> id = decodeStringElement(descriptor, index = 0)
1 -> status = decodeStringElement(descriptor, index = 1)
else -> throw SerializationException("Unexpected index $index")
}
}
return if (id != null && status != null) Thing(id, status)
else Empty
}
}
override fun serialize(encoder: Encoder, value: OptionalThing) {
TODO("Not implemented, not needed")
}
}
}
}
When 'Thing' is a field within json object:
"thing":{"id":"ABC1","status":"A_STATUS"} // could be {}
you can annotate property like that:
#Serializable(with = OptionalThing.ThingSerializer::class)
val thing: OptionalThing
Tested for:
classpath "org.jetbrains.kotlin:kotlin-serialization:1.4.10"
implementation "org.jetbrains.kotlinx:kotlinx-serialization-json:1.0.1"
For the following Kotlin class:
class ProductLogic(
private val product: Product?
) {
fun shouldShow(): Boolean {
if (product == null) {
return false
}
val version = product.version!!
if (!Utils.isAtLeastVersionX(version.major, version.minor)) {
return false
}
return true
}
}
I am trying to write a parameterized test in Kotlin:
#RunWith(ParameterizedRobolectricTestRunner::class)
#Config(constants = BuildConfig::class, sdk = [19], packageName = "com.example")
class ProductLogicTest(
private val product: Product?,
private val shouldShow: Boolean
) {
#Before
fun setUp() {
// doReturn(VERSION).`when`(product).version // (2) Raises a NotAMockException
}
#Test
fun shouldShow() {
assertThat(ProductLogic(product).shouldShow(), `is`(shouldShow))
}
companion object {
#JvmStatic
#Parameters(name = "{index}: {0} => {1}")
fun data(): Collection<Array<Any?>> {
val productMock = mock<Product>(Product::class.java)
doReturn(VERSION).`when`(productMock).version // (1) Is not applied
return asList(
arrayOf(productMock, false),
// ...
)
}
}
I want to parameterize the value of the Product#version property. When I (1) modify its value in the data() function it is not applied when running test. When I (2) try to modify its value in #Before a NotAMockException is raised:
org.mockito.exceptions.misusing.NotAMockException:
Argument passed to when() is not a mock!
Example of correct stubbing:
doThrow(new RuntimeException()).when(mock).someMethod();
Please note that the example is simplified - the real ProductLogic class consists of more parameters which rectify to using a parameterized test.
Robolectric and Mockito versions:
testImplementation 'org.robolectric:robolectric:4.1'
testImplementation 'org.mockito:mockito-core:2.23.4'
Also, to mock final classes, I created file src/test/resources/mockito-extensions/org.mockito.plugins.MockMaker with content:
mock-maker-inline
Classes to test:
class ProductLogic(private val product: Product?) {
fun shouldShow(): Boolean {
if (product == null) {
return false
}
val version = product.version
return !isAtLeastVersionX(version.minor, version.major)
}
private fun isAtLeastVersionX(minor: Int, major: Int): Boolean {
val v = 5
return v in minor..major
}
}
class Product(val version: Version)
class Version(val minor: Int, val major: Int)
Next test code works for me and test is passed:
import org.hamcrest.CoreMatchers.`is`
import org.junit.Assert.assertThat
import org.junit.Before
import org.junit.Test
import org.junit.runner.RunWith
import org.mockito.Mockito.doReturn
import org.mockito.Mockito.mock
import org.robolectric.ParameterizedRobolectricTestRunner
import org.robolectric.annotation.Config
import java.util.Arrays.asList
#RunWith(ParameterizedRobolectricTestRunner::class)
#Config(sdk = [19], packageName = "com.example")
class ProductLogicTest(private val product: Product,
private val shouldShow: Boolean) {
#Before
fun setUp() {
//doReturn(VERSION).`when`(product).version // if uncomment works fine
}
#Test
fun shouldShow() {
assertThat(ProductLogic(product).shouldShow(), `is`(shouldShow))
}
companion object {
private val VERSION = Version(1, 5)
#JvmStatic
#ParameterizedRobolectricTestRunner.Parameters(name = "{index}: {0} => {1}")
fun data(): Collection<Array<Any?>> {
val productMock = mock(Product::class.java)
doReturn(VERSION).`when`(productMock).version // Works fine
return asList(
arrayOf(productMock, false)
)
}
}
}
[enter image description here][1]I am facing some issue in Drools I want to pass date as a date type but currently we don't have any method in JSONObject to handle dates .My JSONObject looks like this.
{"id":600,"city":"Gotham","age":25,"startDate":"29-DEC-2017","endDate":"2014-08-31"}
My Drool condition looks like this.
package com.rules
import org.drools.core.spi.KnowledgeHelper;
import org.json.JSONObject;
rule "ComplexRule1"
salience 100
dialect "mvel"
date-effective "16-Jan-2018 00:00"
no-loop
when
$cdr : JSONObject( $cdr.optString("startDate") >= '28-Dec-2017')
then
$cdr.put("Action_1" , new JSONObject().put("actionName","Complex_Rule1_Action1").put("actionTypeName","SEND OFFER").put("channelName","SMS").put("messageTemplateName","SMSTemplate").put("#timestamp",(new java.text.SimpleDateFormat("yyyy/MM/dd HH:mm:ss")).format(new java.util.Date())).put("ruleFileName","ComplexRule1.drl").put("ruleName","ComplexRule1"));
end
I am currently using .optString Because we dont have any methods like optString/optInt/optBoolean for date. So how can I handle date in Drools?
Any help will be appreciated.
Regards Puneet
My new DRL looks like this :
package com.rules
import com.aravind.drools.SuperJSONObject;
import org.drools.core.spi.KnowledgeHelper;
import org.json.JSONObject;
rule "Convert to SuperJSONObject"
when
$cdr: JSONObject()
then
insert(new SuperJSONObject($cdr));
end
rule "ComplexRule1"
salience 100
dialect "mvel"
date-effective "16-Jan-2018 00:00"
no-loop
when
$cdr : SuperJSONObject( $cdr.getAsDate("startDate") == '28-Dec-2017')
then
$cdr.getObject().put("Action_1" , new JSONObject().put("actionName","Complex_Rule1_Action1").put("actionTypeName","SEND OFFER").put("channelName","SMS").put("messageTemplateName","SMSTemplate").put("#timestamp",(new java.text.SimpleDateFormat("yyyy/MM/dd HH:mm:ss")).format(new java.util.Date())).put("ruleFileName","ComplexRule1.drl").put("ruleName","ComplexRule1"));
end
Class look like this :
import java.text.ParseException;
import java.text.SimpleDateFormat;
import java.util.Date;
import org.json.*;
public class SuperJSONObject {
public final JSONObject obj;
SimpleDateFormat sdfmt2= new SimpleDateFormat("yyyy/MM/dd");
public SuperJSONObject(JSONObject obj){
this.obj = obj;
}
public Date getAsDate(String field) throws ParseException{
return sdfmt2.parse(this.obj.optString(field));
}
public JSONObject getObject(){
return this.obj;
}
}
Another Class is like this
import java.io.File
import java.io.FileReader
import org.drools.KnowledgeBase
import org.drools.KnowledgeBaseFactory
import org.drools.builder.KnowledgeBuilder
import org.drools.builder.KnowledgeBuilderFactory
import org.drools.builder.ResourceType
import org.drools.io.ResourceFactory
import org.drools.runtime.StatefulKnowledgeSession
import org.json.JSONObject
object RunStandAloneDrools {
def main(args: Array[String]): Unit = {
var jsonObjectArray: Array[JSONObject] = new Array(1)
jsonObjectArray(0) = new JSONObject("{\"id\":600,\"city\":\"Gotham\",\"age\":25,\"startDate\":\"28-Dec-2017\",\"endDate\":\"2014-08-01\"}")
var file: String = "/home/puneet/Downloads/ComplexRule1.drl"
var kbuilder: KnowledgeBuilder = KnowledgeBuilderFactory.newKnowledgeBuilder()
kbuilder.add(ResourceFactory.newReaderResource(new FileReader(new File(file))), ResourceType.DRL)
println("Errors? " + kbuilder.getErrors.size())
var iter = kbuilder.getErrors.iterator()
while(iter.hasNext()){
println(iter.next().getMessage)
}
var kbase: KnowledgeBase = KnowledgeBaseFactory.newKnowledgeBase()
kbase.addKnowledgePackages(kbuilder.getKnowledgePackages)
var session: StatefulKnowledgeSession = kbase.newStatefulKnowledgeSession()
callRulesEngine(jsonObjectArray,session)
println("Done")
}
def callRulesEngine(data: Array[JSONObject], knowledgeSession: StatefulKnowledgeSession): Unit = {
data.map ( x => callRulesEngine(x,knowledgeSession) )
}
def callRulesEngine(data: JSONObject, knowledgeSession: StatefulKnowledgeSession): Unit = {
try {
println("Input data " + data.toString())
knowledgeSession.insert(data)
knowledgeSession.fireAllRules()
println("Facts details " + knowledgeSession.getFactCount)
println("Enriched data " + data.toString())
} catch {
case (e: Exception) => println("Exception", e);
}
}
`
Output is not coming as per expectations
There are multiple ways to deal with this, but the fundamental thing is for you to understand that this is NOT a Drools issue at all. Your question is more on how do get a Date from a JSONObject.
One way this could be achieved is by using a function in Drools to make the conversion.
But I don't like functions, so I'll give you another, more elaborated, way to deal with this situation (and many others where a type conversion is required).
The idea is to create a wrapper class for your JSONObject- a SuperJSONObject- that will expose all the functionality you need. For the implementation of this class I will be using composition, but you can use inheritance (or a proxy) if you want.
public class SuperJSONObject {
public final JSONObject obj;
public SuperJSONObject(JSONObject obj){
this.obj = obj;
}
//expose all the methods from JSONObject you want/need
public Date getAsDate(String field){
return someDateParser.parse(this.obj.optString(field));
}
public JSONObject getObject(){
return this.obj;
}
}
So now we have a getAsDate() method that we can use in our rules. But we first need to convert a JSONObject into a SuperJSONObject before we can even use that method. You can do this in multiple ways and places. I'll be showing how to do it in DRL.
rule "Convert to SuperJSONObject"
when
$jo: JSONObject() //you may want to filter which objects are converted by adding constraints to this pattern
then
insert(new SuperJSONObject($jo));
end
And now we are good to go. We can now write a rule using this new class as follows:
rule "ComplexRule1"
salience 100
dialect "mvel"
date-effective "16-Jan-2018 00:00"
no-loop
when
$cdr : SuperJSONObject( getAsDate("startDate") >= '28-Dec-2017')
then
$cdr.getObject().put("Action_1" , ...);
end
After I have written all this code, I might reconsider the option of a simple function in DRL... :P
Hope it helps,
Is there an easy way to serialize to json without "tpe" field inside an object?
I need to serialize case classes to json structures and then, send them over the wire (They won't been deserialized in future). I have a specific api, so.. I don't need additional fields.
For class Person illustrated below:
case class Person(Name: String, Age: int, CandyLover: Boolean)
I'd like to see following:
{
"Name": "Paul",
"Age": 23,
"CandyLover": true
}
I would suggest you to take a look at spray-json or argonaut. Or play-json if you are already using Play.
Scala pickling is not really a json library, it was not created to generate JSON for public API, it's not flexible, you can't define JSON protocol first and then provide pickling serialization to match protocol. Pickling is for computer-to-computer serialization where no one is really care about bytes going between apps.
Probably the simplest way to do it is to pass somehow the Hint into JSONPickleBuilder. Quick way to do it is to override builders in JSONPickleFormat by calling hintStaticallyElidedType() method from PickleTools.
Here is kind of code snippet to demonstrate it:
import org.specs2.matcher.JsonMatchers
import org.specs2.mutable.Specification
import org.specs2.specification.Scope
import scala.pickling.Defaults._
import scala.pickling.json.{JSONPickleBuilder, JSONPickleFormat, JsonFormats}
import scala.pickling.{Output, PBuilder, PickleTools, StringOutput}
class PersonJsonFormatsTest extends Specification with JsonMatchers {
trait Context extends Scope with PersonJsonFormats
trait PersonJsonFormats extends JsonFormats {
override implicit val pickleFormat: JSONPickleFormat = new PersonJSONPickleFormat
}
class PersonJSONPickleFormat extends JSONPickleFormat {
override def createBuilder() = new JSONPickleBuilder(this, new StringOutput) with PickleTools {
hintStaticallyElidedType()
}
override def createBuilder(out: Output[String]): PBuilder = new JSONPickleBuilder(this, out) with PickleTools {
hintStaticallyElidedType()
}
}
"Pickle" should {
"Serialize Person without $type field in resulting JSON" in new Context {
case class Person(Name: String, Age: Int, CandyLover: Boolean)
val pickledPersonObject = Person("Paul", 23, CandyLover = true).pickle
println(pickledPersonObject.value)
pickledPersonObject.value must not */("$type" → ".*".r)
}
}
}
The output of println(pickledPersonObject.value) will be as you need:
{
"Name": "Paul",
"Age": 23,
"CandyLover": true
}
This way you will stay aligned with further pickle updates.
P.S. If someone knows more elegant and convenient way to reach the same behaviour - please let us know :-)
For scala-pickling 0.10.x:
import scala.pickling._
import scala.pickling.json.{JSONPickle, JSONPickleBuilder, JSONPickleFormat, JsonFormats}
import scala.pickling.pickler.AllPicklers
object serialization extends JsonFormats with Ops with AllPicklers {
override implicit val pickleFormat: JSONPickleFormat = new JSONPickleFormat {
private def setHints(h: Hintable): Unit = {
h.hintStaticallyElidedType()
h.hintDynamicallyElidedType()
}
override def createBuilder(): JSONPickleBuilder = {
val builder = super.createBuilder()
setHints(builder)
builder
}
override def createBuilder(out: Output[String]): PBuilder = {
val builder = super.createBuilder(out)
setHints(builder)
builder
}
override def createReader(pickle: JSONPickle): PReader = {
val reader = super.createReader(pickle)
setHints(reader)
reader
}
}
}
object SerializationTest extends App {
import serialization._
case class Person(firstName: String, lastName: String)
val pickle: JSONPickle = Person("Evelyn", "Patterson").pickle
val jsonString: String = pickle.value // {"firstName": "Evelyn","lastName": "Patterson"}
val person: Person = jsonString.unpickle[Person]
}
For scala-pickling 0.11.x:
import scala.pickling._
import scala.pickling.json.{JSONPickle, JSONPickleBuilder, JSONPickleFormat}
import scala.pickling.pickler.AllPicklers
object serialization extends AllPicklers {
private final class CustomJSONPickleFormat(tag: FastTypeTag[_]) extends JSONPickleFormat {
private def setHints(h: Hintable) {
h.hintElidedType(tag)
}
override def createBuilder(): JSONPickleBuilder = {
val b = super.createBuilder()
setHints(b)
b
}
override def createBuilder(out: Output[String]): PBuilder = {
val b = super.createBuilder(out)
setHints(b)
b
}
override def createReader(pickle: JSONPickle): PReader = {
val b = super.createReader(pickle)
setHints(b)
b
}
}
implicit val staticOnly = static.StaticOnly // for compile time serialization methods generation
implicit final class EncodeDecodeOps[T](picklee: T) {
def encode(implicit pickler: Pickler[T]): String = {
val pickleFormat = new CustomJSONPickleFormat(pickler.tag)
functions.pickle(picklee)(pickleFormat, pickler).value
}
def decode[A](implicit c: T => String, unpickler: Unpickler[A]): A = {
val pickleFormat = new CustomJSONPickleFormat(unpickler.tag)
functions.unpickle[A](json.JSONPickle(picklee))(unpickler, pickleFormat)
}
}
}
case class Person(firstName: String, lastName: String) {
#transient var x = "test"
}
object SerializationTest extends App {
import serialization._
val jsonString = Person("Lisa", "Daniels").encode
println(jsonString)
val person = jsonString.decode[Person]
println(person)
}