Skip to content

Commit 0e83265

Browse files
committed
add state to json and jdbc processor streams (allowing persisting entity state to a plain file and/or to a database table)
1 parent 4377809 commit 0e83265

File tree

26 files changed

+922
-47
lines changed

26 files changed

+922
-47
lines changed

build.sbt

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@ ThisBuild / organization := "app.softnetwork"
88

99
name := "generic-persistence-api"
1010

11-
ThisBuild / version := "0.6.2.1"
11+
ThisBuild / version := "0.7-SNAPSHOT"
1212

1313
ThisBuild / scalaVersion := "2.12.18"
1414

common/src/main/scala/app/softnetwork/serialization/package.scala

Lines changed: 69 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,11 @@ import org.json4s.ext.{JavaTypesSerializers, JodaTimeSerializers}
44
import org.json4s.jackson.Serialization
55
import org.json4s._
66

7+
import java.text.SimpleDateFormat
8+
import java.time.{Instant, LocalDate, LocalDateTime}
9+
import java.util.Date
710
import scala.language.implicitConversions
11+
import scala.reflect.ClassTag
812

913
/** Created by smanciot on 14/05/2020.
1014
*/
@@ -21,7 +25,8 @@ package object serialization {
2125
implicit def map2String(data: Map[String, Any])(implicit formats: Formats): String =
2226
serialization.write(data)
2327

24-
val defaultExcludedFields = List("serialVersionUID", "__serializedSizeCachedValue", "bitmap$0")
28+
val defaultExcludedFields: List[String] =
29+
List("serialVersionUID", "__serializedSizeCachedValue", "bitmap$0")
2530

2631
def caseClass2Map(
2732
cc: AnyRef
@@ -64,4 +69,67 @@ package object serialization {
6469
*/
6570
implicit def option2String(opt: Option[String]): String = opt.getOrElse("")
6671

72+
import scala.reflect.runtime.universe._
73+
import scala.reflect.runtime.{currentMirror => cm}
74+
75+
def isCaseClass[T: TypeTag](paramType: Type): Boolean = {
76+
paramType.typeSymbol.isClass && paramType.typeSymbol.asClass.isCaseClass
77+
}
78+
79+
def updateCaseClass[T: TypeTag](obj: T, updates: Map[String, Any])(implicit
80+
tag: ClassTag[T]
81+
): T = {
82+
val classType = typeOf[T].typeSymbol.asClass
83+
val classMirror = cm.reflect(obj)
84+
85+
// Get the primary constructor of the case class
86+
val constructor = typeOf[T].decl(termNames.CONSTRUCTOR).asMethod
87+
val classInstanceMirror = cm.reflectClass(classType)
88+
89+
// Get the parameters of the constructor
90+
val params = constructor.paramLists.flatten
91+
92+
// Build new parameter values (updated if in the map, or original if not)
93+
val newArgs = params.map { param =>
94+
val paramName = param.name.toString
95+
val paramType = param.typeSignature
96+
updates.get(paramName) match {
97+
case Some(newValue) =>
98+
// Handle type conversion based on the expected parameter type
99+
(paramType, newValue) match {
100+
case (t, v: String) if t =:= typeOf[Boolean] =>
101+
v.toBoolean // Convert string to Boolean
102+
case (t, v: String) if t =:= typeOf[Date] =>
103+
new SimpleDateFormat().parse(v) // Convert string to Date
104+
case (t, v: String) if t =:= typeOf[Double] =>
105+
v.toDouble // Convert string to Double
106+
case (t, v: String) if t =:= typeOf[Float] =>
107+
v.toFloat // Convert string to Float
108+
case (t, v: String) if t =:= typeOf[Instant] =>
109+
Instant.parse(v) // Convert string to Instant
110+
case (t, v: String) if t =:= typeOf[Int] =>
111+
v.toInt // Convert string to Int
112+
case (t, v: String) if t =:= typeOf[LocalDate] =>
113+
LocalDate.parse(v) // Convert string to LocalDate
114+
case (t, v: String) if t =:= typeOf[LocalDateTime] =>
115+
LocalDateTime.parse(v) // Convert string to LocalDateTime
116+
case (t, v: String) if t =:= typeOf[Long] =>
117+
v.toLong // Convert string to Long
118+
case (t, v: String) if t =:= typeOf[Short] =>
119+
v.toShort // Convert string to Short
120+
case (t, v: Map[String, Any]) if isCaseClass(t) =>
121+
updateCaseClass(classMirror.reflectField(t.decl(TermName(paramName)).asTerm).get, v)
122+
case _ =>
123+
newValue // If types match or are already compatible, use the value directly
124+
}
125+
case None =>
126+
// Reflectively get the current field value from the case class instance
127+
val fieldTerm = typeOf[T].decl(TermName(paramName)).asTerm
128+
classMirror.reflectField(fieldTerm).get
129+
}
130+
}
131+
132+
// Create a new instance of the case class using the updated arguments
133+
classInstanceMirror.reflectConstructor(constructor)(newArgs: _*).asInstanceOf[T]
134+
}
67135
}

core/src/main/scala/app/softnetwork/persistence/model/package.scala

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,11 @@
11
package app.softnetwork.persistence
22

3+
import app.softnetwork.serialization._
4+
import org.json4s.Formats
5+
36
import java.time.Instant
7+
import scala.language.postfixOps
8+
import scala.reflect.ClassTag
49

510
/** Created by smanciot on 27/05/2020.
611
*/
@@ -30,4 +35,33 @@ package object model {
3035
trait CborDomainObject
3136

3237
trait ProtobufStateObject extends ProtobufDomainObject with State
38+
39+
implicit class CamelCaseString(s: String) {
40+
def toSnakeCase: String = s.foldLeft("") { (acc, char) =>
41+
if (char.isUpper) {
42+
if (acc.isEmpty) char.toLower.toString
43+
else acc + "_" + char.toLower
44+
} else {
45+
acc + char
46+
}
47+
}
48+
def $: String = toSnakeCase
49+
}
50+
51+
case class StateWrapper[T <: State](
52+
uuid: String,
53+
lastUpdated: Instant,
54+
deleted: Boolean,
55+
state: Option[T]
56+
) extends State {
57+
def asJson(implicit formats: Formats): String = {
58+
serialization.write[StateWrapper[T]](this.copy(deleted = deleted || state.isEmpty))
59+
}
60+
}
61+
62+
trait StateWrappertReader[T <: State] extends ManifestWrapper[StateWrapper[T]] {
63+
def read(json: String)(implicit formats: Formats): StateWrapper[T] = {
64+
serialization.read[StateWrapper[T]](json)(formats, manifestWrapper.wrapped)
65+
}
66+
}
3367
}

core/src/main/scala/app/softnetwork/persistence/query/ExternalPersistenceProvider.scala

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@ import scala.reflect.ClassTag
1010
*/
1111
trait ExternalPersistenceProvider[T <: Timestamped] { _: ManifestWrapper[T] =>
1212

13-
/** Creates the unerlying document to the external system
13+
/** Creates the underlying document to the external system
1414
*
1515
* @param document
1616
* - the document to create
@@ -22,7 +22,7 @@ trait ExternalPersistenceProvider[T <: Timestamped] { _: ManifestWrapper[T] =>
2222
def createDocument(document: T)(implicit t: ClassTag[T] = manifestWrapper.wrapped): Boolean =
2323
false
2424

25-
/** Updates the unerlying document to the external system
25+
/** Updates the underlying document to the external system
2626
*
2727
* @param document
2828
* - the document to update
@@ -38,7 +38,7 @@ trait ExternalPersistenceProvider[T <: Timestamped] { _: ManifestWrapper[T] =>
3838
t: ClassTag[T] = manifestWrapper.wrapped
3939
): Boolean = false
4040

41-
/** Upserts the unerlying document referenced by its uuid to the external system
41+
/** Upsert the underlying document referenced by its uuid to the external system
4242
*
4343
* @param uuid
4444
* - the uuid of the document to upsert
@@ -49,7 +49,7 @@ trait ExternalPersistenceProvider[T <: Timestamped] { _: ManifestWrapper[T] =>
4949
*/
5050
def upsertDocument(uuid: String, data: String): Boolean = false
5151

52-
/** Deletes the unerlying document referenced by its uuid to the external system
52+
/** Deletes the underlying document referenced by its uuid to the external system
5353
*
5454
* @param uuid
5555
* - the uuid of the document to delete
@@ -78,7 +78,7 @@ trait ExternalPersistenceProvider[T <: Timestamped] { _: ManifestWrapper[T] =>
7878
* @param m
7979
* - implicit Manifest for T
8080
* @return
81-
* the documents founds or an empty list otherwise
81+
* the documents found or an empty list otherwise
8282
*/
8383
def searchDocuments(
8484
query: String

0 commit comments

Comments
 (0)