Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Implemented Json to Tlv and Tlv to Json Converter in Kotlin #26458

Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion examples/java-matter-controller/BUILD.gn
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
# Copyright (c) 2022 Project CHIP Authors
# Copyright (c) 2022-2023 Project CHIP Authors
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
Expand Down Expand Up @@ -38,6 +38,7 @@ kotlin_binary("java-matter-controller") {
output_name = "java-matter-controller"
deps = [
":java",
"${chip_root}/src/controller/java:json_to_tlv_to_json_test",
"${chip_root}/src/controller/java:tlv_read_write_test",
"${chip_root}/src/controller/java:tlv_reader_test",
"${chip_root}/src/controller/java:tlv_writer_test",
Expand Down
35 changes: 34 additions & 1 deletion src/controller/java/BUILD.gn
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
# Copyright (c) 2020-2021 Project CHIP Authors
# Copyright (c) 2020-2023 Project CHIP Authors
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
Expand Down Expand Up @@ -204,6 +204,39 @@ kotlin_library("tlv_read_write_test") {
kotlinc_flags = [ "-Xlint:deprecation" ]
}

kotlin_library("json") {
output_name = "libCHIPJson.jar"

deps = [
":tlv",
"${chip_root}/third_party/java_deps:gson",
]

sources = [
"src/chip/json/JsonToTlv.kt",
"src/chip/json/TlvToJson.kt",
]

kotlinc_flags = [ "-Xlint:deprecation" ]
}

kotlin_library("json_to_tlv_to_json_test") {
output_name = "JsonToTlvToJsonTest.jar"

deps = [
":json",
":tlv",
"${chip_root}/third_party/java_deps:gson",
"${chip_root}/third_party/java_deps:junit-4",
"${chip_root}/third_party/java_deps:kotlin-test",
"${chip_root}/third_party/java_deps:truth",
]

sources = [ "tests/chip/json/JsonToTlvToJsonTest.kt" ]

kotlinc_flags = [ "-Xlint:deprecation" ]
}

android_library("java") {
output_name = "CHIPController.jar"

Expand Down
201 changes: 201 additions & 0 deletions src/controller/java/src/chip/json/JsonToTlv.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,201 @@
/*
*
* Copyright (c) 2023 Project CHIP Authors
* Copyright (c) 2023 Google LLC.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

package chip.json

import com.google.gson.JsonArray
import com.google.gson.JsonElement
import com.google.gson.JsonObject
import com.google.gson.JsonParser
import com.google.protobuf.ByteString
import java.util.Base64

/**
* Implements Matter JSON to TLV converter.
*
* Note that NOT all TLV configurations are supported by the current implementation. Below 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
*
* @param json string representing Json encoded data to be converted into TLV format
* @throws IllegalArgumentException if the data was invalid
*/
fun TlvWriter.fromJsonString(json: String): ByteArray {
validateIsJsonObjectAndConvert(JsonParser.parseString(json), AnonymousTag)
return validateTlv().getEncoded()
}

/**
* Converts Json Object into TLV Structure or TLV top level elements.
*
* @param json object to be converted to TLV.
* @throws IllegalArgumentException if the data was invalid
*/
private fun TlvWriter.fromJson(json: JsonObject): TlvWriter {
json.keySet().forEach { key ->
val (tag, type, subType) = extractTagAndTypeFromJsonKey(key)
fromJson(json.get(key), tag, type, subType)
}
return this
}

/**
* Converts Json Array into TLV Array.
*
* @param json object to be converted to TLV.
* @param type Type of array elements.
* @throws IllegalArgumentException if the data was invalid
*/
private fun TlvWriter.fromJson(json: JsonArray, type: String): TlvWriter {
json.iterator().forEach { element -> fromJson(element, AnonymousTag, type) }
return this
}

/**
* Converts Json Element into TLV Array.
*
* @param element element to be converted to TLV.
* @param tag element tag.
* @param type element type.
* @param subType array elements type. Only relevant when type is an Array. Should be empty string
* in all other cases.
* @throws IllegalArgumentException if the data was invalid
*/
private fun TlvWriter.fromJson(element: JsonElement, tag: Tag, type: String, subType: String = "") {
when (type) {
JSON_VALUE_TYPE_INT -> put(tag, validateIsNumber(element).toLong())
JSON_VALUE_TYPE_UINT -> put(tag, validateIsNumber(element).toLong().toULong())
JSON_VALUE_TYPE_BOOL -> put(tag, validateIsBoolean(element))
JSON_VALUE_TYPE_FLOAT -> put(tag, validateIsDouble(element).toFloat())
JSON_VALUE_TYPE_DOUBLE -> put(tag, validateIsDouble(element))
JSON_VALUE_TYPE_BYTES -> put(tag, validateIsString(element).base64Encode())
JSON_VALUE_TYPE_STRING -> put(tag, validateIsString(element))
JSON_VALUE_TYPE_NULL -> validateIsNullAndPut(element, tag)
JSON_VALUE_TYPE_STRUCT -> validateIsJsonObjectAndConvert(element, tag)
JSON_VALUE_TYPE_ARRAY -> {
if (subType.isEmpty()) {
throw IllegalArgumentException("Multi-Dimensional JSON Array is Invalid")
} else {
require(element.isJsonArray()) { "Expected Array; the actual element is: $element" }
startArray(tag).fromJson(element.getAsJsonArray(), subType).endArray()
}
}
JSON_VALUE_TYPE_EMPTY ->
throw IllegalArgumentException("Empty array was expected but there is value: $element}")
else -> throw IllegalArgumentException("Invalid type was specified: $type")
}
}

/**
* Extracts tag and type fields from Json key. Valid JSON key SHOULD have 1, 2, or 3 fields
* constracted as [name:][tag:]type[-subtype]
*
* @param key Json element key value.
* @throws IllegalArgumentException if the key format was invalid
*/
private fun extractTagAndTypeFromJsonKey(key: String): Triple<Tag, String, String> {
val keyFields = key.split(":")
var type = keyFields.last()
val typeFields = type.split("-")
var subType = ""

val tagNumber =
when (keyFields.size) {
2 -> keyFields.first().toUIntOrNull()
3 -> keyFields[1].toUIntOrNull()
else -> throw IllegalArgumentException("Invalid JSON key value: $key")
}

val tag =
when {
tagNumber == null -> throw IllegalArgumentException("Invalid JSON key value: $key")
tagNumber <= UByte.MAX_VALUE.toUInt() -> ContextSpecificTag(tagNumber.toInt())
tagNumber <= UShort.MAX_VALUE.toUInt() -> CommonProfileTag(2, tagNumber)
else -> CommonProfileTag(4, tagNumber)
}

// Valid type field of the JSON key SHOULD have type and optional subtype component
require(typeFields.size in (1..2)) { "Invalid JSON key value: $key" }

if (typeFields.size == 2) {
require(typeFields[0] == JSON_VALUE_TYPE_ARRAY) { "Invalid JSON key value: $key" }
type = JSON_VALUE_TYPE_ARRAY
subType = typeFields[1]
}

return Triple(tag, type, subType)
}

private fun String.base64Encode(): ByteString {
return ByteString.copyFrom(Base64.getDecoder().decode(this))
}

/** Verifies JsonElement is Number. If yes, returns the value. */
private fun validateIsNumber(element: JsonElement): Number {
require(
element.isJsonPrimitive() &&
(element.getAsJsonPrimitive().isNumber() || element.getAsJsonPrimitive().isString())
) {
"Expected Integer represented as a Number or as a String; the actual element is: $element"
}
return element.getAsJsonPrimitive().getAsNumber()
}

/** Verifies JsonElement is Boolean. If yes, returns the value. */
private fun validateIsBoolean(element: JsonElement): Boolean {
require(element.isJsonPrimitive() && element.getAsJsonPrimitive().isBoolean()) {
"Expected Boolean; the actual element is: $element"
}
return element.getAsJsonPrimitive().getAsBoolean()
}

/** Verifies JsonElement is Double. If yes, returns the value. */
private fun validateIsDouble(element: JsonElement): Double {
require(element.isJsonPrimitive() && element.getAsJsonPrimitive().isNumber()) {
"Expected Double; the actual element is: $element"
}
return element.getAsJsonPrimitive().getAsDouble()
}

/** Verifies JsonElement is String. If yes, returns the value. */
private fun validateIsString(element: JsonElement): String {
require(element.isJsonPrimitive() && element.getAsJsonPrimitive().isString()) {
"Expected String; the actual element is: $element"
}
return element.getAsJsonPrimitive().getAsString()
}

/** Verifies JsonElement is Null. If yes, puts it into TLV. */
private fun TlvWriter.validateIsNullAndPut(element: JsonElement, tag: Tag) {
require(element.isJsonNull()) { "Expected Null; the actual element is: $element" }
putNull(tag)
}

/** Verifies JsonElement is JsonObject. If yes, converts it into TLV Structure. */
private fun TlvWriter.validateIsJsonObjectAndConvert(element: JsonElement, tag: Tag) {
require(element.isJsonObject()) { "Expected JsonObject; the actual element is: $element" }
startStructure(tag).fromJson(element.getAsJsonObject()).endStructure()
}
Loading