embulk-parser-firebase_avro 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (46) hide show
  1. checksums.yaml +7 -0
  2. data/.circleci/config.yml +27 -0
  3. data/.gitignore +80 -0
  4. data/.scalafmt.conf +2 -0
  5. data/LICENSE.txt +21 -0
  6. data/README.md +52 -0
  7. data/build.gradle +81 -0
  8. data/build.sbt +29 -0
  9. data/gradle/wrapper/gradle-wrapper.jar +0 -0
  10. data/gradle/wrapper/gradle-wrapper.properties +6 -0
  11. data/gradlew +169 -0
  12. data/gradlew.bat +84 -0
  13. data/lib/embulk/parser/firebase_avro.rb +3 -0
  14. data/project/build.properties +1 -0
  15. data/project/plugins.sbt +3 -0
  16. data/src/main/scala/org/embulk/parser/firebase_avro/FirebaseAvroParserPlugin.scala +66 -0
  17. data/src/main/scala/org/embulk/parser/firebase_avro/LoanPattern.scala +19 -0
  18. data/src/main/scala/org/embulk/parser/firebase_avro/Parser.scala +111 -0
  19. data/src/main/scala/org/embulk/parser/firebase_avro/PluginTask.scala +5 -0
  20. data/src/main/scala/org/embulk/parser/firebase_avro/ValueHolder.scala +5 -0
  21. data/src/main/scala/org/embulk/parser/firebase_avro/column/Column.scala +61 -0
  22. data/src/main/scala/org/embulk/parser/firebase_avro/column/Columns.scala +47 -0
  23. data/src/main/scala/org/embulk/parser/firebase_avro/column/EventDimension.scala +19 -0
  24. data/src/main/scala/org/embulk/parser/firebase_avro/column/UserDimension.scala +28 -0
  25. data/src/main/scala/org/embulk/parser/firebase_avro/define/Root.scala +5 -0
  26. data/src/main/scala/org/embulk/parser/firebase_avro/define/root/Event_Dim.scala +8 -0
  27. data/src/main/scala/org/embulk/parser/firebase_avro/define/root/User_Dim.scala +11 -0
  28. data/src/main/scala/org/embulk/parser/firebase_avro/define/root/event_dim/Params.scala +3 -0
  29. data/src/main/scala/org/embulk/parser/firebase_avro/define/root/event_dim/params/Value.scala +6 -0
  30. data/src/main/scala/org/embulk/parser/firebase_avro/define/root/user_dim/App_Info.scala +7 -0
  31. data/src/main/scala/org/embulk/parser/firebase_avro/define/root/user_dim/Bundle_Info.scala +3 -0
  32. data/src/main/scala/org/embulk/parser/firebase_avro/define/root/user_dim/Device_Info.scala +13 -0
  33. data/src/main/scala/org/embulk/parser/firebase_avro/define/root/user_dim/Geo_Info.scala +3 -0
  34. data/src/main/scala/org/embulk/parser/firebase_avro/define/root/user_dim/Ltv_Info.scala +3 -0
  35. data/src/main/scala/org/embulk/parser/firebase_avro/define/root/user_dim/Traffic_Source.scala +5 -0
  36. data/src/main/scala/org/embulk/parser/firebase_avro/define/root/user_dim/User_Properties.scala +3 -0
  37. data/src/main/scala/org/embulk/parser/firebase_avro/define/root/user_dim/user_properties/Value.scala +5 -0
  38. data/src/main/scala/org/embulk/parser/firebase_avro/define/root/user_dim/user_properties/value/Value.scala +6 -0
  39. data/src/main/scala/org/embulk/parser/firebase_avro/json/CustomEncoder.scala +21 -0
  40. data/src/main/scala/org/embulk/parser/firebase_avro/json/event_dim/EventParmsJsonSerializer.scala +29 -0
  41. data/src/main/scala/org/embulk/parser/firebase_avro/json/user_dim/UserPropertiesJsonSerializer.scala +34 -0
  42. data/src/test/scala/org/embulk/parser/firebase_avro/Implicitly.scala +9 -0
  43. data/src/test/scala/org/embulk/parser/firebase_avro/ParserTest.scala +22 -0
  44. data/src/test/scala/org/embulk/parser/firebase_avro/column/ColumnsTest.scala +18 -0
  45. data/src/test/scala/org/embulk/parser/firebase_avro/json/event_dim/EventParmsJsonSerializerTest.scala +19 -0
  46. metadata +138 -0
