Skip to content

Commit

Permalink
ADT: Bug fix: serialization issue with strings which starts with a nu…
Browse files Browse the repository at this point in the history
…meric value. (#21623)
  • Loading branch information
azabbasi authored May 18, 2021
1 parent 11b597c commit ac81982
Show file tree
Hide file tree
Showing 25 changed files with 1,425 additions and 789 deletions.
29 changes: 16 additions & 13 deletions sdk/digitaltwins/azure-digitaltwins-core/CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,37 +1,39 @@
# Release History

## 1.1.0 (2021-05-13)
## 1.1.0 (2021-05-17)

### Fixes and improvements

- Fixed a bug where string tokens that start with a numeric values would transform into number type after payload serialization.
- Upgraded `reactor-core` dependency from `3.3.12.RELEASE` to `3.4.3`
- Upgraded `jackson-annotations` dependency from `2.12.1` to `2.12.2`
- Upgraded `azure-core` dependency from `1.13.0` to `1.16.0`
- Upgraded `jackson-annotations` dependency from `2.12.1` to `2.12.2`
- Upgraded `azure-core` dependency from `1.13.0` to `1.16.0`
- [azure-core changelog](https://github.com/Azure/azure-sdk-for-java/blob/master/sdk/core/azure-core/CHANGELOG.md#1160-2021-05-07)
- Upgraded `azure-core-serializer-json-jackson` dependency from `1.1.2` to `1.2.3`
- Upgraded `azure-core-serializer-json-jackson` dependency from `1.1.2` to `1.2.3`
- [azure-core-serializer-json-jackson changelog](https://github.com/Azure/azure-sdk-for-java/blob/master/sdk/core/azure-core-serializer-json-jackson/CHANGELOG.md#123-2021-05-07)


## 1.0.3 (2021-02-24)

### Dependency Updates

- Upgraded `jackson-annotations` dependency from `2.11.3` to `2.12.1`
- Upgraded `azure-identity` dependency from `1.2.2` to `1.2.3`
- Upgraded `jackson-annotations` dependency from `2.11.3` to `2.12.1`
- Upgraded `azure-identity` dependency from `1.2.2` to `1.2.3`
- [azure-identity changelog](https://github.com/Azure/azure-sdk-for-java/blob/master/sdk/identity/azure-identity/CHANGELOG.md#123-2021-02-09)
- Upgraded `azure-core` dependency from `1.12.0` to `1.13.0`
- Upgraded `azure-core` dependency from `1.12.0` to `1.13.0`
- [azure-core changelog](https://github.com/Azure/azure-sdk-for-java/blob/master/sdk/core/azure-core/CHANGELOG.md#1130-2021-02-05)
- Upgraded `azure-core-http-netty` dependency from `1.7.1` to `1.8.0`
- Upgraded `azure-core-http-netty` dependency from `1.7.1` to `1.8.0`
- [azure-core-http-netty changelog](https://github.com/Azure/azure-sdk-for-java/blob/master/sdk/core/azure-core-http-netty/CHANGELOG.md#180-2021-02-05)
- Upgraded `azure-core-serializer-json-jackson` dependency from `1.1.1` to `1.1.2`
- Upgraded `azure-core-serializer-json-jackson` dependency from `1.1.1` to `1.1.2`
- [azure-core-serializer-json-jackson changelog](https://github.com/Azure/azure-sdk-for-java/blob/master/sdk/core/azure-core-serializer-json-jackson/CHANGELOG.md#112-2021-02-05)


## 1.0.2 (2021-01-21)

### Dependency Updates

- Added diagnostic contexts to async APIs including service namespace.
- Upgraded `azure-core` dependency from `1.11.0` to `1.12.0`
- Upgraded `azure-core` dependency from `1.11.0` to `1.12.0`
- [azure-core changelog](https://github.com/Azure/azure-sdk-for-java/blob/master/sdk/core/azure-core/CHANGELOG.md#1120-2021-01-11)
- Upgraded `azure-identity` dependency from `1.2.1` to `1.2.2`
- Upgraded `azure-identity` dependency from `1.2.1` to `1.2.2`
- [azure-identity changelog](https://github.com/Azure/azure-sdk-for-java/blob/master/sdk/identity/azure-identity/CHANGELOG.md#122-2021-01-12)
- Upgraded `azure-core-http-netty` dependency from `1.7.0` to `1.7.1`
- [azure-core-http-netty changelog](https://github.com/Azure/azure-sdk-for-java/blob/master/sdk/core/azure-core-http-netty/CHANGELOG.md#171-2021-01-11)
Expand Down Expand Up @@ -72,6 +74,7 @@ Note that these breaking changes are only breaking changes from the preview vers
- Renamed CreateModels API parameter `models` to `dtdtlModels` for clarity.

### Fixes and improvements

- Fixed bug where `CreateDigitalTwin` and `CreateRelationship` APIs always sent ifNoneMatch header with value "*" making it impossible to replace an existing entity.

## 1.0.0-beta.3 (2020-10-01)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
package com.azure.digitaltwins.core.implementation.serializer;

import com.fasterxml.jackson.core.JsonGenerator;
import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.SerializerProvider;
import com.fasterxml.jackson.databind.ser.std.StdSerializer;
Expand All @@ -23,18 +24,30 @@ public DigitalTwinsStringSerializer(Class<String> t, ObjectMapper mapper) {
}

@Override
public void serialize(String s, JsonGenerator jsonGenerator, SerializerProvider serializerProvider) throws IOException {
if (isValidJson(s)) {
jsonGenerator.writeRawValue(s);
public void serialize(String stringToken, JsonGenerator jsonGenerator, SerializerProvider serializerProvider) throws IOException {
if (shouldWriteRawValue(stringToken)) {
jsonGenerator.writeRawValue(stringToken);
} else {
jsonGenerator.writeString(s);
jsonGenerator.writeString(stringToken);
}
}

private boolean isValidJson(String jsonInString ) {
/**
* Decides whether or not a string token should be written as a raw value.
* For example: a string representation of a json payload should be written as raw value as it's the json part we are interested in.
* It's important to note that only string tokens will end up in the string serializer.
* If the token is of a non-string primitive type, it should be written as a string and not as that data type.
* take "1234" or "false" as examples, they are both valid json nodes of types Number and Boolean but the token
* is not intended to be intercepted as primitive types (since it's a string token). The only types we like to treat as
* json payloads are actual json objects (for when String is chosen as the generic type for APIs) or the token itself is an escaped
* json string node.
* @param stringToken The string token to evaluate.
* @return True if the string token should be treated as a json node and not a string representation.
*/
private boolean shouldWriteRawValue(String stringToken ) {
try {
mapper.readTree(jsonInString);
return true;
JsonNode node = mapper.readTree(stringToken);
return node.isContainerNode() || node.isTextual();
} catch (IOException e) {
return false;
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
// Copyright (c) Microsoft Corporation. All rights reserved.
// Licensed under the MIT License.

package com.azure.digitaltwins.core;

import com.azure.digitaltwins.core.implementation.serializer.DigitalTwinsStringSerializer;
import com.fasterxml.jackson.core.JsonFactory;
import com.fasterxml.jackson.core.JsonGenerator;
import com.fasterxml.jackson.databind.ObjectMapper;
import org.junit.jupiter.api.Assertions;
import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.CsvSource;

import java.io.IOException;
import java.io.StringWriter;

public class StringSerializerUnitTests {

private static DigitalTwinsStringSerializer serializer = new DigitalTwinsStringSerializer(String.class, new ObjectMapper());

@ParameterizedTest
@CsvSource(value = {
"1234 | \"1234\"",
"false | \"false\"",
"true | \"true\"",
"1234 room | \"1234 room\"",
"{ \"a\" : 2 } | { \"a\" : 2 }",
"{ \"a\" : false } | { \"a\" : false }",
"{ \"a\" : \"false\" } | { \"a\" : \"false\" }",
"{ \"a\" : \"some text\" } | { \"a\" : \"some text\" }",
"[ 3, 2 ] | [ 3, 2 ]"
}, delimiter = '|')
public void serializeStringTokens(String input, String expected) throws IOException {
String result = serializeTheToken(input);
Assertions.assertEquals(expected, result);
}

private String serializeTheToken(String token) throws IOException {
StringWriter writer = new StringWriter();
JsonGenerator generator = new JsonFactory().createGenerator(writer);

serializer.serialize(token, generator, null);
generator.flush();
generator.close();

return writer.toString();
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -474,4 +474,59 @@ public void deleteTwinSucceedsWhenETagMatches(HttpClient httpClient, DigitalTwin
}
}
}

@ParameterizedTest(name = DISPLAY_NAME_WITH_ARGUMENTS)
@MethodSource("com.azure.digitaltwins.core.TestHelper#getTestParameters")
@Override
public void digitalTwinWithNumericStringProperty(HttpClient httpClient, DigitalTwinsServiceVersion serviceVersion) throws JsonProcessingException {
DigitalTwinsAsyncClient asyncClient = getAsyncClient(httpClient, serviceVersion);

String floorTwinId = UniqueIdHelper.getUniqueDigitalTwinId(TestAssetDefaults.FLOOR_TWIN_ID_PREFIX, asyncClient, randomIntegerStringGenerator);

String floorModelId = UniqueIdHelper.getUniqueModelId(TestAssetDefaults.FLOOR_MODEL_ID, asyncClient, randomIntegerStringGenerator);
String hvacModelId = UniqueIdHelper.getUniqueModelId(TestAssetDefaults.HVAC_MODEL_ID, asyncClient, randomIntegerStringGenerator);
String roomModelId = UniqueIdHelper.getUniqueModelId(TestAssetDefaults.ROOM_MODEL_ID, asyncClient, randomIntegerStringGenerator);

String roomModel = TestAssetsHelper.getRoomModelPayload(roomModelId, floorModelId);
String floorModel = TestAssetsHelper.getFloorModelPayload(floorModelId, roomModelId, hvacModelId);
String floorTwin = TestAssetsHelper.getFloorTwinPayload(floorModelId);

List<String> modelsList = new ArrayList<>(Arrays.asList(roomModel, floorModel));

try {
// Create models to test the Twin lifecycle.
StepVerifier
.create(asyncClient.createModels(modelsList))
.assertNext(createResponseList -> logger.info("Created models successfully"))
.verifyComplete();

// Create a Twin
BasicDigitalTwin floorTwinToCreate = deserializeJsonString(floorTwin, BasicDigitalTwin.class);
floorTwinToCreate.addToContents("name", "1234");
floorTwinToCreate.addToContents("roomType", "1234 spacious");

StepVerifier.create(asyncClient.createOrReplaceDigitalTwin(floorTwinId, floorTwinToCreate, BasicDigitalTwin.class))
.assertNext(createdTwin -> {
assertEquals(createdTwin.getId(), floorTwinId);
logger.info("Created {} twin successfully", createdTwin.getId());
})
.verifyComplete();
}
// clean up
finally {
try {
if (roomModelId != null) {
asyncClient.deleteModel(roomModelId).block();
}
if (floorTwinId != null) {
asyncClient.deleteDigitalTwin(floorTwinId).block();
}
if (floorModelId != null) {
asyncClient.deleteModel(floorModelId).block();
}
} catch (Exception ex) {
throw new AssertionFailedError("Test cleanup failed", ex);
}
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,9 @@ public abstract class TwinTestBase extends DigitalTwinsTestBase {
@Test
public abstract void digitalTwinLifecycle(HttpClient httpClient, DigitalTwinsServiceVersion serviceVersion) throws JsonProcessingException;

@Test
public abstract void digitalTwinWithNumericStringProperty(HttpClient httpClient, DigitalTwinsServiceVersion serviceVersion) throws JsonProcessingException;

@Test
public abstract void twinNotExistThrowsNotFoundException(HttpClient httpClient, DigitalTwinsServiceVersion serviceVersion);

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,7 @@ public void digitalTwinLifecycle(HttpClient httpClient, DigitalTwinsServiceVersi
DigitalTwinsClient client = getClient(httpClient, serviceVersion);

String roomTwinId = UniqueIdHelper.getUniqueDigitalTwinId(TestAssetDefaults.ROOM_TWIN_ID_PREFIX, client, randomIntegerStringGenerator);

String floorModelId = UniqueIdHelper.getUniqueModelId(TestAssetDefaults.FLOOR_MODEL_ID, client, randomIntegerStringGenerator);
String roomModelId = UniqueIdHelper.getUniqueModelId(TestAssetDefaults.ROOM_MODEL_ID, client, randomIntegerStringGenerator);

Expand All @@ -52,10 +53,10 @@ public void digitalTwinLifecycle(HttpClient httpClient, DigitalTwinsServiceVersi
Iterable<DigitalTwinsModelData> createdList = client.createModels(modelsList);
logger.info("Created models successfully");

BasicDigitalTwin createdTwin = client.createOrReplaceDigitalTwin(roomTwinId, deserializeJsonString(roomTwin, BasicDigitalTwin.class), BasicDigitalTwin.class);
BasicDigitalTwin createdRoomTwin = client.createOrReplaceDigitalTwin(roomTwinId, deserializeJsonString(roomTwin, BasicDigitalTwin.class), BasicDigitalTwin.class);

logger.info("Created {} twin successfully", createdTwin.getId());
assertEquals(createdTwin.getId(), roomTwinId);
logger.info("Created {} twin successfully", createdRoomTwin.getId());
assertEquals(createdRoomTwin.getId(), roomTwinId);

// Get Twin.
DigitalTwinsResponse<String> getTwinResponse = client.getDigitalTwinWithResponse(roomTwinId, String.class, Context.NONE);
Expand Down Expand Up @@ -452,4 +453,55 @@ public void deleteTwinSucceedsWhenETagMatches(HttpClient httpClient, DigitalTwin
}
}
}

@ParameterizedTest(name = DISPLAY_NAME_WITH_ARGUMENTS)
@MethodSource("com.azure.digitaltwins.core.TestHelper#getTestParameters")
@Override
public void digitalTwinWithNumericStringProperty(HttpClient httpClient, DigitalTwinsServiceVersion serviceVersion) throws JsonProcessingException {
DigitalTwinsClient client = getClient(httpClient, serviceVersion);

String floorTwinId = UniqueIdHelper.getUniqueDigitalTwinId(TestAssetDefaults.FLOOR_TWIN_ID_PREFIX, client, randomIntegerStringGenerator);

String floorModelId = UniqueIdHelper.getUniqueModelId(TestAssetDefaults.FLOOR_MODEL_ID, client, randomIntegerStringGenerator);
String hvacModelId = UniqueIdHelper.getUniqueModelId(TestAssetDefaults.HVAC_MODEL_ID, client, randomIntegerStringGenerator);
String roomModelId = UniqueIdHelper.getUniqueModelId(TestAssetDefaults.ROOM_MODEL_ID, client, randomIntegerStringGenerator);

String roomModel = TestAssetsHelper.getRoomModelPayload(roomModelId, floorModelId);
String floorModel = TestAssetsHelper.getFloorModelPayload(floorModelId, roomModelId, hvacModelId);
String floorTwin = TestAssetsHelper.getFloorTwinPayload(floorModelId);

List<String> modelsList = new ArrayList<>(Arrays.asList(roomModel, floorModel));

try {
// Create models to test the Twin lifecycle.
client.createModels(modelsList);
logger.info("Created models successfully");

BasicDigitalTwin floorTwinToCreate = deserializeJsonString(floorTwin, BasicDigitalTwin.class);
floorTwinToCreate.addToContents("name", "1234");
floorTwinToCreate.addToContents("roomType", "1234 spacious");

BasicDigitalTwin createdFloorTwin = client.createOrReplaceDigitalTwin(floorTwinId, floorTwinToCreate , BasicDigitalTwin.class);

logger.info("Created {} twin successfully", createdFloorTwin.getId());
assertEquals(createdFloorTwin.getId(), floorTwinId);
}
// clean up
finally {
try {
if (roomModelId != null) {
client.deleteModel(roomModelId);
}
if (floorTwinId != null) {
client.deleteDigitalTwin(floorTwinId);
}
if (floorModelId != null) {
client.deleteModel(floorModelId);
}

} catch (Exception ex) {
throw new AssertionFailedError("Test cleanup failed", ex);
}
}
}
}
Loading

0 comments on commit ac81982

Please sign in to comment.