Json Jolt教程
这是国内外目前第一篇较为详细系统的讲述Java JOLT用法及部分原理的文章,如有错误,请及时留言指出。如有转载,请标明出处。
Jolt GitHub: https://github.com/bazaarvoice/jolt
Jolt online demo: https://jolt-demo.appspot.com
Jolt是用Java编写的JSON到JSON转换库,其中指示如何转换的"specification"本身就是一个JSON文档。以下文档中,我统一以 Spec 代替如何转换的"specification"json文档。以LHS(left hand side)代表Spec json的keys,RHS(right hand side)代表Spec json的values。部分示例都是摘取于Jolt源代码注释文档。
用处:
- 将从ElasticSearch、MongoDb、Cassandra等等取出的数据转换后输出出来
- 从大型JSON文档中提取数据供自己使用
概览
Jolt :
- 提供一组转换,可以将它们"链接(chained)"在一起,形成整个JSON到JSON的转换。
- 重点是转换JSON数据的结构,而不是操作特定的值
- 其思想是:使用Jolt正确的来获得大部分结构,然后编写代码来修正值
- 消费和生产json : in-memory tree of Maps, Lists, Strings, etc.
- 使用Jackson (或者其他工具) 来序列化和反序列化json文本
其他同类工具
- jq从JSON文件中提取数据的命令行工具(随时使用,可通过brew获得)
- JsonPathJava:使用类似XPATH的语法从JSON中提取数据。
- JsonSurferJava:流式JsonPath处理器,专门处理大而复杂的JSON数据。
性能表现
Jolt的主要目标是提供一种可声明的方式快速的去转换json,也就是说,与上面列出的同类工具选项相比,Jolt应该有一个更好的运行效率。
实际运行中:
- 转换可以使其Spec初始化一次,并在多线程环境中多次重用
- '*'通配符逻辑被重做,以减少在常见情况下使用Regex,这是一个显著的速度改进。
- 对Shiftr并行树步进行了优化。
有两点需要注意:
- Jolt不是基于流的,所以如果有一个非常大的Json文档要转换,则需要有足够的内存来容纳它。
- 转换过程将创建和丢弃大量对象,因此垃圾收集器将有工作要做。
Stock Transforms
shift : 复制输入json到输出json
default : 为json树增加默认值
remove : 从json树中去除数据
sort : 按字母顺序排序映射键值(一般用于调试和增加人工可读性)
cardinality : 修正输入数据的基数。urls元素通常是一个List,但是如果只有一个,那么它就是一个字符串
modify-overwrite-beta:总是写
modify-default-beta:当键值对应的值是null时写入
modify-define-beta:当键值不存在时写入
自定义Java类全路径名称:实现Transform或ContextualTransform接口,可选择SpecDriven接口
每个转换都有自己的DSL(领域特定语言),以便简化其工作。 目前,上面前五个转换只影响数据的结构。要进行数据操作,需要编写Java代码或者使用modify。如果你编写Java代码实现了转换接口,那么你可以将代码插入转换链中。 使用自定义Java转换实现数据操作,开箱即用的Java转换应该能够完成大多数结构转换
在线的谷歌文档PPT地址:https://docs.google.com/presentation/d/1sAiuiFC4Lzz4-064sg1p8EQt2ev0o442MfEbvrpD1ls/edit?usp=sharing
shift
Shiftr指定来自输入JSON的数据应该放在输出JSON中的什么位置,也就是输入JSON的数据应该如何进行移位,以生成输出JSON数据。
在底层,一个Shiftr命令是从输入路径到输出路径的映射,类似于Unix中的"mv"命令"mv /var/data/mysql/data /media/backup/mysql"。
在Shiftr中,输入路径是JSON树结构,输出路径是扁平的点标记法路径。
你可以从JSON输入数据的一个副本开始,然后通过为你关心的每段数据提供点表示法输出路径,将其修改为Shiftr Spec。
例如:给定如下输入的json
{
"rating": {
"quality": {
"value": 3,
"max": 5
}
}
}
一个简单的Shiftr规范可以通过复制输入,并修改它来为每段数据提供输出路径:
{
"rating": {
"quality": {
"value": "SecondaryRatings.quality.Value", // copy 3 to "SecondaryRatings.quality.Value"
"max": "SecondaryRatings.quality.RatingRange" // copy 5 to "SecondaryRatings.quality.RatingRange"
}
}
}
会得到如下的输出:
{
"SecondaryRatings": {
"quality": {
"Value": 3,
"RatingRange": 5
}
}
}
如上所示,Shiftr Spec可以完全由文字字符串值组成,但是它的强大功能是来自通配符。通过使用通配符,你可以做到不仅知道数据及其直接键,而且知道到该数据的整个输入路径。
例如有如下输入json
{
"rating": {
"primary": {
"value": 3, // want this value to goto output path "Rating"
"max": 5 // want this value to goto output path "RatingRange"
},
"quality": { // want output path "SecondaryRatings.quality.Id" = "quality", aka we want the value of the key to be used
"value": 3, // want this value to goto output path "SecondaryRatings.quality.Value"
"max": 5 // want this value to goto output path "SecondaryRatings.quality.Range"
},
"sharpness": { // want output path "SecondaryRatings.sharpness.Id" = "sharpness"
"value": 7, // want this value to goto output path "SecondaryRatings.sharpness.Value"
"max": 10 // want this value to goto output path "SecondaryRatings.sharpness.Range"
}
}
}
Spec:
{
"rating": {
"primary": {
"value": "Rating", // 输出 -> "Rating" : 3
"max": "RatingRange" // 输出 -> "RatingRange" : 5
},
"*": { // 匹配类似 "rating.[除primary以外的节点]"
"value": "SecondaryRatings.&1.Value", // "rating.*.value"位置的数据移动到 "SecondaryRatings.*.Value"
// "&1" 表示取计算到&1式子时的WalkedPathTree的上一层的WalkedPath并取subKey(下面会有讲解, 例子中对应的值是"quality"或"sharpness" )
// 输出 -> "SecondaryRatings.quality.Value" : 3
// "SecondaryRatings.sharpness.Value" : 7
"max": "SecondaryRatings.&1.Range", // "rating.*.max"位置的数据移动到 "SecondaryRatings.*.Range"
// 输出 -> "SecondaryRatings.quality.Range" : 5
// "SecondaryRatings.sharpness.Range" : 10
"$": "SecondaryRatings.&1.Id" // $ 表示使用输入键本身的值作为数据
// 输出 -> "SecondaryRatings.quality.Id" : "quality"
// 输出 -> "SecondaryRatings.sharpness.Id" : "sharpness"
}
}
}
得到以下输出:
{
"Rating": 3,
"RatingRange": 5,
"SecondaryRatings": {
"quality": {
"Range": 5,
"Value": 3,
"Id": "quality" // the special $ operator allows us to use input key the text value of "quality", as the "Id" of the output
},
"sharpness": {
"Range": 10,
"Value": 7,
"Id": "sharpness" // the special $ operator allows us to use input key the text value of "sharpness", as the "Id" of the output
}
}
}
Shiftr WalkedPathTree
首先,这部分在官方文档、国内外学习资料都是没有的,是本人在看了部分源码和实践后总结而来,可能会有些错误,如果有人看出,请一定留言指正!
理论:
Jolt在shiftr中,依据Spec处理input的时候,会时刻保持一颗动态的 WalkedPath,总体来讲是一个链表。当做新的匹配计算操作,就会把当前信息(其中有一颗refTree,refTree是input json的一个引用,还有一个subKey,是匹配到的键值)保存到WalkedPath中。当到Spec树中的树叶节点时计算完成时,也就是Spec树某一树枝下的最后一个计算操作完成时(完成包括匹配计算和输出计算),会从WalkedPath中删除掉之前存储的当前信息。WalkedPath虽然外层是一个链表,但是因为通配符,同一级多个匹配等等的作用(代码里有递归),画出完整的WalkedPath的轨迹图就成了一颗树。
比如说我们在上面的例子里看到了一个&1,&是取相应节点的键值的意思,会在下面有详细说明。依据上面的示例,我们画一下当第一次计算到&1时的WalkedPathTree
下面是程序debug到第一次计算到&1的时候的截图:
进一步获取&1结果的截图:
Shiftr 通配符
'*'通配符
- 只允许在LHS使用
- 通配符 * 可以匹配整个key字符串,也可以用于匹配key字符串的一部分。
匹配整个key字符串:
输入:
{
"rating" : {
"quality": {
"value": 3,
"max": 5
},
"sharpness" : {
"value" : 7,
"max" : 10
}
}
}
在这个例子中, "rating.quality"和"rating.sharpness" 都有相同的结构,这样我们可以使用通配符 * 来匹配"quality"和"sharpness",避免为"quality"和"sharpness"编写非常相似的规则。 Spec:
[
{
"operation": "shift",
"spec": {
"rating": {
"*": {
"max": "Rating.[#2].amx",
"value": "Rating.[#2].value"
}
}
}
}
]
用于匹配key的一部分:
输入:
{
"tag-Pro": "Awesome",
"tag-Con": "Bogus"
}
可以使用'tag-*'来匹配"tag-Pro"和"tag-Con" Spec:
[
{
"operation": "shift",
"spec": {
"tag-*": "&(0,1)"
}
}
]
结果:
'&'通配符
- 在LHS和RHS都能使用,意义为取WalkedPath中的subKey的值来使用
- 在Spec标准格式其实是 &(0,0),有两个参数,第一个参数是指明在当前WalkedPath向上几个level,第二个参数是指明使用第几个subKey。& = &0 = &(0) = &(0,0) ,第二个参数是与*通配符一起使用的。
例如,输入json:
{
"test": {
"test": "value",
"test2": "value2"
}
}
Spec:
[
{
"operation": "shift",
"spec": {
"test": {
"&": "ratingNames"
}
}
}
]
&计算取到的值是test,再去匹配test 结果:
&在输入路径寻找值:
{
"foo" : {
"bar": {
"baz": // &0 = baz, &1 = bar, &2 = foo
}
}
}
需要指明使用第几个匹配值,比如说"tag-*-*"匹配到"tag-Foo-Bar",那么:
&(0,0) = "tag-Foo-Bar"
&(0,1) = "Foo"
&(0,2) = "Bar"
`$`通配符
- 只在LHS使用
- 这个通配符的存在反映了一个事实,即输出JSON的value既可以是输入JSON的value,也可以是输入JSON的key
- Shiftr的基本操作是复制输入JSON的value,因此我们需要一种方法来指定我们想要复制输入JSON的key。因此,
$
指定我们希望使用输入键或输入键派生的值作为要放在输出JSON中的数据。
有两个使用场景:
- 当输入JSON中的key需要是输出JSON中的id(某个位置)值时,请查看上面的例子"$":"SecondaryRatings.&1.Id"。
- 需要列出所有输入键。
比如有如下输入json
{
"rating": {
"primary": {
"value": 3,
"max": 5
},
"quality": {
"value": 3,
"max": 7
}
}
}
我们期望的输出结果:
{
"ratings" : [ "primary", "quality" ] // Aside: this is an example of implicit JSON array creation in the output which is detailed further down.
// For now just observe that the input keys "primary" and "quality" have both made it to the output.
}
那么相应的Spec:
{
"rating": {
"*": { // match all keys below "rating"
"$": "ratings" // output each of the "keys" to "ratings" in the output
}
}
}
'#'通配符
- 在LHS RHS都可以使用,但是意义不同
- 在RHS, #号只在数组中有效 比如[#2],[#2]的意思是,向上两层(WalkedPathTree)并询问该节点有多少匹配,然后将其用作数组中的索引。这意味着,当Shiftr执行输入数据和Spec的并行树遍历时,它会跟踪在Spec树的每个级别上处理了多少匹配项。如果您想将一个JSON映射转换成一个JSON数组,而不关心数组的顺序,这是非常有用的。
- 在LSH中,#允许您指定硬编码字符串作为输出中的值。
- 在LSH中,#是根节点,不允许有子节点
示例Spec,如果hidden该值为布尔真,则写出"enabled"字符串。
"hidden" : {
"true" : { // 如果"hidden" 值是true
"#disabled" : "clients.clientId" //将 "disabled" 值写到 "clients.clientId",标准格式是 #(disabled) #disabled会自动转成#(disabled)
}
}
按照这个例子,完全是可以在shiftr里做到为json添加常量。
再来看一个例子:
输入json
{
"ratings": {
"primary": 5,
"quality": 4,
"design": 5
}
}
Spec:
[
{
"operation": "shift",
"spec": {
"ratings": {
"*": {
//
// #2 means go three levels up the tree (count from 0),
// and ask the "ratings" node, how many of it's
// children have been matched.
//
// This allows us to put the Name and the Value into
// the same object in the Ratings array.
"$": "Ratings[#2].Name",//$上文已经讲解 使key作为输出value
"@": "Ratings[#2].Value"//@下文会讲解
}
}
}
}
]
输出json:
{
"Ratings" : [ {
"Name" : "primary",
"Value" : 5
}, {
"Name" : "quality",
"Value" : 4
}, {
"Name" : "design",
"Value" : 5
} ]
}
Jolt demo注释说 '#2 means go three levels up the tree (count from 0)',但我认为3层的叙述是模糊的,WalkedPath是一个数组,从0开始,假设长度为5,则向上2层对应的索引下标为5-1-2 = 2 向上2层的理解其实是最好的。 其中源码关键部分如下:
手绘图walkedPathTree如下:
'|'通配符
- 只在LHS生效
或的意思,很简单,直接看Spec例子吧:
Spec:
{
"rating|Rating" : "rating-primary" // match "rating" or "Rating" copy the data to "rating-primary"
}
'@'通配符
- Spec LHS、RHS都有效,意义一样
- 在LHS,@是根节点,不允许有子节点
如果希望将输入值和输入键都放在输出JSON中的某个位置,则必须使用这个通配符。
比如
{
"foo" : "place.to.put.value", // leveraging the implicit operation of Shiftr which is to operate on input JSON values
}
如果我们想要对键和值做一些事情: Spec
{
"foo" : {
"$" : "place.to.put.key",
"@" : "place.to.put.value" // '@' explicitly tell Shiftr to operate on the input JSON value of the parent key "foo"
}
}
因此,@通配符是将树中此级别的数据值复制到输出,比如上面例子中@是把foo对应的值copy到place.to.put.value
@的高级用法的标准格式是"@(3,title)",上文中已经介绍过WalkedPathTree这个概念,3的意思在计算到@(3,title)这一level时,返回WalkedPathTree向上3level对应的refTree,然后在返回的数据中去匹配title。
示例:
计算@(2,test)时的debug截图:
然后在最后计算@(2,test2)的时候,walkedPathTree又增加了一层(其实@(2,test2) @(3,test2)分别返回1层和0层的treeRef,都是root根节点的子树,效果是一样的,如下图:)
将@(2,test2)换成@(3,test2)发现结果其实是一样的:
单独的@等价于@0:
因为@都是同一级,所以计算他们的时候WalkedPath是相同的
@通配符在RHS的示例:
input json
{
"author" : "Stephen Hawking",
"book" : "A Brief History of Time"
}
期望输出json
{
"Stephen Hawking" : "A Brief History of Time"
}
Spec
{
"@author" : "@book"
}
@book RHS,其实是比计算@author时的walkedPathTree多一层的
json数组
shift把json数组当做Map
处理输入的json数组
例如有输入json:
{
"Photos": [ "AAA.jpg", "BBB.jpg" ]
}
Spec
{
"Photos" :
{
"1" : "photo-&-url" // 指定只匹配数组下标为1的数据
}
}
输出json:
{
"photo-1-url": "BBB.jpg"
}
处理输出的json数组
[]用于在json数组中指定数组索引,只在RHS生效。
比如有输入json:
{
"photo-1-id": "327704",
"photo-1-url": "http://bob.com/0001/327704/photo.jpg"
}
Spec:
{
"photo-1-id": "Photos[1].Id", // Declare the "Photos" in the output to be an array,
"photo-1-url": "Photos[1].Url" // that the 1-th array location should have data
// same as above but more powerful
// note '&' logic can be used inside the '[ ]' notation "photo-*-url": "Photos[&(0,1)].Url"
}
输出json:
{
"Photos": [
null , // note Photos[0] is null, because no data was pushed to it
{
"Id":"327704",
"Url":"http://bob.com/0001/327704/photo.jpg"
}
]
}
Spec中的json数组
同一个输入源,可以输出到多个目标输出处,可以使用数组的形式,如下
输入json:
{ "foo" : 3 }
Spec:
{ "foo" : [ "bar", "baz" ] }
输出json:
{
"bar" : 3,
"baz" : 3
}
隐式数组
如果Spec使得多个匹配写到同一个输出位置,输出变成数组
比如输入json:
{
"foo" : "bar",
"tuna" : "marlin"
}
Spec:
{
"foo" : "baz",
"tuna" : "baz"
}
输出:
{
"baz" : [ "bar", "marlin" ] // Note the order of this Array should not be relied upon
}
算法
(个人理解,有错请指出)
Algorithm High Level
同时处理输入数据和Shiftr规范,并在每次匹配时执行Shiftr命令/映射。
Algorithm Low Level
根据Spec的不同有两种算法,第一种是无通配符的,算法是同时处理输入数据和Shiftr规范,并在每次匹配时执行Shiftr命令/映射。
第二种含有多个通配符,算法是同时走Spec和输入JSon,并保持一个匹配输入路径数据结构。确定输入JSON键和RHS的匹配,按照以下顺序匹配spec键:
注意,'|'键的顺序要分割成他们的子键来算
- 尝试将输入键与具体键值匹配
- 如果没有找到匹配,尝试匹配'&'计算值。
- 2.1如果存在一个以上的"&"键,按字母顺序应用/匹配它们。
- 如果没有找到匹配,试着用"*"通配符值匹配
- 3.1'*'通配符按字母顺序排序和应用/匹配。
注意,如果'@'和的父键匹配,则始终执行'@'和键的处理,并且不会阻止任何其他匹配。
default
default它以一种非破坏性的方式应用默认值。
比较:
- Shitr遍历输入数据并询问它的Spec"它应该放在哪里?"
- Defaultr遍寻Spec并询问"数据中是否存在这种情况?"如果没有,那就加上去。"
例如,输入json:
{
"Rating":3,
"SecondaryRatings":{
"quality":{
"Range":7,
"Value":3,
"Id":"quality"
},
"sharpness": {
"Value":4,
"Id":"sharpness"
}
}
}
期望输出:
{
"Rating":3,
"RatingRange" : 5,
"SecondaryRatings":{
"quality":{
"Range":7,
"Value":3,
"Id":"quality",
"ValueLabel": null,
"Label": null,
"MaxLabel": "Great",
"MinLabel": "Terrible",
"DisplayType": "NORMAL"
},
"sharpness": {
"Range":5,
"Value":4,
"Id":"sharpness",
"ValueLabel": null,
"Label": null,
"MaxLabel": "High",
"MinLabel": "Low",
"DisplayType": "NORMAL"
}
}
}
Spec
{
"RatingRange" : 5,
"SecondaryRatings": {
"quality|value" : {
"ValueLabel": null,
"Label": null,
"MaxLabel": "Great",
"MinLabel": "Terrible",
"DisplayType": "NORMAL"
},
"*": {
"Range" : 5,
"ValueLabel": null,
"Label": null,
"MaxLabel": "High",
"MinLabel": "Low",
"DisplayType": "NORMAL"
}
}
}
通配符
- '*' 将这些默认值应用于此级别的所有输入键
- '|' 将这些默认值应用到输入键(如果存在的话)
- '[]' 表名输入键应该是一个数组,这意味着这个条目下面的所有defaultr键必须是整数。
比如Spec:
{
"photos[]" : {
"2" : {
"url" : "http://www.bazaarvoice.com",
"caption" : ""
}
}
}
算法:
- 深度优先执行Spec
- 在Spec树的每一层,Defaultr从最具体到最不具体的Spec键:
- 优先匹配对比具体值
- "|",根据有多少个或值进行子排序,然后按字母顺序排序(用于确定性行为)
- "*"
在Defaultr Spec树的给定级别上,只有文字键强制Defaultr在输入数据中创建新条目:要么作为单个文字值,要么添加新的嵌套数组或映射对象。通配符操作符是在文字键之后应用的,如果这些键在输入文档中还没有出现,则不会导致添加这些键(自然地或者已经从文字规范键中默认添加)。
- 1 遵循Spec
- 2 对于spec中的每个文字键(specKey)
- 如果specKey是map或array,输入为null,则默认输出为空map或array
- 递归文字键
- 如果specKey是一个map或数组,而输入不是null,而是"wrong"类型,跳过,不要递归
- 如果specKey是一个文字值,则默认输出的文字和值,不要递归
- 3 对于Spec中的每个通配符
- 从默认值中找到所有匹配通配符的键值
- 将每个键值视为一个文本speckey
为了避免歧义,如果输入为null, Defaultr抛出一个异常。
remove
remove是一种从输入JSON中删除内容的转换。
对比:
- Shitr遍历输入数据并询问它的Spec"它应该放在哪里?"
- Defaultr遍寻Spec并询问"数据中是否存在这种情况?"如果没有,那就加上去。"
- Removr遍寻Spec并询问"如果存在,就删除"
例如:
输入json:
{
"~emVersion" : "2",
"id":"123124",
"productId":"31231231",
"submissionId":"34343",
"this" : "stays",
"configured" : {
"a" : "b",
"c" : "d"
}
}
期望输出:
{
"id":"123124",
"this" : "stays",
"configured" : {
"a" : "b"
}
}
Spec:
{
"~emVersion" : "",
"productId":"",
"submissionId":"",
"configured" : {
"c" : ""
}
}
通配符
'*' 只在LHS,可匹配整个键值或者键值的一部分
比如匹配整个键值 输入json:
{
"ratings": {
"Set1": {
"a": "a",
"b": "b"
},
"Set2": {
"c": "c",
"b": "b"
}
}
}
期望输出json:
{
"ratings": {
"Set1": {
"a": "a"
},
"Set2": {
"c": "c"
}
}
}
Spec:
{
"ratings":{
"*":{
"b":""
}
}
}
比如匹配键值的一部分,输入json:
{
"ratings_legacy": {
"Set1": {
"a": "a",
"b": "b"
},
"Set2": {
"a": "a",
"b": "b"
}
},
"ratings_new": {
"Set1": {
"a": "a",
"b": "b"
},
"Set2": {
"a": "a",
"b": "b"
}
}
}
Spec
{
"ratings_*": {
"Set1": ""
}
}
输出json:
{
"ratings_legacy" : {
"Set2" : {
"a" : "a",
"b" : "b"
}
},
"ratings_new" : {
"Set2" : {
"a" : "a",
"b" : "b"
}
}
}
再举两个匹配数组索引的例子:
匹配数组每个元素删除其中一个成员:
匹配删除索引为0的数组元素:
cardinality
改变输入JSON数据元素的基数(单个还是数组)。CardinalityTransform的目的是处理与返回数据的基数不一致的数据源。
例如,假设您知道文档中将有一个"photos"元素。如果您的底层数据源想要变得更好,那么它可能会根据实际有多少张照片来调整photos元素的"类型"。
关键词
- 只在RHS
'ONE':如果输入值是一个列表,则获取该列表中的第一个元素,并将其设置为该元素的数据,不支持其他类型
'MANY':如果输入不是列表,则创建一个列表并将第一个元素设置为输入值。如果输入是"null",使它成为一个空列表。如果输入是一个列表,不支持。
比如:
输入json:
{
"review" : {
"rating" : [ 5, 4 ]
}
}
Spec:
{
"review" : {
"rating" : "ONE"
}
}
输出json:
{
"review" : {
"rating" : 5
}
}
通配符
'*' 只在RHS,只能匹配整个键值,不能匹配键值的一部分。
'@' 只在RHS,当需要修改嵌套在修改内容中的内容时,应该使用这个通配符。
比如:
输入json:
{
"views" : [
{ "count" : 1024 },
{ "count" : 2048 }
]
}
Spec
{
"views" : {
"@" : "ONE",
"count" : "MANY"
}
}
输出json:
{
"views" : {
"count" : [ 1024 ]
}
}
sort
递归地将JSON对象内的所有映射排序为新的已排序的LinkedHashMaps,从而使序列化的表示具有确定性。用于调试和测试。
注意,这将复制输入映射和列表对象。
排序顺序是标准的按字母顺序升序排列,带"~"前缀的键的特殊情况将被移到顶部。
modify
这部分文档源码项目也没有整理,以下是个人由源码以及示例整理而出
overwrite modify-overwrite-beta Overwritr:总是写
default modify-default-beta Defaultr:当键值对应的值是null时写入
define modify-define-beta Definr:当键值不存在时写入
modify模式包含了一系列函数,函数其本质是Java自带的,在Spec以下是函数及其对应的源码:
private static final Map<String, Function> STOCK_FUNCTIONS = new HashMap<>( );
static {
STOCK_FUNCTIONS.put( "toLower", new Strings.toLowerCase() );
STOCK_FUNCTIONS.put( "toUpper", new Strings.toUpperCase() );
STOCK_FUNCTIONS.put( "concat", new Strings.concat() );
STOCK_FUNCTIONS.put( "join", new Strings.join() );
STOCK_FUNCTIONS.put( "split", new Strings.split() );
STOCK_FUNCTIONS.put( "substring", new Strings.substring() );
STOCK_FUNCTIONS.put( "trim", new Strings.trim() );
STOCK_FUNCTIONS.put( "leftPad", new Strings.leftPad() );
STOCK_FUNCTIONS.put( "rightPad", new Strings.rightPad() );
STOCK_FUNCTIONS.put( "min", new Math.min() );
STOCK_FUNCTIONS.put( "max", new Math.max() );
STOCK_FUNCTIONS.put( "abs", new Math.abs() );
STOCK_FUNCTIONS.put( "avg", new Math.avg() );
STOCK_FUNCTIONS.put( "intSum", new Math.intSum() );
STOCK_FUNCTIONS.put( "doubleSum", new Math.doubleSum() );
STOCK_FUNCTIONS.put( "longSum", new Math.longSum() );
STOCK_FUNCTIONS.put( "intSubtract", new Math.intSubtract() );
STOCK_FUNCTIONS.put( "doubleSubtract", new Math.doubleSubtract() );
STOCK_FUNCTIONS.put( "longSubtract", new Math.longSubtract() );
STOCK_FUNCTIONS.put( "divide", new Math.divide() );
STOCK_FUNCTIONS.put( "divideAndRound", new Math.divideAndRound() );
STOCK_FUNCTIONS.put( "toInteger", new Objects.toInteger() );
STOCK_FUNCTIONS.put( "toDouble", new Objects.toDouble() );
STOCK_FUNCTIONS.put( "toLong", new Objects.toLong() );
STOCK_FUNCTIONS.put( "toBoolean", new Objects.toBoolean() );
STOCK_FUNCTIONS.put( "toString", new Objects.toString() );
STOCK_FUNCTIONS.put( "size", new Objects.size() );
STOCK_FUNCTIONS.put( "squashNulls", new Objects.squashNulls() );
STOCK_FUNCTIONS.put( "recursivelySquashNulls", new Objects.recursivelySquashNulls() );
STOCK_FUNCTIONS.put( "noop", Function.noop );
STOCK_FUNCTIONS.put( "isPresent", Function.isPresent );
STOCK_FUNCTIONS.put( "notNull", Function.notNull );
STOCK_FUNCTIONS.put( "isNull", Function.isNull );
STOCK_FUNCTIONS.put( "firstElement", new Lists.firstElement() );
STOCK_FUNCTIONS.put( "lastElement", new Lists.lastElement() );
STOCK_FUNCTIONS.put( "elementAt", new Lists.elementAt() );
STOCK_FUNCTIONS.put( "toList", new Lists.toList() );
STOCK_FUNCTIONS.put( "sort", new Lists.sort() );
}
示例1
输入json:
{
"x": [ 3, 2, 1, "go" ],
"small": "small",
"BIG": "BIG",
"people": [
{
"firstName": "Bob",
"lastName": "Smith",
"address": {
"state": null
}
},
{
"firstName": "Sterling",
"lastName": "Archer"
}
]
}
Spec 使用相应函数基本格式是 =函数名(参数1,参数2)
[
{
"operation": "modify-default-beta",//值为null时写入
"spec": {
// String join the values in the array x, with a comma and a space
"y": "=join(',',@(1,x))",
"z": "=join(' ',@(1,x))",
//
// make small big, and make big small
"small_toUpper": "=toUpper(@(1,small))",
"BIG_toLower": "=toLower(@(1,BIG))",
//
// Here we are dealing with an array of people objects.
// We want to build a fullName for them, and default them
// to 'live' in Texas, only if they don't already have an address defined.
"people": {
"*": {
// build the fullName from the first and last names
//
// @(1,firstName)
// - is "evaluated" before being passed to the =concat function.
// - means go up two levels, and then come back down and grab
// the value from firstName
"fullName": "=concat(@(1,firstName),' ',@(1,lastName))",
//
// Suffix of "?" means only match if the input actually has an "address"
"address?": {
// The transform "modify-default-beta" will only match if the
// "left hand side" does not exist or is null
"state": "Texas"
}
}
}
}
}
]
举个例子,将一个json中key全部转小写:
[
{
"operation": "shift",
"spec": {
"*": {
"$": "&1.key",
"@": "&1.value"
}
}
},
{
"operation": "modify-overwrite-beta",
"spec": {
"*": {
"key": "=toLower"
}
}
},
{
"operation": "shift",
"spec": {
"*": {
"value": "@(1,key)"
}
}
}
]
扩展自定义
完全限定的Java ClassName:类实现Transform 或ContextualTransform接口,并且可以选择是SpecDriven 的(标记接口)
Transform
SpecDriven
Spec类似于:
[
{
"operation": "com.bazaarvoice.tuna.CustomTransform",
"spec" : { ... } // optional spec to use to construct a custom {@link Transform} if it has the {@link SpecDriven} marker interface.
},
//...
]
Chainr
Chainr是一种机制,可以把多个转换Spec连接在一起,类似于:
[
{
"operation": "[operation-name]",
// stuff that the specific transform needs go here
},
//...
]
operation-name 可以是shift、default、remove、cardinality、sort、java、modify-overwrite-beta、modify-default-beta、modify-define-beta 示例:
{
// Input data for the unit test
"input": {
"rating": {
"primary": {
"value": 3,
"max": 5
},
"sharpness": {
"value": 5,
"max": 5
},
"quality": {
"value": 3,
"max": 7
}
}
},
// The test uses this Chainr spec, on the input data to produce the "expected" JSON below
"spec": [
{
// first operation is shift
"operation": "shift",
"spec": {
"rating": {
"primary": {
"value": "Rating",
"max": "RatingRange"
},
"*": {
"value": "SecondaryRatings.&1.Value",
"max": "SecondaryRatings.&1.Range",
"$": "SecondaryRatings.&.Id"
}
}
}
},
{
// second operation is to default some values
"operation": "default",
"spec": {
"~z": "zz",
"~a": "aa",
"~id": "id",
"zz": "zz",
"~deleteme": "deleteme"
}
},
{
// third operation is to remove something
"operation": "remove",
"spec": {
"~deleteme": ""
}
},
{
// last operation is to sort the JSON
"operation": "sort"
}
],
// The expected result
"expected": {
"~a": "aa",
"~id": "id",
"~z": "zz",
"Rating": 3,
"RatingRange": 5,
"SecondaryRatings": {
"quality": {
"Id": "quality",
"Range": 7,
"Value": 3
},
"sharpness": {
"Id": "sharpness",
"Range": 5,
"Value": 5
}
},
"zz": "zz"
}
}
再举一个我在JOLT GitHub Issue(Conditional based on a input field #900)上看到的一个稍微比较复杂点的实际问题
输入json如下:
{
"addressBook": {
"primaryAddress": "pxDdd",
"addresses": [
{
"id": "pxDd",
"fullName": "Daniel Garcia",
"street": "Berenguer",
"houseNumber": "234",
"houseName": "",
"floorNo": "",
"streetType": "Str",
"doorNo": "",
"postOfficeBox": "",
"city": "Barcelona",
"zipCode": "08812",
"state": "",
"province": "",
"addressLine1": "",
"addressLine2": "",
"addressLine3": "",
"countryCode": "ES"
},
{
"id": "pxDdd",
"fullName": "Daniel Garcia",
"street": "Berenguer",
"houseNumber": "234",
"houseName": "",
"floorNo": "",
"streetType": "Str",
"doorNo": "",
"postOfficeBox": "",
"city": "Barcelona",
"zipCode": "08812",
"state": "",
"province": "",
"addressLine1": "",
"addressLine2": "",
"addressLine3": "",
"countryCode": "ES"
}
]
}
}
需求是筛选出addresses数组中id的值与primaryAddress值相等的成员,如果primaryAddress字段不存在,则默认输出数据中第一个元素。如果有多个匹配值,则输出第一个匹配值。
我给出的Spec方案如下,这个Spec中就串联了default shift cardinality三种操作:
[
{
"operation": "default",//如果不存在primaryAddress,则增加一个值为空的primaryAddress
"spec": {
"addressBook": {
"primaryAddress": ""
}
}
},
{
"operation": "shift",//第一次改变json格式,目的是能够做id与primaryAddress值匹配
"spec": {
"addressBook": {
"primaryAddress": "addressBook.primaryAddress",
"addresses": {
"*": "addressBook.addresses.@(2,primaryAddress)"
}
}
}
},
{
"operation": "shift",
"spec": {
"addressBook": {
"primaryAddress": {
"": { // 如果primaryAddress值是"",说明字段不存在,则去addresses数组第一个值输出
"@(2,addresses)": {
"": {
"0": "address[]"
}
}
}
},
"addresses": {
"*": {
"*": {
"id": {
"&2": { // 如果primaryAddress与id值相同,则取当前这个元素输出
"@2": "address[]"
}
}
}
}
}
}
}
}, {
"operation": "cardinality",
"spec": {
"address": "ONE" //如果是多个输出,则取第一个
}
}
]
结果如图:
Java调用
添加Maven Dependency
<dependency>
<groupId>com.bazaarvoice.jolt</groupId>
<artifactId>jolt-core</artifactId>
<version>${latest.jolt.version}</version>
</dependency>
<dependency>
<groupId>com.bazaarvoice.jolt</groupId>
<artifactId>json-utils</artifactId>
<version>${latest.jolt.version}</version>
</dependency>
点击查看版本信息
- jolt-core :only one dependency on apache.commons for StringUtils.The goal is for the jolt-core artifact to be pure Java, so that it does not cause any dependency issues.
- json-utils : Jackson wrapper and testing utilities. Used by jolt-core as a test dependency.If you are willing to pull in Jackson 2, this artifact provides nice utility methods.
示例代码:
package com.bazaarvoice.jolt.sample;
import com.bazaarvoice.jolt.Chainr;
import com.bazaarvoice.jolt.JsonUtils;
import java.io.IOException;
import java.util.List;
public class JoltSample {
public static void main(String[] args) throws IOException {
// How to access the test artifacts, i.e. JSON files
// JsonUtils.classpathToList : assumes you put the test artifacts in your class path
// JsonUtils.filepathToList : you can use an absolute path to specify the files
List chainrSpecJSON = JsonUtils.classpathToList( "/json/sample/spec.json" );
Chainr chainr = Chainr.fromSpec( chainrSpecJSON );
Object inputJSON = JsonUtils.classpathToObject( "/json/sample/input.json" );
Object transformedOutput = chainr.transform( inputJSON );
System.out.println( JsonUtils.toJsonString( transformedOutput ) );
}
}
输入json:
{
"rating": {
"primary": {
"value": 3
},
"quality": {
"value": 3
}
}
}
Spec
[
{
"operation": "shift",
"spec": {
"rating": {
"primary": {
"value": "Rating"
},
"*": {
"value": "SecondaryRatings.&1.Value",
"$": "SecondaryRatings.&.Id"
}
}
}
},
{
"operation": "default",
"spec": {
"Range" : 5,
"SecondaryRatings" : {
"*" : {
"Range" : 5
}
}
}
}
]
输出json:
{
"Rating": 3,
"Range": 5,
"SecondaryRatings": {
"quality": {
"Id": "quality",
"Value": 3,
"Range": 5
}
}
}
一些例子
E1 在Github issue上看到一个挺有意思的问题,说的是嵌套数组的问题,看数据是从ES查询出来的
输入数据如下,是一个嵌套数组,最外层root数组,里层hits数组,需求是想要把hits数组切分成一个个元素,比如示例中有两个hits数组,一共三个元素,最后结果数组里应该就有三个元素,详细请对照期望输出json
[
{
"_id": "W55553",
"inner_hits": {
"items": {
"hits": {
"hits": [
{
"_source": {
"status": 3200,
"item_no": 1
}
}
]
}
}
}
},
{
"_id": "W55555",
"inner_hits": {
"items": {
"hits": {
"hits": [
{
"_source": {
"status": 3500,
"item_no": 2
}
},
{
"_source": {
"status": 3200,
"item_no": 3
}
}
]
}
}
}
}
]
期望输出如下:
[
{
"id": "W55553",
"order": {
"customer_order": {
"sales_order": {
"sales_order_items": [
{
"status": 3200,
"item_no": 1
}
]
}
}
}
},
{
"id": "W55555",
"order": {
"customer_order": {
"sales_order": {
"sales_order_items": [
{
"status": 3500,
"item_no": 2
}
]
}
}
}
},
{
"id": "W55555",
"order": {
"customer_order": {
"sales_order": {
"sales_order_items": [
{
"status": 3200,
"item_no": 3
}
]
}
}
}
}
]
我给出的方案如下:
The problem of nested arrays is really funny. And I try to get around it.
First, I add a ID field for each '_source' element of 'hits' array.
Then, I add every '_source' that JOLT found to an new array -> '_source:[].order……'
Finally, exact the id field
总的策略是化嵌套数组为非嵌套问题
[
{
"operation": "modify-define-beta",
"spec": {
//第一步,循环处理每个_source,并为_source添加相应的id字段
"*": {
"inner_hits": {
"items": {
"hits": {
"hits": {
"*": {
"\_source": {
"id": "@(7,_id)"
}
}
}
}
}
}
}
}
},
{
"operation": "shift",
"spec": {
//第二步,把处理过程中遇到的每一个_source都输出到一个新的数组中
"*": {
"inner_hits": {
"items": {
"hits": {
"hits": {
"*": {
"_source": "[].order.customer_order.sales_order.sales_order_items[]"
}
}
}
}
}
}
}
},
{
"operation": "shift",
"spec": {
//第三步 提取ID字段,达成期望json格式
"*": {
"order": {
"customer_order": {
"sales_order": {
"sales_order_items": {
"0": {
"*": {
"@": "[#8].order.customer_order.sales_order.sales_order_items[0].&"
},
"id": "[#7].id"
}
}
}
}
}
}
}
}
]
E2
jolt github issue上帮忙解决的第二个问题Issue906,输入json如下
{
"params": {
"key1": "value2",
"key4": "value33",
"key_mult5": ["value_mult11","value_mult12"]
}
}
期望的输出如下,他想把输入json的params下的每一个元素的key和value都提到数组params里,但是,输入json的params每个元素的key有可能是数组,有可能不是,这就有些难度了。
{
"params": [
{
"key": "key1",
"value": "value2"
},
{
"key": "key4",
"value": "value33"
},
{
"key": "key_mult5",
"value": "value_mult11"
},
{
"key": "key_mult5",
"value": "value_mult12"
}
]
}
我提出的解决方案是
[
{
"operation": "cardinality",//先统一格式,把每一个元素都变成数组
"spec": {
"params": {
"*": "MANY"
}
}
}, {
"operation": "shift",//然后遍历每个元素数组下的value
"spec": {
"params": {
"*": {
"*": {
"@": "params[].&2"
}
}
}
}
},
{
"operation": "shift",//最后提取到key value
"spec": {
"params": {
"*": {
"*": {
"$": "params[#3].key",
"@": "params[#3].value"
}
}
}
}
}
]
- HDUOJ -----Color the ball
- poj-----Ultra-QuickSort(离散化+树状数组)
- HDUOJ---1241Oil Deposits(dfs)
- HDUOJ------2398Savings Account
- HDUOJ-----2399GPA
- HDUOJ----旋转的二进制
- HDUOJ---3743Frosh Week(BIT+离散化)
- Node.js新手必须知道的4个JavaScript概念
- HUDOJ-----1394Minimum Inversion Number
- 提升 Node.js 应用性能的 5 个技巧
- HDUOJ-----1166敌兵布阵
- HDUOJ------2492Ping pong
- HDUOJ----2489 Minimal Ratio Tree
- HDUOJ----2487Ugly Windows
- JavaScript 教程
- JavaScript 编辑工具
- JavaScript 与HTML
- JavaScript 与Java
- JavaScript 数据结构
- JavaScript 基本数据类型
- JavaScript 特殊数据类型
- JavaScript 运算符
- JavaScript typeof 运算符
- JavaScript 表达式
- JavaScript 类型转换
- JavaScript 基本语法
- JavaScript 注释
- Javascript 基本处理流程
- Javascript 选择结构
- Javascript if 语句
- Javascript if 语句的嵌套
- Javascript switch 语句
- Javascript 循环结构
- Javascript 循环结构实例
- Javascript 跳转语句
- Javascript 控制语句总结
- Javascript 函数介绍
- Javascript 函数的定义
- Javascript 函数调用
- Javascript 几种特殊的函数
- JavaScript 内置函数简介
- Javascript eval() 函数
- Javascript isFinite() 函数
- Javascript isNaN() 函数
- parseInt() 与 parseFloat()
- escape() 与 unescape()
- Javascript 字符串介绍
- Javascript length属性
- javascript 字符串函数
- Javascript 日期对象简介
- Javascript 日期对象用途
- Date 对象属性和方法
- Javascript 数组是什么
- Javascript 创建数组
- Javascript 数组赋值与取值
- Javascript 数组属性和方法
- 一起来学演化计算-matlab基本函数min
- 一起来学演化计算-matlab基本函数find
- 欧拉图和哈密顿图
- python 操作 txt 文件中数据教程[4]-python 去掉 txt 文件行尾换行
- java字符数组char[]和字符串String之间的转换
- python操作txt文件中数据教程[3]-python读取文件夹中所有txt文件并将数据转为csv文件
- python操作txt文件中数据教程[1]-使用python读写txt文件
- python循环删除列表元素常见错误与正确方法
- Python字符串,整型,浮点数相互转化
- python创建与遍历List二维列表
- 论文研读-基于变量分类的动态多目标优化算法
- matplotlib交互模式与pacharm单独Figure设置
- numpy二进制转换和范围缩放
- 使用数组作为索引遍历numpy数组
- enumerate 遍历numpy数组