data/gradlew.bat ADDED
@@ -0,0 +1,84 @@
1
+ @if "%DEBUG%" == "" @echo off
2
+ @rem ##########################################################################
3
+ @rem
4
+ @rem Gradle startup script for Windows
5
+ @rem
6
+ @rem ##########################################################################
7
+
8
+ @rem Set local scope for the variables with windows NT shell
9
+ if "%OS%"=="Windows_NT" setlocal
10
+
11
+ set DIRNAME=%~dp0
12
+ if "%DIRNAME%" == "" set DIRNAME=.
13
+ set APP_BASE_NAME=%~n0
14
+ set APP_HOME=%DIRNAME%
15
+
16
+ @rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
17
+ set DEFAULT_JVM_OPTS=
18
+
19
+ @rem Find java.exe
20
+ if defined JAVA_HOME goto findJavaFromJavaHome
21
+
22
+ set JAVA_EXE=java.exe
23
+ %JAVA_EXE% -version >NUL 2>&1
24
+ if "%ERRORLEVEL%" == "0" goto init
25
+
26
+ echo.
27
+ echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
28
+ echo.
29
+ echo Please set the JAVA_HOME variable in your environment to match the
30
+ echo location of your Java installation.
31
+
32
+ goto fail
33
+
34
+ :findJavaFromJavaHome
35
+ set JAVA_HOME=%JAVA_HOME:"=%
36
+ set JAVA_EXE=%JAVA_HOME%/bin/java.exe
37
+
38
+ if exist "%JAVA_EXE%" goto init
39
+
40
+ echo.
41
+ echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME%
42
+ echo.
43
+ echo Please set the JAVA_HOME variable in your environment to match the
44
+ echo location of your Java installation.
45
+
46
+ goto fail
47
+
48
+ :init
49
+ @rem Get command-line arguments, handling Windows variants
50
+
51
+ if not "%OS%" == "Windows_NT" goto win9xME_args
52
+
53
+ :win9xME_args
54
+ @rem Slurp the command line arguments.
55
+ set CMD_LINE_ARGS=
56
+ set _SKIP=2
57
+
58
+ :win9xME_args_slurp
59
+ if "x%~1" == "x" goto execute
60
+
61
+ set CMD_LINE_ARGS=%*
62
+
63
+ :execute
64
+ @rem Setup the command line
65
+
66
+ set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar
67
+
68
+ @rem Execute Gradle
69
+ "%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %CMD_LINE_ARGS%
70
+
71
+ :end
72
+ @rem End local scope for the variables with windows NT shell
73
+ if "%ERRORLEVEL%"=="0" goto mainEnd
74
+
75
+ :fail
76
+ rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of
77
+ rem the _cmd.exe /c_ return code!
78
+ if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1
79
+ exit /b 1
80
+
81
+ :mainEnd
82
+ if "%OS%"=="Windows_NT" endlocal
83
+
84
+ :omega
@@ -0,0 +1,3 @@
1
+ Embulk::JavaPlugin.register_parser(
2
+ "firebase_avro", "org.embulk.parser.firebase_avro.FirebaseAvroParserPlugin",
3
+ File.expand_path('../../../../classpath', __FILE__))
@@ -0,0 +1 @@
1
+ sbt.version=0.13.15
@@ -0,0 +1,3 @@
1
+ addSbtPlugin("com.julianpeeters" % "sbt-avrohugger" % "0.16.0")
2
+ addSbtPlugin("com.lucidchart" % "sbt-scalafmt" % "0.4")
3
+ addSbtPlugin("io.get-coursier" % "sbt-coursier" % "1.0.0-RC6")
@@ -0,0 +1,66 @@
1
+ package org.embulk.parser.firebase_avro
2
+
3
+ import java.io.InputStream
4
+
5
+ import scala.collection.JavaConverters._
6
+ import com.sksamuel.avro4s.AvroInputStream
7
+ import io.circe.Json
8
+ import org.apache.commons.compress.utils.IOUtils
9
+ import org.embulk.config.ConfigSource
10
+ import org.embulk.config.TaskSource
11
+ import org.embulk.parser.firebase_avro.column.Columns
12
+ import org.embulk.parser.firebase_avro.define.Root
13
+ import org.embulk.spi._
14
+ import org.embulk.spi.json.JsonParser
15
+ import org.embulk.spi.time.Timestamp
16
+ import org.embulk.spi.util.FileInputInputStream
17
+
18
+ object FirebaseAvroParserPlugin {
19
+ def buildColumn(): Schema = {
20
+ new Schema(Columns.instance.map(_.embulkColumn).asJava)
21
+ }
22
+ }
23
+
24
+ class FirebaseAvroParserPlugin extends ParserPlugin {
25
+ override def transaction(config: ConfigSource, control: ParserPlugin.Control): Unit = {
26
+ val task = config.loadConfig(classOf[PluginTask])
27
+ control.run(task.dump, FirebaseAvroParserPlugin.buildColumn())
28
+ }
29
+
30
+ override def run(taskSource: TaskSource, schema: Schema, input: FileInput, output: PageOutput): Unit =
31
+ LoanPattern(new FileInputInputStream(input)) { efis =>
32
+ LoanPattern(new PageBuilder(Exec.getBufferAllocator, schema, output)) { pb =>
33
+ while (efis.nextFile()) {
34
+ addRecords(efis, pb)
35
+ }
36
+ pb.finish()
37
+ }
38
+ }
39
+
40
+ def addRecords(is: InputStream, pb: PageBuilder): Unit =
41
+ AvroInputStream.data[Root](IOUtils.toByteArray(is)).iterator().foreach { record =>
42
+ Parser(record).foreach { rows =>
43
+ rows.foreach {
44
+ case ValueHolder(c, Some(x: Int)) =>
45
+ pb.setLong(c, x)
46
+ case ValueHolder(c, Some(x: Long)) =>
47
+ pb.setLong(c, x)
48
+ case ValueHolder(c, Some(x: Double)) =>
49
+ pb.setDouble(c, x)
50
+ case ValueHolder(c, Some(x: Float)) =>
51
+ pb.setDouble(c, x)
52
+ case ValueHolder(c, Some(x: Boolean)) =>
53
+ pb.setBoolean(c, x)
54
+ case ValueHolder(c, Some(x: String)) =>
55
+ pb.setString(c, x)
56
+ case ValueHolder(c, Some(x: Json)) =>
57
+ pb.setJson(c, new JsonParser().parse(x.noSpaces))
58
+ case ValueHolder(c, Some(x: Timestamp)) =>
59
+ pb.setTimestamp(c, x)
60
+ case ValueHolder(c, None) =>
61
+ pb.setNull(c)
62
+ }
63
+ pb.addRecord()
64
+ }
65
+ }
66
+ }
@@ -0,0 +1,19 @@
1
+ package org.embulk.parser.firebase_avro
2
+
3
+ import scala.util.control.Exception.ignoring
4
+
5
+ object LoanPattern {
6
+
7
+ type Closable = { def close(): Unit }
8
+
9
+ def apply[R <: Closable, A](resource: R)(f: R => A): A = {
10
+ try {
11
+ f(resource)
12
+ } finally {
13
+ ignoring(classOf[Throwable]) apply {
14
+ resource.close()
15
+ }
16
+ }
17
+ }
18
+
19
+ }
@@ -0,0 +1,111 @@
1
+ package org.embulk.parser.firebase_avro
2
+
3
+ import org.embulk.parser.firebase_avro.column.Columns
4
+ import org.embulk.parser.firebase_avro.define.Root
5
+ import org.embulk.parser.firebase_avro.define.root.{Event_Dim, User_Dim}
6
+ import org.embulk.parser.firebase_avro.json.event_dim.EventParmsJsonSerializer
7
+ import org.embulk.parser.firebase_avro.json.user_dim.UserPropertiesJsonSerializer
8
+
9
+ import scala.language.reflectiveCalls
10
+
11
+ object Parser {
12
+
13
+ def apply(record: Root): Seq[Seq[ValueHolder[_]]] = {
14
+ val userFields = userDims(record.user_dim.getOrElse(sys.error("could not get user")))
15
+ if (record.event_dim.isEmpty) sys.error("empty event")
16
+ record.event_dim.map {
17
+ userFields ++ eventDims(_)
18
+ }
19
+ }
20
+
21
+ private def eventDims(eventDim: Event_Dim) = {
22
+ val c = Columns.find("event_dim", _: String)
23
+ Seq(
24
+ ValueHolder(c("name"), eventDim.name),
25
+ ValueHolder(c("date"), eventDim.date),
26
+ ValueHolder(c("timestamp_micros"), eventDim.timestamp_micros),
27
+ ValueHolder(c("previous_timestamp_micros"), eventDim.previous_timestamp_micros),
28
+ ValueHolder(c("value_in_usd"), eventDim.value_in_usd),
29
+ ValueHolder(c("params"), EventParmsJsonSerializer(eventDim.params))
30
+ )
31
+ }
32
+
33
+ private def userDims(userDim: User_Dim) = {
34
+ val userFields = {
35
+ val c = Columns.find("user_dim", _: String)
36
+ Seq(
37
+ ValueHolder(c("user_id"), userDim.user_id),
38
+ ValueHolder(c("first_open_timestamp_micros"), userDim.first_open_timestamp_micros),
39
+ ValueHolder(c("user_properties"), UserPropertiesJsonSerializer(userDim.user_properties))
40
+ )
41
+ }
42
+
43
+ val appInfo = {
44
+ val c = Columns.find("app_info", _: String)
45
+ val that = userDim.app_info
46
+ Seq(
47
+ ValueHolder(c("app_id"), that.flatMap(_.app_id)),
48
+ ValueHolder(c("app_instance_id"), that.flatMap(_.app_instance_id)),
49
+ ValueHolder(c("app_platform"), that.flatMap(_.app_platform)),
50
+ ValueHolder(c("app_store"), that.flatMap(_.app_store)),
51
+ ValueHolder(c("app_version"), that.flatMap(_.app_version))
52
+ )
53
+ }
54
+
55
+ val bundleInfo = {
56
+ val c = Columns.find("bundle_info", _: String)
57
+ val that = userDim.bundle_info
58
+ Seq(
59
+ ValueHolder(c("bundle_sequence_id"), that.flatMap(_.bundle_sequence_id)),
60
+ ValueHolder(c("server_timestamp_offset_micros"), that.flatMap(_.server_timestamp_offset_micros))
61
+ )
62
+ }
63
+
64
+ val geoInfo = {
65
+ val c = Columns.find("geo_info", _: String)
66
+ val that = userDim.geo_info
67
+ Seq(
68
+ ValueHolder(c("city"), that.flatMap(_.city)),
69
+ ValueHolder(c("continent"), that.flatMap(_.continent)),
70
+ ValueHolder(c("country"), that.flatMap(_.country)),
71
+ ValueHolder(c("region"), that.flatMap(_.region))
72
+ )
73
+ }
74
+
75
+ val deviceInfo = {
76
+ val c = Columns.find("device_info", _: String)
77
+ val that = userDim.device_info
78
+ Seq(
79
+ ValueHolder(c("device_category"), that.flatMap(_.device_category)),
80
+ ValueHolder(c("device_id"), that.flatMap(_.device_id)),
81
+ ValueHolder(c("device_model"), that.flatMap(_.device_model)),
82
+ ValueHolder(c("device_time_zone_offset_seconds"), that.flatMap(_.device_time_zone_offset_seconds)),
83
+ ValueHolder(c("limited_ad_tracking"), that.flatMap(_.limited_ad_tracking)),
84
+ ValueHolder(c("mobile_brand_name"), that.flatMap(_.mobile_brand_name)),
85
+ ValueHolder(c("mobile_marketing_name"), that.flatMap(_.mobile_marketing_name)),
86
+ ValueHolder(c("mobile_model_name"), that.flatMap(_.mobile_model_name)),
87
+ ValueHolder(c("platform_version"), that.flatMap(_.platform_version)),
88
+ ValueHolder(c("resettable_device_id"), that.flatMap(_.resettable_device_id)),
89
+ ValueHolder(c("user_default_language"), that.flatMap(_.user_default_language))
90
+ )
91
+ }
92
+
93
+ val trafficSource = {
94
+ val c = Columns.find("traffic_source", _: String)
95
+ val that = userDim.traffic_source
96
+ Seq(
97
+ ValueHolder(c("user_acquired_campaign"), that.flatMap(_.user_acquired_campaign)),
98
+ ValueHolder(c("user_acquired_medium"), that.flatMap(_.user_acquired_medium)),
99
+ ValueHolder(c("user_acquired_source"), that.flatMap(_.user_acquired_source))
100
+ )
101
+ }
102
+
103
+ val ltvInfo = {
104
+ val c = Columns.find("ltv_info", _: String)
105
+ val that = userDim.ltv_info
106
+ Seq(ValueHolder(c("currency"), that.flatMap(_.currency)), ValueHolder(c("revenue"), that.flatMap(_.revenue)))
107
+ }
108
+
109
+ userFields ++ appInfo ++ bundleInfo ++ geoInfo ++ deviceInfo ++ trafficSource ++ ltvInfo
110
+ }
111
+ }
@@ -0,0 +1,5 @@
1
+ package org.embulk.parser.firebase_avro
2
+
3
+ import org.embulk.config.Task
4
+
5
+ trait PluginTask extends Task
@@ -0,0 +1,5 @@
1
+ package org.embulk.parser.firebase_avro
2
+
3
+ import org.embulk.spi.Column
4
+
5
+ case class ValueHolder[+A](column: Column, value: Option[A] = None)
@@ -0,0 +1,61 @@
1
+ package org.embulk.parser.firebase_avro.column
2
+
3
+ import org.embulk.spi.{Column => EmbulkColumn}
4
+ import org.embulk.spi.`type`.Types
5
+
6
+ import scala.language.implicitConversions
7
+
8
+ object Column {
9
+
10
+ import scala.reflect.runtime.universe._
11
+
12
+ def getGeneral[A](index: Int)(implicit tpe: TypeTag[A]): Seq[Column] = {
13
+ var counter = index
14
+ typeOf[A].members.filter(!_.isMethod).flatMap { implicit member =>
15
+ val column = member.typeSignature match {
16
+ case t if t =:= typeOf[Option[String]] =>
17
+ Some(factory(counter, Types.STRING))
18
+ case t if t =:= typeOf[Option[Int]] =>
19
+ Some(factory(counter, Types.LONG))
20
+ case t if t =:= typeOf[Option[Long]] =>
21
+ Some(factory(counter, Types.LONG))
22
+ case t if t =:= typeOf[Option[Double]] =>
23
+ Some(factory(counter, Types.DOUBLE))
24
+ case t if t =:= typeOf[Option[Boolean]] =>
25
+ Some(factory(counter, Types.BOOLEAN))
26
+ case t if t =:= typeOf[String] =>
27
+ Some(factory(counter, Types.STRING))
28
+ case t if t =:= typeOf[Int] =>
29
+ Some(factory(counter, Types.LONG))
30
+ case t if t =:= typeOf[Long] =>
31
+ Some(factory(counter, Types.LONG))
32
+ case t if t =:= typeOf[Double] =>
33
+ Some(factory(counter, Types.DOUBLE))
34
+ case t if t =:= typeOf[Boolean] =>
35
+ Some(factory(counter, Types.BOOLEAN))
36
+ case _ =>
37
+ counter = counter - 1
38
+ None
39
+ }
40
+ counter = counter + 1
41
+ column
42
+ }
43
+ }.toList
44
+
45
+ private def factory[A](counter: Int, tpe: org.embulk.spi.`type`.Type)(implicit tt: TypeTag[A],
46
+ symbol: Symbol): Column = {
47
+ val fullpath =
48
+ (tt.tpe.etaExpand.toString.toLowerCase + "." + symbol.name.toString).trim
49
+ Column(fullpath, new EmbulkColumn(counter, name, tpe))
50
+ }
51
+
52
+ def name[A](implicit tpe: TypeTag[A], symbol: Symbol): String = {
53
+ val fullpath = tpe.tpe.etaExpand.toString
54
+ val lessLength = "org.embulk.parser.firebase_avro.define.root.".length
55
+ (fullpath
56
+ .substring(lessLength)
57
+ .toLowerCase + "." + symbol.name.toString).trim
58
+ }
59
+ }
60
+
61
+ case class Column(fullPath: String, embulkColumn: EmbulkColumn)
@@ -0,0 +1,47 @@
1
+ package org.embulk.parser.firebase_avro.column
2
+
3
+ import org.embulk.spi.{Column => EmbulkColumn}
4
+
5
+ object Columns {
6
+ val instance = Columns()
7
+ private val map: Map[String, EmbulkColumn] = instance.map { c =>
8
+ c.fullPath -> c.embulkColumn
9
+ }.toMap
10
+
11
+ private var cache: Map[String, EmbulkColumn] = Map.empty
12
+
13
+ def find(className: String, fieldName: String): EmbulkColumn = {
14
+ cache.get(s"$className.$fieldName") match {
15
+ case Some(v) => v
16
+ case None =>
17
+ val detect = map.flatMap {
18
+ case (name, column) =>
19
+ if (name.contains(s"$className.$fieldName")) Some(column)
20
+ else None
21
+ }
22
+ if (detect.size == 1) {
23
+ val result = detect.head
24
+ cache = cache ++ Map(s"$className.$fieldName" -> detect.head)
25
+ result
26
+ } else
27
+ sys.error(s"could not find column. ${className + "." + fieldName}")
28
+ }
29
+ }
30
+
31
+ def apply(): Seq[Column] = {
32
+ val userColumns = UserDimension(0)
33
+ val eventColumns = EventDimension(userColumns.size)
34
+ userColumns ++ eventColumns
35
+ }
36
+
37
+ def generate(startIndex: Int)(s: Seq[((Int) => Seq[Column])]): Seq[Column] = {
38
+ var index = startIndex
39
+ s.foldLeft[List[Column]](Nil) {
40
+ case (a, b) =>
41
+ val additionalColumn = b(index)
42
+ index = index + additionalColumn.size
43
+ a ++ additionalColumn
44
+ }
45
+ }
46
+
47
+ }
@@ -0,0 +1,19 @@
1
+ package org.embulk.parser.firebase_avro.column
2
+
3
+ import org.embulk.spi.{Column => EmbulkColumn}
4
+ import org.embulk.spi.`type`.Types
5
+
6
+ object EventDimension {
7
+ def apply(index: Int): Seq[Column] = {
8
+ Columns.generate(index) {
9
+ List(
10
+ Column
11
+ .getGeneral[org.embulk.parser.firebase_avro.define.root.Event_Dim],
12
+ (i: Int) =>
13
+ List(
14
+ Column("org.embulk.parser.firebase_avro.define.root.event_dim.params",
15
+ new EmbulkColumn(i, "event_dim.params", Types.JSON)))
16
+ )
17
+ }
18
+ }
19
+ }
@@ -0,0 +1,28 @@
1
+ package org.embulk.parser.firebase_avro.column
2
+
3
+ import org.embulk.parser.firebase_avro.define.root.user_dim._
4
+ import org.embulk.spi.{Column => EmbulkColumn}
5
+ import org.embulk.spi.`type`.Types
6
+
7
+ object UserDimension {
8
+ def apply(index: Int): Seq[Column] = {
9
+ Columns.generate(index) {
10
+ List(
11
+ Column
12
+ .getGeneral[org.embulk.parser.firebase_avro.define.root.User_Dim],
13
+ (i: Int) =>
14
+ List(
15
+ Column("org.embulk.parser.firebase_avro.define.root.user_dim.user_properties",
16
+ new EmbulkColumn(i, "user_dim.user_property", Types.JSON))),
17
+ Column.getGeneral[Device_Info],
18
+ Column.getGeneral[Geo_Info],
19
+ Column.getGeneral[App_Info],
20
+ Column.getGeneral[Traffic_Source],
21
+ Column.getGeneral[Bundle_Info],
22
+ Column.getGeneral[Ltv_Info]
23
+ )
24
+ }
25
+
26
+ }
27
+
28
+ }