Skip to content

Commit c216384

Browse files
authored
Implemented Json to Tlv and Tlv to Json Converter in Kotlin (#26527)
Note that NOT all TLV configurations are supported by the current implementation. Here is the list of limitations: - TLV Lists are not supported - Multi-Dimensional TLV Arrays are not supported - All elements of an array MUST be of the same type - The top level TLV element MUST be a single structure with AnonymousTag - The following tags are supported: - AnonymousTag are used only with TLV Arrays elements or a top-level structure - ContextSpecificTag are used only with TLV Structure elements - CommonProfileTag are used only with TLV Structure elements - Infinity Float/Double values are not supported
1 parent f0d4f2d commit c216384

File tree

9 files changed

+2119
-6
lines changed

9 files changed

+2119
-6
lines changed

examples/java-matter-controller/BUILD.gn

+2-1
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
# Copyright (c) 2022 Project CHIP Authors
1+
# Copyright (c) 2022-2023 Project CHIP Authors
22
#
33
# Licensed under the Apache License, Version 2.0 (the "License");
44
# you may not use this file except in compliance with the License.
@@ -38,6 +38,7 @@ kotlin_binary("java-matter-controller") {
3838
output_name = "java-matter-controller"
3939
deps = [
4040
":java",
41+
"${chip_root}/src/controller/java:json_to_tlv_to_json_test",
4142
"${chip_root}/src/controller/java:tlv_read_write_test",
4243
"${chip_root}/src/controller/java:tlv_reader_test",
4344
"${chip_root}/src/controller/java:tlv_writer_test",

src/controller/java/BUILD.gn

+36-1
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
# Copyright (c) 2020-2021 Project CHIP Authors
1+
# Copyright (c) 2020-2023 Project CHIP Authors
22
#
33
# Licensed under the Apache License, Version 2.0 (the "License");
44
# you may not use this file except in compliance with the License.
@@ -204,6 +204,41 @@ kotlin_library("tlv_read_write_test") {
204204
kotlinc_flags = [ "-Xlint:deprecation" ]
205205
}
206206

207+
kotlin_library("jsontlv") {
208+
output_name = "libCHIPJson.jar"
209+
210+
deps = [
211+
":tlv",
212+
"${chip_root}/third_party/java_deps:gson",
213+
"${chip_root}/third_party/java_deps:protobuf-java",
214+
]
215+
216+
sources = [
217+
"src/chip/jsontlv/JsonToTlv.kt",
218+
"src/chip/jsontlv/TlvToJson.kt",
219+
"src/chip/jsontlv/types.kt",
220+
]
221+
222+
kotlinc_flags = [ "-Xlint:deprecation" ]
223+
}
224+
225+
kotlin_library("json_to_tlv_to_json_test") {
226+
output_name = "JsonToTlvToJsonTest.jar"
227+
228+
deps = [
229+
":jsontlv",
230+
":tlv",
231+
"${chip_root}/third_party/java_deps:gson",
232+
"${chip_root}/third_party/java_deps:junit-4",
233+
"${chip_root}/third_party/java_deps:kotlin-test",
234+
"${chip_root}/third_party/java_deps:truth",
235+
]
236+
237+
sources = [ "tests/chip/jsontlv/JsonToTlvToJsonTest.kt" ]
238+
239+
kotlinc_flags = [ "-Xlint:deprecation" ]
240+
}
241+
207242
android_library("java") {
208243
output_name = "CHIPController.jar"
209244

Original file line numberDiff line numberDiff line change
@@ -0,0 +1,202 @@
1+
/*
2+
*
3+
* Copyright (c) 2023 Project CHIP Authors
4+
* Copyright (c) 2023 Google LLC.
5+
*
6+
* Licensed under the Apache License, Version 2.0 (the "License");
7+
* you may not use this file except in compliance with the License.
8+
* You may obtain a copy of the License at
9+
*
10+
* http://www.apache.org/licenses/LICENSE-2.0
11+
*
12+
* Unless required by applicable law or agreed to in writing, software
13+
* distributed under the License is distributed on an "AS IS" BASIS,
14+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
15+
* See the License for the specific language governing permissions and
16+
* limitations under the License.
17+
*/
18+
19+
package chip.jsontlv
20+
21+
import com.google.gson.JsonArray
22+
import com.google.gson.JsonElement
23+
import com.google.gson.JsonObject
24+
import com.google.gson.JsonParser
25+
import com.google.protobuf.ByteString
26+
import java.util.Base64
27+
import chip.tlv.*
28+
29+
/**
30+
* Implements Matter JSON to TLV converter.
31+
*
32+
* Note that NOT all TLV configurations are supported by the current implementation. Below is the
33+
* list of limitations:
34+
* - TLV Lists are not supported
35+
* - Multi-Dimensional TLV Arrays are not supported
36+
* - All elements of an array MUST be of the same type
37+
* - The top level TLV element MUST be a single structure with AnonymousTag
38+
* - The following tags are supported:
39+
* - AnonymousTag are used only with TLV Arrays elements or a top-level structure
40+
* - ContextSpecificTag are used only with TLV Structure elements
41+
* - CommonProfileTag are used only with TLV Structure elements
42+
* - Infinity Float/Double values are not supported
43+
*
44+
* @param json string representing Json encoded data to be converted into TLV format
45+
* @throws IllegalArgumentException if the data was invalid
46+
*/
47+
fun TlvWriter.fromJsonString(json: String): ByteArray {
48+
validateIsJsonObjectAndConvert(JsonParser.parseString(json), AnonymousTag)
49+
return validateTlv().getEncoded()
50+
}
51+
52+
/**
53+
* Converts Json Object into TLV Structure or TLV top level elements.
54+
*
55+
* @param json object to be converted to TLV.
56+
* @throws IllegalArgumentException if the data was invalid
57+
*/
58+
private fun TlvWriter.fromJson(json: JsonObject): TlvWriter {
59+
json.keySet().forEach { key ->
60+
val (tag, type, subType) = extractTagAndTypeFromJsonKey(key)
61+
fromJson(json.get(key), tag, type, subType)
62+
}
63+
return this
64+
}
65+
66+
/**
67+
* Converts Json Array into TLV Array.
68+
*
69+
* @param json object to be converted to TLV.
70+
* @param type Type of array elements.
71+
* @throws IllegalArgumentException if the data was invalid
72+
*/
73+
private fun TlvWriter.fromJson(json: JsonArray, type: String): TlvWriter {
74+
json.iterator().forEach { element -> fromJson(element, AnonymousTag, type) }
75+
return this
76+
}
77+
78+
/**
79+
* Converts Json Element into TLV Array.
80+
*
81+
* @param element element to be converted to TLV.
82+
* @param tag element tag.
83+
* @param type element type.
84+
* @param subType array elements type. Only relevant when type is an Array. Should be empty string
85+
* in all other cases.
86+
* @throws IllegalArgumentException if the data was invalid
87+
*/
88+
private fun TlvWriter.fromJson(element: JsonElement, tag: Tag, type: String, subType: String = "") {
89+
when (type) {
90+
JSON_VALUE_TYPE_INT -> put(tag, validateIsNumber(element).toLong())
91+
JSON_VALUE_TYPE_UINT -> put(tag, validateIsNumber(element).toLong().toULong())
92+
JSON_VALUE_TYPE_BOOL -> put(tag, validateIsBoolean(element))
93+
JSON_VALUE_TYPE_FLOAT -> put(tag, validateIsDouble(element).toFloat())
94+
JSON_VALUE_TYPE_DOUBLE -> put(tag, validateIsDouble(element))
95+
JSON_VALUE_TYPE_BYTES -> put(tag, validateIsString(element).base64Encode())
96+
JSON_VALUE_TYPE_STRING -> put(tag, validateIsString(element))
97+
JSON_VALUE_TYPE_NULL -> validateIsNullAndPut(element, tag)
98+
JSON_VALUE_TYPE_STRUCT -> validateIsJsonObjectAndConvert(element, tag)
99+
JSON_VALUE_TYPE_ARRAY -> {
100+
if (subType.isEmpty()) {
101+
throw IllegalArgumentException("Multi-Dimensional JSON Array is Invalid")
102+
} else {
103+
require(element.isJsonArray()) { "Expected Array; the actual element is: $element" }
104+
startArray(tag).fromJson(element.getAsJsonArray(), subType).endArray()
105+
}
106+
}
107+
JSON_VALUE_TYPE_EMPTY ->
108+
throw IllegalArgumentException("Empty array was expected but there is value: $element}")
109+
else -> throw IllegalArgumentException("Invalid type was specified: $type")
110+
}
111+
}
112+
113+
/**
114+
* Extracts tag and type fields from Json key. Valid JSON key SHOULD have 1, 2, or 3 fields
115+
* constracted as [name:][tag:]type[-subtype]
116+
*
117+
* @param key Json element key value.
118+
* @throws IllegalArgumentException if the key format was invalid
119+
*/
120+
private fun extractTagAndTypeFromJsonKey(key: String): Triple<Tag, String, String> {
121+
val keyFields = key.split(":")
122+
var type = keyFields.last()
123+
val typeFields = type.split("-")
124+
var subType = ""
125+
126+
val tagNumber =
127+
when (keyFields.size) {
128+
2 -> keyFields.first().toUIntOrNull()
129+
3 -> keyFields[1].toUIntOrNull()
130+
else -> throw IllegalArgumentException("Invalid JSON key value: $key")
131+
}
132+
133+
val tag =
134+
when {
135+
tagNumber == null -> throw IllegalArgumentException("Invalid JSON key value: $key")
136+
tagNumber <= UByte.MAX_VALUE.toUInt() -> ContextSpecificTag(tagNumber.toInt())
137+
tagNumber <= UShort.MAX_VALUE.toUInt() -> CommonProfileTag(2, tagNumber)
138+
else -> CommonProfileTag(4, tagNumber)
139+
}
140+
141+
// Valid type field of the JSON key SHOULD have type and optional subtype component
142+
require(typeFields.size in (1..2)) { "Invalid JSON key value: $key" }
143+
144+
if (typeFields.size == 2) {
145+
require(typeFields[0] == JSON_VALUE_TYPE_ARRAY) { "Invalid JSON key value: $key" }
146+
type = JSON_VALUE_TYPE_ARRAY
147+
subType = typeFields[1]
148+
}
149+
150+
return Triple(tag, type, subType)
151+
}
152+
153+
private fun String.base64Encode(): ByteString {
154+
return ByteString.copyFrom(Base64.getDecoder().decode(this))
155+
}
156+
157+
/** Verifies JsonElement is Number. If yes, returns the value. */
158+
private fun validateIsNumber(element: JsonElement): Number {
159+
require(
160+
element.isJsonPrimitive() &&
161+
(element.getAsJsonPrimitive().isNumber() || element.getAsJsonPrimitive().isString())
162+
) {
163+
"Expected Integer represented as a Number or as a String; the actual element is: $element"
164+
}
165+
return element.getAsJsonPrimitive().getAsNumber()
166+
}
167+
168+
/** Verifies JsonElement is Boolean. If yes, returns the value. */
169+
private fun validateIsBoolean(element: JsonElement): Boolean {
170+
require(element.isJsonPrimitive() && element.getAsJsonPrimitive().isBoolean()) {
171+
"Expected Boolean; the actual element is: $element"
172+
}
173+
return element.getAsJsonPrimitive().getAsBoolean()
174+
}
175+
176+
/** Verifies JsonElement is Double. If yes, returns the value. */
177+
private fun validateIsDouble(element: JsonElement): Double {
178+
require(element.isJsonPrimitive() && element.getAsJsonPrimitive().isNumber()) {
179+
"Expected Double; the actual element is: $element"
180+
}
181+
return element.getAsJsonPrimitive().getAsDouble()
182+
}
183+
184+
/** Verifies JsonElement is String. If yes, returns the value. */
185+
private fun validateIsString(element: JsonElement): String {
186+
require(element.isJsonPrimitive() && element.getAsJsonPrimitive().isString()) {
187+
"Expected String; the actual element is: $element"
188+
}
189+
return element.getAsJsonPrimitive().getAsString()
190+
}
191+
192+
/** Verifies JsonElement is Null. If yes, puts it into TLV. */
193+
private fun TlvWriter.validateIsNullAndPut(element: JsonElement, tag: Tag) {
194+
require(element.isJsonNull()) { "Expected Null; the actual element is: $element" }
195+
putNull(tag)
196+
}
197+
198+
/** Verifies JsonElement is JsonObject. If yes, converts it into TLV Structure. */
199+
private fun TlvWriter.validateIsJsonObjectAndConvert(element: JsonElement, tag: Tag) {
200+
require(element.isJsonObject()) { "Expected JsonObject; the actual element is: $element" }
201+
startStructure(tag).fromJson(element.getAsJsonObject()).endStructure()
202+
}

0 commit comments

Comments
 (0)