
com.microsoft.azure.sdk.iot.deps.twin.TwinCollection Maven / Gradle / Ivy
// Copyright (c) Microsoft. All rights reserved.
// Licensed under the MIT license. See LICENSE file in the project root for full license information.
package com.microsoft.azure.sdk.iot.deps.twin;
import com.google.gson.JsonElement;
import com.google.gson.JsonObject;
import com.microsoft.azure.sdk.iot.deps.serializer.ParserUtility;
import com.microsoft.azure.sdk.iot.deps.util.Tools;
import java.util.HashMap;
import java.util.Map;
/**
* Representation of a single Twin collection.
*
* The TwinCollection is an extension of a {@code HashMap} of {@code String} and
* {@code Object} that contain individual and general versioning mechanism.
*
*
By the Twin definition, the {@code Object} can contain types of {@code Boolean},
* {@code Number}, {@code String}, {@code Object}, or a sub-TwinCollection, but
* it cannot be types defined by the user or arrays.
*
*
A TwinCollection can contain up to 5 levels of sub TwinCollections. Once the
* TwinCollection is a extension of the {@code HashMap}, both TwinCollection as well
* as its sub-TwinCollections can be casted to Map of String and Object.
*
*
The collection will be represented in the rest API as a JSON in the body. It can
* or cannot contain the metadata (identified by the $ character at the
* beginning of the key.
*
*
Because of the Twin metadata, the character $ is not allowed in the entry key.
*
*
For instance, the following JSON is a valid TwinCollection with its metadata.
*
* {@code
* {
* "Color":"White",
* "MaxSpeed":{
* "Value":500,
* "NewValue":300
* },
* "$metadata":{
* "$lastUpdated":"2017-09-21T02:07:44.238Z",
* "$lastUpdatedVersion":4,
* "Color":{
* "$lastUpdated":"2017-09-21T02:07:44.238Z",
* "$lastUpdatedVersion":4,
* },
* "MaxSpeed":{
* "$lastUpdated":"2017-09-21T02:07:44.238Z",
* "$lastUpdatedVersion":4,
* "Value":{
* "$lastUpdated":"2017-09-21T02:07:44.238Z",
* "$lastUpdatedVersion":4
* },
* "NewValue":{
* "$lastUpdated":"2017-09-21T02:07:44.238Z",
* "$lastUpdatedVersion":4
* }
* }
* },
* "$version":4
* }
* }
*
*
* This class exposes the Twin collection with or without metadata as a Map here
* user can gat both the value and the metadata. For instance, in the above TwinCollection,
* {@link #get(Object)} for Color will return White and the {@link #getTwinMetadataFinal(String)}
* for Color will return the Object TwinMetadata that contain {@link TwinMetadata#getLastUpdated()}
* that will returns the {@code Date} 2017-09-21T02:07:44.238Z and {@link TwinMetadata#getLastUpdatedVersion()}
* that will returns the {@code Integer} 4.
*
*
For the nested TwinCollection, you can do the same, for instance, the following code will return the
* value and metadata of the NewValue nested in MaxSpeed:
*
* {@code
* // Get the value of the MaxSpeed, which is a inner TwinCollection.
* TwinCollection innerMaxSpeed = (TwinCollection) twinCollection.get("MaxSpeed");
*
* // From the inner TwinCollection, get the value of the NewValue.
* Long maxSpeedNewValue = innerMaxSpeed.get("NewValue");
*
* // As in the root TwinCollection, the inner TwinCollection contain its own metadata.
* // So, get the metadata information for the inner NewValue.
* TwinMetadata maxSpeedNewValueMetadata = innerMaxSpeed.getTwinMetadataFinal("NewValue");
* Date newValueLastUpdated = maxSpeedNewValueMetadata.getLastUpdated(); //Shall contain `2017-09-21T02:07:44.238Z`
* Integer newValueLastUpdatedVersion = maxSpeedNewValueMetadata.getLastUpdatedVersion(); //Shall contain `4`
* }
*
*
* @see Understand and use device twins in IoT Hub
* @see Device Twin Api
*/
public class TwinCollection extends HashMap
{
// the Twin collection version
private static final String VERSION_TAG = "$version";
private Integer version;
// the Twin collection metadata
private static final String METADATA_TAG = "$metadata";
private TwinMetadata twinMetadata;
private Map metadataMap = new HashMap<>();
/**
* Constructor
*
* Creates an empty collection. Fill it with {@link #putFinal(String, Object)}
* or {@link #putAllFinal(Map)}.
*/
public TwinCollection()
{
/* SRS_TWIN_COLLECTION_21_001: [The constructor shall create a new instance of the super class.] */
super();
}
/**
* Constructor
*
*
Creates a new Twin collection coping the provided Map.
*
* @param map the Map of {@code ? extends String} and {@code Object} with the Twin collection
*/
public TwinCollection(Map map)
{
/* SRS_TWIN_COLLECTION_21_002: [If the Map is null or empty, the constructor shall create a new empty instance.] */
if((map != null) && !map.isEmpty())
{
/* SRS_TWIN_COLLECTION_21_003: [The constructor shall create a new instance of the super class and add the provided Map by calling putAllFinal.] */
this.putAllFinal(map);
}
}
/**
* Constructor
*
*
Creates a new Twin collection coping the provided collection.
*
* @param collection the Collection of {@code ? extends String} and {@code Object} with the Twin collection
*/
public TwinCollection(TwinCollection collection)
{
/* SRS_TWIN_COLLECTION_21_025: [If the Collection is null or empty, the constructor shall create a new empty instance.] */
if((collection != null) && !collection.isEmpty())
{
/* SRS_TWIN_COLLECTION_21_026: [The constructor shall create a new instance of the super class and add the provided Map.] */
/* SRS_TWIN_COLLECTION_21_027: [The constructor shall copy the version and metadata from the provided TwinCollection.] */
this.version = collection.getVersionFinal();
this.twinMetadata = collection.getTwinMetadataFinal();
for (Map.Entry entry: collection.entrySet())
{
if(entry.getValue() instanceof TwinCollection)
{
super.put((String)entry.getKey(), new TwinCollection((TwinCollection)entry.getValue()));
}
else
{
super.put((String)entry.getKey(), entry.getValue());
}
this.metadataMap.put((String)entry.getKey(), collection.getTwinMetadataFinal((String)entry.getKey()));
}
}
}
/**
* Add all information in the provided Map to the TwinCollection.
*
*
Override {@code HashMap.putAll(Map)}.
*
*
This function will add all entries in the Map to the TwinCollection. If the provided
* key already exists, it will replace the value by the new one. This function will not
* delete or change the content of the other keys in the Map.
*
*
As defined by the Twin, the value of a entry can be an inner Map. TwinCollection will
* accept up to 5 levels of inner Maps.
*
* @deprecated as of Deps version 0.7.1, please use {@link #putAllFinal(Map)}
*
* @param map A {@code Map} of entries to add to the TwinCollection.
*/
@Deprecated
@Override
public void putAll(Map map)
{
/* SRS_TWIN_COLLECTION_21_004: [The putAll shall throw IllegalArgumentException if the provided Map is null, empty or invalid.] */
if((map == null) || map.isEmpty())
{
throw new IllegalArgumentException("map to add cannot be null or empty.");
}
/* SRS_TWIN_COLLECTION_21_005: [The putAll shall copy all entries in the provided Map to the TwinCollection.] */
for(Map.Entry entry: map.entrySet())
{
this.putFinal(entry.getKey(), entry.getValue());
}
}
/**
* Add all information in the provided Map to the TwinCollection.
*
*
This function will add all entries in the Map to the TwinCollection. If the provided
* key already exists, it will replace the value by the new one. This function will not
* delete or change the content of the other keys in the Map.
*
*
As defined by the Twin, the value of a entry can be an inner Map. TwinCollection will
* accept up to 5 levels of inner Maps.
*
* @param map A {@code Map} of entries to add to the TwinCollection.
*/
public final void putAllFinal(Map map)
{
/* SRS_TWIN_COLLECTION_21_004: [The putAll shall throw IllegalArgumentException if the provided Map is null, empty or invalid.] */
if((map == null) || map.isEmpty())
{
throw new IllegalArgumentException("map to add cannot be null or empty.");
}
/* SRS_TWIN_COLLECTION_21_005: [The putAll shall copy all entries in the provided Map to the TwinCollection.] */
for(Map.Entry entry: map.entrySet())
{
this.putFinal(entry.getKey(), entry.getValue());
}
}
/**
* Add a single new entry in the TwinCollection.
*
*
Override {@code HashMap.put(String, Object)}.
*
*
This function will add a single pair key value to the TwinCollection. By the
* Twin definition, the {@code Object} can contain types of {@code Boolean},
* {@code Number}, {@code String}, {@code Object}, or up to 5 levels of
* sub-TwinCollection, but it cannot be types defined by the user or arrays.
*
* @param key the {@code String} that represent the key of the new entry. It cannot be {#code null} or empty.
* @param value the {@code Object} that represents the value of the new entry. It cannot be user defined type or array.
* @return The {@code Object} that correspond to the last value of this key. It will be {@code null} if there is no previous value.
* @deprecated as of Deps version 0.7.1, please use {@link #putFinal(String, Object)}
*/
@Override
@Deprecated
public Object put(String key, Object value)
{
if (key == null || key.isEmpty())
{
/* SRS_TWIN_COLLECTION_21_010: [The put shall throw IllegalArgumentException if the provided key is null, empty, or invalid, or if the value is invalid.] */
throw new IllegalArgumentException("Key cannot be null or empty");
}
/* SRS_TWIN_COLLECTION_21_006: [The put shall return the previous value of the key.] */
Object last = get(key);
/* SRS_TWIN_COLLECTION_21_007: [The put shall add the new pair key value to the TwinCollection.] */
if(value instanceof Map)
{
/* SRS_TWIN_COLLECTION_21_008: [If the value contain a Map, the put shall convert this map in inner TwinCollection.] */
super.put(key, new TwinCollection((Map)value));
}
else
{
super.put(key, value);
}
/* SRS_TWIN_COLLECTION_21_009: [The put shall throw IllegalArgumentException if the final collection contain more that 5 levels.] */
/* Codes_SRS_TWIN_COLLECTION_34_028: [The put shall not validate the map if the provided key is a metadata tag, or a version tag.] */
if (!key.equals(VERSION_TAG) && !key.equals(METADATA_TAG))
{
ParserUtility.validateMap(this);
}
return last;
}
/**
* Add a single new entry in the TwinCollection.
*
*
This function will add a single pair key value to the TwinCollection. By the
* Twin definition, the {@code Object} can contain types of {@code Boolean},
* {@code Number}, {@code String}, {@code Object}, or up to 5 levels of
* sub-TwinCollection, but it cannot be types defined by the user or arrays.
*
* @param key the {@code String} that represent the key of the new entry. It cannot be {#code null} or empty.
* @param value the {@code Object} that represents the value of the new entry. It cannot be user defined type or array.
* @return The {@code Object} that correspond to the last value of this key. It will be {@code null} if there is no previous value.
*/
public final Object putFinal(String key, Object value)
{
if (key == null || key.isEmpty())
{
/* SRS_TWIN_COLLECTION_21_010: [The put shall throw IllegalArgumentException if the provided key is null, empty, or invalid, or if the value is invalid.] */
throw new IllegalArgumentException("Key cannot be null or empty");
}
/* SRS_TWIN_COLLECTION_21_006: [The put shall return the previous value of the key.] */
Object last = get(key);
/* SRS_TWIN_COLLECTION_21_007: [The put shall add the new pair key value to the TwinCollection.] */
if(value instanceof Map)
{
/* SRS_TWIN_COLLECTION_21_008: [If the value contain a Map, the put shall convert this map in inner TwinCollection.] */
super.put(key, new TwinCollection((Map)value));
}
else
{
super.put(key, value);
}
/* SRS_TWIN_COLLECTION_21_009: [The put shall throw IllegalArgumentException if the final collection contain more that 5 levels.] */
/* Codes_SRS_TWIN_COLLECTION_34_028: [The put shall not validate the map if the provided key is a metadata tag, or a version tag.] */
if (!key.equals(VERSION_TAG) && !key.equals(METADATA_TAG))
{
ParserUtility.validateMap(this);
}
return last;
}
/**
* Internal Constructor from raw map.
*
*
This internal constructor is used to the deserialization process.
*
*
During the deserialization process, the GSON will convert both tags and
* properties to a raw Map, which will includes the $version and $metadata
* as part of the collection. So, we need to reorganize this map using the
* TwinCollection format. This constructor will do that.
*
*
For instance, the following JSON is a valid TwinCollection with its metadata.
*
* {@code
* {
* "Color":"White",
* "MaxSpeed":{
* "Value":500,
* "NewValue":300
* },
* "$metadata":{
* "$lastUpdated":"2017-09-21T02:07:44.238Z",
* "$lastUpdatedVersion":4,
* "Color":{
* "$lastUpdated":"2017-09-21T02:07:44.238Z",
* "$lastUpdatedVersion":4,
* },
* "MaxSpeed":{
* "$lastUpdated":"2017-09-21T02:07:44.238Z",
* "$lastUpdatedVersion":4,
* "Value":{
* "$lastUpdated":"2017-09-21T02:07:44.238Z",
* "$lastUpdatedVersion":4
* },
* "NewValue":{
* "$lastUpdated":"2017-09-21T02:07:44.238Z",
* "$lastUpdatedVersion":4
* }
* }
* },
* "$version":4
* }
* }
*
*
* @param rawCollection the {@code Map} with contain all TwinCollection information, without
* any differentiation between each entity is the Twin information and each entity is part
* of the Twin metadata.
* @return The instance of the {@link TwinCollection}.
* @throws IllegalArgumentException If the provided rowCollection contain an invalid parameter.
*/
protected static TwinCollection createFromRawCollection(Map rawCollection)
{
TwinCollection twinCollection = new TwinCollection();
Map metadata = null;
/* SRS_TWIN_COLLECTION_21_011: [The constructor shall convert the provided rawCollection in a valid TwinCollection.] */
for (Map.Entry entry: rawCollection.entrySet())
{
/* SRS_TWIN_COLLECTION_21_012: [If the entity contain the key `$version`, the constructor shall set the version with the value of this entity.] */
if(entry.getKey().equals(VERSION_TAG))
{
/* SRS_TWIN_COLLECTION_21_013: [The constructor shall throw IllegalArgumentException if the entity contain the key `$version` and its value is not a integer.] */
if(!(entry.getValue() instanceof Number))
{
throw new IllegalArgumentException("version is not a number");
}
twinCollection.version = ((Number) entry.getValue()).intValue();
}
/* SRS_TWIN_COLLECTION_21_014: [If the entity contain the key `$metadata`, the constructor shall create a TwinMetadata with the value of this entity.] */
else if(entry.getKey().equals(METADATA_TAG))
{
metadata = (Map)entry.getValue();
}
else
{
/* SRS_TWIN_COLLECTION_21_015: [The constructor shall throw IllegalArgumentException if the Twin collection contain more than 5 levels.] */
twinCollection.putFinal(entry.getKey(), entry.getValue());
}
}
if(metadata != null)
{
TwinCollection.addMetadata(twinCollection, metadata);
}
return twinCollection;
}
private static void addMetadata(TwinCollection twinCollection, Map metadata)
{
String lastUpdated = null;
Integer lastUpdatedVersion = null;
for(Map.Entry entry: metadata.entrySet())
{
String key = entry.getKey();
if(key.equals(TwinMetadata.LAST_UPDATE_TAG))
{
lastUpdated = (String)entry.getValue();
}
else if((key.equals(TwinMetadata.LAST_UPDATE_VERSION_TAG))&& (entry.getValue() instanceof Number))
{
lastUpdatedVersion = ((Number)entry.getValue()).intValue();
}
else
{
/* SRS_TWIN_COLLECTION_21_024: [The constructor shall throw IllegalArgumentException if the metadata is inconsistent with the TwinCollection.] */
Object valueInCollection = twinCollection.get(key);
if(valueInCollection == null)
{
throw new IllegalArgumentException("Twin metadata is inconsistent");
}
TwinMetadata twinMetadata = TwinMetadata.tryExtractFromMap(entry.getValue());
if(twinMetadata != null)
{
twinCollection.metadataMap.put(key, twinMetadata);
}
if(valueInCollection instanceof TwinCollection)
{
TwinCollection.addMetadata((TwinCollection)valueInCollection, (Map)entry.getValue());
}
}
}
if((lastUpdatedVersion != null) || !Tools.isNullOrEmpty(lastUpdated))
{
twinCollection.twinMetadata = new TwinMetadata(lastUpdated, lastUpdatedVersion);
}
}
/**
* Serializer
*
* Creates a {@code JsonElement}, which the content represents
* the information in this class and its subclasses in a JSON format.
*
*
This is useful if the caller will integrate this JSON with JSON from
* other classes to generate a consolidated JSON.
*
* @return The {@code JsonElement} with the content of this class.
*/
public JsonElement toJsonElement()
{
/* SRS_TWIN_COLLECTION_21_016: [The toJsonElement shall return a JsonElement with the information in this class in a JSON format.] */
/* SRS_TWIN_COLLECTION_21_017: [The toJsonElement shall not include any metadata in the returned JsonElement.] */
return ParserUtility.mapToJsonElement(this);
}
/**
* Serializer with metadata.
*
*
Return a JsonElement with the full content of this class,
* including the metadata.
*
* @return The {@code JsonElement} with the full content of this class.
*/
protected JsonElement toJsonElementWithMetadata()
{
/* SRS_TWIN_COLLECTION_21_018: [The toJsonElementWithMetadata shall return a JsonElement with the information in this class in a JSON format.] */
JsonObject jsonObject = ParserUtility.mapToJsonElement(this).getAsJsonObject();
/* SRS_TWIN_COLLECTION_21_019: [If version is not null, the toJsonElementWithMetadata shall include the $version in the returned jsonElement.] */
if(this.version != null)
{
jsonObject.addProperty(VERSION_TAG, this.version);
}
/* SRS_TWIN_COLLECTION_21_020: [If twinMetadata is not null, the toJsonElementWithMetadata shall include the $metadata in the returned jsonElement.] */
JsonObject jsonMetadata = new JsonObject();
this.fillJsonMetadata(jsonMetadata);
if(!jsonMetadata.entrySet().isEmpty())
{
jsonObject.add(METADATA_TAG, jsonMetadata);
}
return jsonObject;
}
private void fillJsonMetadata(JsonObject jsonMetadata)
{
if(this.twinMetadata != null)
{
jsonMetadata.addProperty(TwinMetadata.LAST_UPDATE_TAG, ParserUtility.dateTimeUtcToString(this.twinMetadata.getLastUpdated()));
jsonMetadata.addProperty(TwinMetadata.LAST_UPDATE_VERSION_TAG, this.twinMetadata.getLastUpdatedVersion());
}
for(Map.Entry entry: this.metadataMap.entrySet())
{
if(entry.getValue() != null)
{
JsonObject subMapJson = entry.getValue().toJsonElement().getAsJsonObject();
Object value = get(entry.getKey());
if (value instanceof TwinCollection)
{
((TwinCollection) value).fillJsonMetadata(subMapJson);
}
jsonMetadata.add(entry.getKey(), subMapJson);
}
}
}
/**
* Getter for the version.
*
* @deprecated as of Deps version 0.7.1, please use {@link #getVersionFinal()}
*
* @return The {@code Integer} with the version content. It can be {@code null}.
*/
@Deprecated
public Integer getVersion()
{
/* SRS_TWIN_COLLECTION_21_021: [The getVersion shall return a Integer with the stored version.] */
return this.version;
}
/**
* Getter for the version.
*
* @return The {@code Integer} with the version content. It can be {@code null}.
*/
public final Integer getVersionFinal()
{
/* SRS_TWIN_COLLECTION_21_021: [The getVersion shall return a Integer with the stored version.] */
return this.version;
}
/**
* Getter for the TwinCollection metadata
*
* @deprecated as of Deps version 0.7.1, please use {@link #getTwinMetadataFinal()}
*
* @return the {@link TwinMetadata} of the Whole TwinCollection. It can be {@code null}.
*/
@Deprecated
public TwinMetadata getTwinMetadata()
{
/* SRS_TWIN_COLLECTION_21_022: [The getTwinMetadata shall return the metadata of the whole TwinCollection.] */
if(this.twinMetadata == null)
{
return null;
}
return new TwinMetadata(this.twinMetadata);
}
/**
* Getter for the TwinCollection metadata
*
* @return the {@link TwinMetadata} of the Whole TwinCollection. It can be {@code null}.
*/
public final TwinMetadata getTwinMetadataFinal()
{
/* SRS_TWIN_COLLECTION_21_022: [The getTwinMetadata shall return the metadata of the whole TwinCollection.] */
if(this.twinMetadata == null)
{
return null;
}
return new TwinMetadata(this.twinMetadata);
}
/**
* Getter for the entry metadata in the TwinCollection.
*
* @deprecated as of Deps version 0.7.1, please use {@link #getTwinMetadataFinal(String)}
*
* @param key the {@code String} with the name of the entry to retrieve the metadata.
* @return the {@link TwinMetadata} ot the specific entry in the TwinCollection. It can be {@code null}.
*/
@Deprecated
public TwinMetadata getTwinMetadata(String key)
{
/* SRS_TWIN_COLLECTION_21_023: [The getTwinMetadata shall return the metadata of the entry that correspond to the provided key.] */
if(this.metadataMap.get(key) == null)
{
return null;
}
return new TwinMetadata(this.metadataMap.get(key));
}
/**
* Getter for the entry metadata in the TwinCollection.
*
* @param key the {@code String} with the name of the entry to retrieve the metadata.
* @return the {@link TwinMetadata} ot the specific entry in the TwinCollection. It can be {@code null}.
*/
public final TwinMetadata getTwinMetadataFinal(String key)
{
/* SRS_TWIN_COLLECTION_21_023: [The getTwinMetadata shall return the metadata of the entry that correspond to the provided key.] */
if(this.metadataMap.get(key) == null)
{
return null;
}
return new TwinMetadata(this.metadataMap.get(key));
}
/**
* Creates a pretty print JSON with the content of this class and subclasses.
*
* @return The {@code String} with the pretty print JSON.
*/
@Override
public String toString()
{
/* SRS_TWIN_COLLECTION_21_024: [The toString shall return a String with the information in this class in a pretty print JSON.] */
return toJsonElementWithMetadata().toString();
}
}