Skip to content

Commit

Permalink
#466 Convert JSON object to CBORMetadataMap and JSON Array to CBORMet…
Browse files Browse the repository at this point in the history
…adataList / New methods in MetadataBuilder (#467)

* #466 Make json object --> CBOR Map and json array --> CBOR Array methods public and handle null value

* #466 fixed tests

* #466 Handle BigInteger encoded as ByteString

* Add unit tests and improve metadata JSON handling

Added several new unit tests for MetadataBuilder and improved the handling of byte strings in the metadata converter. Also included multiple new test JSON files to cover different metadata scenarios.

* To fix Sonarcloud warnings
  • Loading branch information
satran004 authored Nov 19, 2024
1 parent c3d865f commit 436b396
Show file tree
Hide file tree
Showing 12 changed files with 648 additions and 21 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -3,14 +3,47 @@
import com.bloxbean.cardano.client.metadata.cbor.CBORMetadata;
import com.bloxbean.cardano.client.metadata.cbor.CBORMetadataList;
import com.bloxbean.cardano.client.metadata.cbor.CBORMetadataMap;
import com.bloxbean.cardano.client.metadata.helper.JsonNoSchemaToMetadataConverter;
import com.bloxbean.cardano.client.metadata.helper.MetadataToJsonNoSchemaConverter;
import com.bloxbean.cardano.client.util.JsonUtil;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.*;
import com.fasterxml.jackson.databind.node.ArrayNode;
import com.fasterxml.jackson.databind.node.ObjectNode;

import java.math.BigInteger;

/**
* Builder class to create Metadata, MetadataMap and MetadataList
* The {@code MetadataBuilder} class provides a collection of static methods for creating
* and manipulating metadata objects in various formats such as CBOR and JSON.
* It offers functionality to create, deserialize, and convert metadata
* between JSON and CBOR representations.
*
* <p> This class serves as a utility for handling metadata operations, abstracting
* the complexity of different metadata types and conversion processes behind
* simple method calls. It includes methods for:
* <ul>
* <li>Creating different types of metadata objects like {@code Metadata}, {@code MetadataMap}, {@code MetadataList}.</li>
* <li>Deserialization of CBOR byte arrays to Metadata objects.</li>
* <li>Conversion of JSON strings to Metadata and vice-versa.</li>
* </ul>
*
* <p> Example usage:
* <pre>
* {@code
* Metadata metadata = MetadataBuilder.createMetadata();
* MetadataMap metadataMap = MetadataBuilder.createMap();
* MetadataList metadataList = MetadataBuilder.createList();
* Metadata fromJson = MetadataBuilder.metadataFromJson(jsonString);
* String jsonString = MetadataBuilder.toJson(metadata);
* }
* </pre>
*/
public class MetadataBuilder {

/**
* Create Metadata object
*
* @return Metadata
*/
public static Metadata createMetadata() {
Expand All @@ -19,6 +52,7 @@ public static Metadata createMetadata() {

/**
* Create MetadataMap object
*
* @return MetadataMap
*/
public static MetadataMap createMap() {
Expand All @@ -27,9 +61,122 @@ public static MetadataMap createMap() {

/**
* Create MetadataList object
*
* @return MetadataList
*/
public static MetadataList createList() {
return new CBORMetadataList();
}

/**
* Deserialize cbor bytes to Metadata object
*
* @param cborBytes
* @return Metadata
*/
public static Metadata deserialize(byte[] cborBytes) {
return CBORMetadata.deserialize(cborBytes);
}

/**
* Converts a JSON string to a Metadata object.
*
* @param json the JSON string to be converted to a Metadata object
* @return a Metadata object generated from the provided JSON string
* @throws IllegalArgumentException if the JSON format is invalid
*/
public static Metadata metadataFromJson(String json) {
try {
return JsonNoSchemaToMetadataConverter.jsonToCborMetadata(json);
} catch (Exception e) {
throw new IllegalArgumentException("Invalid json format for metadta", e);
}
}

/**
* Creates a Metadata object from a JSON string.
*
* @param label a BigInteger representing the label associated with the metadata
* @param jsonBody a JSON string representing the metadata content
* @return a Metadata object containing the parsed data
* @throws IllegalArgumentException if the JSON format is invalid or not an object/array
*/
public static Metadata metadataFromJsonBody(BigInteger label, String jsonBody) {
JsonNode jsonNode;
try {
jsonNode = JsonUtil.parseJson(jsonBody);
} catch (JsonProcessingException e) {
throw new IllegalArgumentException("Invalid json format");
}

if (jsonNode instanceof ObjectNode) {
var metadataMap = JsonNoSchemaToMetadataConverter.parseObjectNode((ObjectNode) jsonNode);
return new CBORMetadata().put(label, metadataMap);
} else if (jsonNode.isArray()) {
var metadataList = JsonNoSchemaToMetadataConverter.parseArrayNode((ArrayNode) jsonNode);
return new CBORMetadata().put(label, metadataList);
} else {
throw new IllegalArgumentException("Invalid json format. Should be an object or an array");
}
}

/**
* Parses a JSON string to create a MetadataMap instance.
*
* @param jsonBody the JSON string to be converted to a MetadataMap object
* @return a MetadataMap object generated from the provided JSON object
* @throws IllegalArgumentException if the JSON format is invalid
*/
public static MetadataMap metadataMapFromJsonBody(String jsonBody) {
try {
return JsonNoSchemaToMetadataConverter.parseObjectNode((ObjectNode) JsonUtil.parseJson(jsonBody));
} catch (JsonProcessingException e) {
throw new IllegalArgumentException("Could not parse json body to MetadataMap", e);
}
}

/**
* Converts a JSON string into a MetadataList instance.
*
* @param jsonBody the JSON string to be converted to a MetadataList object
* @return a MetadataList object generated from the provided JSON array
* @throws IllegalArgumentException if the JSON format is invalid or cannot be parsed
*/
public static MetadataList metadataListFromJsonBody(String jsonBody) {
try {
return JsonNoSchemaToMetadataConverter.parseArrayNode((ArrayNode) JsonUtil.parseJson(jsonBody));
} catch (JsonProcessingException e) {
throw new IllegalArgumentException("Could not parse json body to MetadataMap", e);
}
}

/**
* Converts a Metadata object to its JSON representation.
*
* @param metadata the Metadata object to be converted
* @return a JsonNode representing the JSON equivalent of the given Metadata
* @throws IllegalArgumentException if the serialization results in an invalid JSON format
*/
public static String toJson(Metadata metadata) {
try {
return MetadataToJsonNoSchemaConverter.cborBytesToJson(metadata.serialize());
} catch (Exception e) {
throw new IllegalArgumentException("Invalid json format");
}
}

/**
* Converts the provided CBOR byte array to a JSON string.
*
* @param serializedCbor A byte array serialized CBOR data to be converted to JSON.
* @return A string containing the JSON representation of the given CBOR data.
* @throws IllegalArgumentException if the CBOR data cannot be converted to a valid JSON format.
*/
public static String toJson(byte[] serializedCbor) {
try {
return MetadataToJsonNoSchemaConverter.cborBytesToJson(serializedCbor);
} catch (Exception e) {
throw new IllegalArgumentException("Invalid json format");
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,9 @@

@Slf4j
public class MetadataHelper {
private static final int BIG_UINT_TAG = 2;
private static final int BIG_NINT_TAG = 3;
private static final BigInteger MINUS_ONE = BigInteger.valueOf(-1);

public static Object extractActualValue(DataItem dataItem) {
if (dataItem instanceof UnicodeString) {
Expand All @@ -17,7 +20,7 @@ public static Object extractActualValue(DataItem dataItem) {
} else if (dataItem instanceof NegativeInteger) {
return ((NegativeInteger) dataItem).getValue();
} else if (dataItem instanceof ByteString) {
return ((ByteString) dataItem).getBytes();
return parseByteString((ByteString) dataItem);
} else if (dataItem instanceof Map) {
return new CBORMetadataMap((Map) dataItem);
} else if (dataItem instanceof Array) {
Expand Down Expand Up @@ -58,4 +61,16 @@ public static int checkLength(String str) {
return str.getBytes(StandardCharsets.UTF_8).length;
}

public static Object parseByteString(ByteString valueItem) {
var tag = valueItem.getTag();
if (tag != null){
if (tag.getValue() == BIG_UINT_TAG) {
return new BigInteger(1, valueItem.getBytes());
} else if (tag.getValue() == BIG_NINT_TAG) {
return MINUS_ONE.subtract(new BigInteger(1, valueItem.getBytes()));
}
}

return valueItem.getBytes();
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -10,10 +10,7 @@
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.node.ArrayNode;
import com.fasterxml.jackson.databind.node.NumericNode;
import com.fasterxml.jackson.databind.node.ObjectNode;
import com.fasterxml.jackson.databind.node.TextNode;
import com.fasterxml.jackson.databind.node.*;

import java.math.BigInteger;
import java.util.Iterator;
Expand Down Expand Up @@ -67,7 +64,7 @@ private static CBORMetadata parseTopJsonObject(ObjectNode objectNode) {
return metadata;
}

private static CBORMetadataList parseArrayNode(ArrayNode value) {
public static CBORMetadataList parseArrayNode(ArrayNode value) {
CBORMetadataList metadataList = new CBORMetadataList();
Iterator<JsonNode> fields = value.elements();

Expand Down Expand Up @@ -95,7 +92,7 @@ private static CBORMetadataList parseArrayNode(ArrayNode value) {
return metadataList;
}

private static CBORMetadataMap parseObjectNode(ObjectNode jsonObj) {
public static CBORMetadataMap parseObjectNode(ObjectNode jsonObj) {
CBORMetadataMap metadataMap = new CBORMetadataMap();
Iterator<String> fields = jsonObj.fieldNames();

Expand All @@ -104,7 +101,9 @@ private static CBORMetadataMap parseObjectNode(ObjectNode jsonObj) {
JsonNode nd = jsonObj.get(field);

Object cborValue = processValueNode(nd);
if (cborValue instanceof CBORMetadataMap) {
if (cborValue == null) {
metadataMap.put(field, (String)null);
} else if (cborValue instanceof CBORMetadataMap) {
metadataMap.put(field, (CBORMetadataMap) cborValue);
} else if (cborValue instanceof CBORMetadataList) {
metadataMap.put(field, (CBORMetadataList) cborValue);
Expand Down Expand Up @@ -148,6 +147,8 @@ private static Object processValueNode(JsonNode value) {
} else if(value instanceof NumericNode) {
BigInteger valueInt = ((NumericNode)value).bigIntegerValue();
return valueInt;
} else if (value instanceof NullNode) {
return null;
} else {
throw new JsonMetadaException("Invalid value or value not recognized : " + value);
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,15 +3,14 @@
import co.nstant.in.cbor.CborDecoder;
import co.nstant.in.cbor.CborException;
import co.nstant.in.cbor.model.*;
import co.nstant.in.cbor.model.Map;
import com.bloxbean.cardano.client.exception.CborDeserializationException;
import com.bloxbean.cardano.client.metadata.cbor.MetadataHelper;
import com.bloxbean.cardano.client.metadata.exception.MetadataDeSerializationException;
import com.bloxbean.cardano.client.util.HexUtil;
import com.fasterxml.jackson.databind.ObjectMapper;

import java.util.ArrayList;
import java.util.Collection;
import java.util.HashMap;
import java.util.List;
import java.util.*;

import static co.nstant.in.cbor.model.MajorType.*;

Expand Down Expand Up @@ -58,7 +57,7 @@ private static java.util.Map cborHexToJavaMap(String hex) throws CborDeserializa
if(dataItemList != null && dataItemList.size() > 1)
throw new MetadataDeSerializationException("Multiple DataItems found at top level. Should be zero : " + dataItemList.size());

java.util.Map result = new HashMap();
java.util.Map result;
DataItem dataItem = dataItemList.get(0);
if(dataItem instanceof Map) {
result = processMap((Map)dataItem);
Expand All @@ -69,7 +68,7 @@ private static java.util.Map cborHexToJavaMap(String hex) throws CborDeserializa
}

private static java.util.Map processMap(Map map) {
java.util.Map resultMap = new HashMap();
java.util.Map resultMap = new LinkedHashMap();
Collection<DataItem> keys = map.getKeys();
for(DataItem keyItem: keys) {
DataItem valueItem = map.get(keyItem);
Expand All @@ -87,8 +86,7 @@ private static Object processKey(DataItem keyItem) {
} else if(NEGATIVE_INTEGER.equals(keyItem.getMajorType())) {
return ((NegativeInteger) keyItem).getValue();
} else if (BYTE_STRING.equals(keyItem.getMajorType())) {
byte[] bytes = ((ByteString) keyItem).getBytes();
return "0x" + HexUtil.encodeHexString(bytes);
return byteStringToObject((ByteString) keyItem);
} else if (UNICODE_STRING.equals(keyItem.getMajorType())) {
return ((UnicodeString) keyItem).getString();
} else {
Expand All @@ -97,13 +95,14 @@ private static Object processKey(DataItem keyItem) {
}

private static Object processValue(DataItem valueItem) {
if(UNSIGNED_INTEGER.equals(valueItem.getMajorType())){
if (valueItem == SimpleValue.NULL) {
return null;
} else if(UNSIGNED_INTEGER.equals(valueItem.getMajorType())){
return ((UnsignedInteger)valueItem).getValue();
} else if(NEGATIVE_INTEGER.equals(valueItem.getMajorType())) {
return ((NegativeInteger)valueItem).getValue();
} else if(BYTE_STRING.equals(valueItem.getMajorType())) {
byte[] bytes = ((ByteString)valueItem).getBytes();
return "0x" + HexUtil.encodeHexString(bytes);
return byteStringToObject((ByteString) valueItem);
} else if(UNICODE_STRING.equals(valueItem.getMajorType())) {
return ((UnicodeString)valueItem).getString();
} else if(MAP.equals(valueItem.getMajorType())){
Expand All @@ -115,6 +114,16 @@ private static Object processValue(DataItem valueItem) {
}
}

private static Object byteStringToObject(ByteString valueItem) {
var extractedValue = MetadataHelper.parseByteString(valueItem);
if (extractedValue instanceof byte[]) {
byte[] bytes = (byte[]) extractedValue;
return "0x" + HexUtil.encodeHexString(bytes);
} else {
return extractedValue;
}
}

private static Object processArray(Array array) {
List<DataItem> dataItems = array.getDataItems();
List resultList = new ArrayList();
Expand Down
Loading

0 comments on commit 436b396

Please sign in to comment.