All Downloads are FREE. Search and download functionalities are using the official Maven repository.

com.microsoft.azure.sdk.iot.deps.serializer.TwinProperty Maven / Gradle / Ivy

The newest version!
// 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.serializer;

import com.google.gson.Gson;
import com.google.gson.GsonBuilder;
import com.google.gson.JsonElement;
import com.google.gson.internal.LinkedTreeMap;

import java.util.HashMap;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ConcurrentMap;

/**
 * INNER TWINPARSER CLASS
 *
 * TwinProperty is a representation of the device twin property collection.
 * It can represent `Desired` property as well `Reported` property.
 *
 */
public class TwinProperty
{

    private static final String VERSION_TAG = "$version";
    private static final String METADATA_TAG = "$metadata";
    private static final String LAST_UPDATE_TAG = "$lastUpdated";
    private static final String LAST_UPDATE_VERSION_TAG = "$lastUpdatedVersion";

    private Object lock = new Object();

    private class Property
    {
        private Object value;
        private TwinMetadata metadata;
        private Property(Object val, Integer propertyVersion)
        {
            this.value = val;
            this.metadata = new TwinMetadata(propertyVersion);
        }
    }

    private ConcurrentMap property = new ConcurrentHashMap<>();;
    private Integer version;
    private Boolean reportMetadata;

    protected TwinProperty()
    {
        this.reportMetadata = false;
        this.version = null;
    }

    protected void enableMetadata()
    {
        /* Codes_SRS_TWINPARSER_21_020: [The enableMetadata shall enable report metadata in Json for the Desired and for the Reported Properties.] */
        this.reportMetadata = true;
    }

    protected Boolean addProperty(String key, Object value, Integer propertyVersion) throws IllegalArgumentException
    {
        /* Codes_SRS_TWINPARSER_21_059: [The updateDesiredProperty shall only change properties in the map, keep the others as is.] */
        /* Codes_SRS_TWINPARSER_21_061: [All `key` and `value` in property shall be case sensitive.] */
        /* Codes_SRS_TWINPARSER_21_060: [The updateReportedProperty shall only change properties in the map, keep the others as is.] */
        /* Codes_SRS_TWINPARSER_21_062: [All `key` and `value` in property shall be case sensitive.] */
        Boolean change = false;

        if(key == null)
        {
            /* Codes_SRS_TWINPARSER_21_073: [If any `key` is null, the updateDesiredProperty shall throw IllegalArgumentException.] */
            /* Codes_SRS_TWINPARSER_21_079: [If any `key` is null, the updateReportedProperty shall throw IllegalArgumentException.] */
            throw new IllegalArgumentException("Property key shall not be null");
        }
        if(key.isEmpty())
        {
            /* Codes_SRS_TWINPARSER_21_074: [If any `key` is empty, the updateDesiredProperty shall throw IllegalArgumentException.] */
            /* Codes_SRS_TWINPARSER_21_080: [If any `key` is empty, the updateReportedProperty shall throw IllegalArgumentException.] */
            throw new IllegalArgumentException("Property key shall not be empty");
        }
        if(key.length()>128)
        {
            /* Codes_SRS_TWINPARSER_21_075: [If any `key` is more than 128 characters long, the updateDesiredProperty shall throw IllegalArgumentException.] */
            /* Codes_SRS_TWINPARSER_21_081: [If any `key` is more than 128 characters long, the updateReportedProperty shall throw IllegalArgumentException.] */
            throw new IllegalArgumentException("Property key is too big for json");
        }
        if(key.contains(".") || key.contains(" ") || key.contains("$") )
        {
            /* Codes_SRS_TWINPARSER_21_076: [If any `key` has an illegal character, the updateDesiredProperty shall throw IllegalArgumentException.] */
            /* Codes_SRS_TWINPARSER_21_082: [If any `key` has an illegal character, the updateReportedProperty shall throw IllegalArgumentException.] */
            throw new IllegalArgumentException("Property key contains illegal character");
        }

        if(value == null)
        {
            /* Codes_SRS_TWINPARSER_21_078: [If any `value` is null, the updateDesiredProperty shall delete it from the collection and report on Json.] */
            /* Codes_SRS_TWINPARSER_21_084: [If any `value` is null, the updateReportedProperty shall delete it from the collection and report on Json.] */
            if(property.containsKey(key))
            {
                property.remove(key);
            }
            change = true;
        }
        else
        {
            /* Codes_SRS_TWINPARSER_21_077: [If any `key` already exists, the updateDesiredProperty shall replace the existed value by the new one.] */
            /* Codes_SRS_TWINPARSER_21_083: [If any `key` already exists, the updateReportedProperty shall replace the existed value by the new one.] */
            if ((!property.containsKey(key)) || (!property.get(key).value.equals(value)) || reportMetadata)
            {
                change = true;
            }
            property.put(key, new Property(value, propertyVersion));
        }

        return change;
    }

    protected JsonElement update(Map property)
    {
        JsonElement updatedJsonElement;
        TwinProperty updated = new TwinProperty();

        synchronized (lock)
        {
            if (property != null)
            {
                for (Map.Entry entry : property.entrySet())
                {
                    if (addProperty(entry.getKey(), entry.getValue(), null))
                    {
                        /* Codes_SRS_TWINPARSER_21_078: [If any `value` is null, the updateDesiredProperty shall delete it from the collection and report on Json.] */
                        /* Codes_SRS_TWINPARSER_21_084: [If any `value` is null, the updateReportedProperty shall delete it from the collection and report on Json.] */
                        updated.property.put(entry.getKey(), new Property(entry.getValue(), null));
                    }
                }

                if (updated.size() > 0)
                {
                    updatedJsonElement = updated.toJsonElement();
                }
                else
                {
                    updatedJsonElement = null;
                }
            }
            else
            {
                updatedJsonElement = null;
            }
        }

        return updatedJsonElement;
    }

    protected Integer getVersion()
    {
        /* Codes_SRS_TWINPARSER_21_048: [The getDesiredPropertyVersion shall return the desired property version.] */
        /* Codes_SRS_TWINPARSER_21_049: [The getReportedPropertyVersion shall return the reported property version.] */
        return this.version;
    }

    protected TwinMetadata getMetadata(String key)
    {
        TwinMetadata twinMetadata = null;

        synchronized (lock)
        {
            if (property.containsKey(key))
            {
                twinMetadata = property.get(key).metadata;
            }
            else
            {
                twinMetadata = null;
            }
        }

        return twinMetadata;
    }

    protected Map getPropertyMap()
    {
        /* Codes_SRS_TWINPARSER_21_050: [The getDesiredPropertyMap shall return a map with all desired property key value pairs.] */
        /* Codes_SRS_TWINPARSER_21_051: [The getReportedPropertyMap shall return a map with all reported property key value pairs.] */
        Map propertyMap = null;

        synchronized (lock)
        {
            if (property.isEmpty())
            {
                propertyMap = null;
            }
            else
            {
                propertyMap = new HashMap<>();
                for (Map.Entry entry : property.entrySet())
                {
                    Object value = entry.getValue().value;
                    if (value == null)
                    {
                        propertyMap.put(entry.getKey(), null);
                    }
                    else
                    {
                        propertyMap.put(entry.getKey(), value);
                    }
                }
            }
        }
        return propertyMap;
    }

    protected int size()
    {
        return this.property.size();
    }

    protected Object get(String key)
    {
        Object result = null;

        synchronized (lock)
        {
            if (property.containsKey(key))
            {
                result = property.get(key).value;
            }
            else
            {
                result = null;
            }
        }
        return result;
    }

    protected String toJson()
    {
        return toJsonElement().toString();
    }

    protected JsonElement toJsonElement()
    {
        /* Codes_SRS_TWINPARSER_21_017: [The toJsonElement shall return a JsonElement with information in the TwinParser using json format.] */
        Map diffMap = new HashMap<>();
        Map metadata = new HashMap<>();

        synchronized (lock)
        {
            for (Map.Entry entry : property.entrySet())
            {
                diffMap.put(entry.getKey(), entry.getValue().value);
                metadata.put(entry.getKey(), entry.getValue().metadata.toJsonElement());
            }
        }

        if(reportMetadata)
        {
            diffMap.put(METADATA_TAG, metadata);
        }

        if(version != null)
        {
            diffMap.put(VERSION_TAG, version);
        }

        return ParserUtility.mapToJsonElement(diffMap);
    }

    protected void update(LinkedTreeMap jsonTree,
                       TwinChangedCallback onCallback) throws IllegalArgumentException
    {
        Map diffField = new HashMap<>();
        Map diffMetadata = new HashMap<>();

        try
        {
            /* Codes_SRS_TWINPARSER_21_039: [The updateTwin shall fill the fields the properties in the TwinParser class with the keys and values provided in the json string.] */
            /* Codes_SRS_TWINPARSER_21_029: [The updateDesiredProperty shall update the Desired property using the information provided in the json.] */
            /* Codes_SRS_TWINPARSER_21_034: [The updateReportedProperty shall update the Reported property using the information provided in the json.] */
            updateVersion(jsonTree);
            /* Codes_SRS_TWINPARSER_21_041: [The updateTwin shall create a list with all properties that was updated (new key or value) by the new json.] */
            /* Codes_SRS_TWINPARSER_21_030: [The updateDesiredProperty shall generate a map with all pairs key value that had its content changed.] */
            /* Codes_SRS_TWINPARSER_21_035: [The updateReportedProperty shall generate a map with all pairs key value that had its content changed.] */
            diffField = updateFields(jsonTree);
            diffMetadata = updateMetadata(jsonTree);
        }
        catch (Exception e)
        {
            /* Codes_SRS_TWINPARSER_21_092: [If the provided json is not valid, the updateDesiredProperty shall throws IllegalArgumentException.] */
            throw new IllegalArgumentException("Malformed Json:" + e);
        }

        if(reportMetadata)
        {
            for(Map.Entry entry : diffMetadata.entrySet())
            {
                Property val = property.get(entry.getKey());
                if (val == null)
                {
                    diffField.put(entry.getKey(), null);
                }
                else
                {
                    diffField.put(entry.getKey(), val.value.toString());
                }
            }
        }

        /* Codes_SRS_TWINPARSER_21_046: [If OnDesiredCallback was not provided, the updateTwin shall not do anything with the list of updated desired properties.] */
        /* Codes_SRS_TWINPARSER_21_047: [If OnReportedCallback was not provided, the updateTwin shall not do anything with the list of updated reported properties.] */
        /* Codes_SRS_TWINPARSER_21_069: [If there is no change in the Desired property, the updateTwin shall not change the reported collection and not call the OnReportedCallback.] */
        /* Codes_SRS_TWINPARSER_21_070: [If there is no change in the Reported property, the updateTwin shall not change the reported collection and not call the OnReportedCallback.] */
        /* Codes_SRS_TWINPARSER_21_032: [If the OnDesiredCallback is set as null, the updateDesiredProperty shall discard the map with the changed pairs.] */
        /* Codes_SRS_TWINPARSER_21_033: [If there is no change in the Desired property, the updateDesiredProperty shall not change the collection and not call the OnDesiredCallback.] */
        /* Codes_SRS_TWINPARSER_21_037: [If the OnReportedCallback is set as null, the updateReportedProperty shall discard the map with the changed pairs.] */
        /* Codes_SRS_TWINPARSER_21_038: [If there is no change in the Reported property, the updateReportedProperty shall not change the collection and not call the OnReportedCallback.] */
        /* Codes_SRS_TWINPARSER_21_093: [If the provided json is not valid, the updateReportedProperty shall throws IllegalArgumentException.] */
        if((diffField.size() != 0) &&(onCallback != null))
        {
            /* Codes_SRS_TWINPARSER_21_044: [If OnDesiredCallback was provided, the updateTwin shall create a new map with a copy of all pars key values updated by the json in the Desired property, and OnDesiredCallback passing this map as parameter.] */
            /* Codes_SRS_TWINPARSER_21_045: [If OnReportedCallback was provided, the updateTwin shall create a new map with a copy of all pars key values updated by the json in the Reported property, and OnReportedCallback passing this map as parameter.] */
            /* Codes_SRS_TWINPARSER_21_031: [The updateDesiredProperty shall send the map with all changed pairs to the upper layer calling onDesiredCallback (TwinChangedCallback).] */
            /* Codes_SRS_TWINPARSER_21_036: [The updateReportedProperty shall send the map with all changed pairs to the upper layer calling onReportedCallback (TwinChangedCallback).] */
            onCallback.execute(diffField);
        }
    }

    protected void update(String json, TwinChangedCallback onCallback) throws IllegalArgumentException
    {
        LinkedTreeMap newValues;
        try
        {
            /* Codes_SRS_TWINPARSER_21_095: [If the provided json have any duplicated `key`, the updateReportedProperty shall throws IllegalArgumentException.] */
            /* Codes_SRS_TWINPARSER_21_096: [If the provided json have any duplicated `key`, the updateDesiredProperty shall throws IllegalArgumentException.] */
            Gson gson = new GsonBuilder().create();
            newValues = (LinkedTreeMap) gson.fromJson(json, LinkedTreeMap.class);
        }
        catch (Exception e)
        {
            throw new IllegalArgumentException("Malformed Json:" + e);
        }
        update(newValues, onCallback);
    }

    private void updateVersion(LinkedTreeMap jsonTree)
    {
        for (Map.Entry entry : jsonTree.entrySet())
        {
            if (entry.getKey().equals(VERSION_TAG))
            {
                version = new Integer( (int) ((double) entry.getValue()));
                break;
            }
        }
    }

    private Map  updateMetadata(LinkedTreeMap jsonTree)
    {
        Map diff = new HashMap<>();
        for (Map.Entry entry : jsonTree.entrySet())
        {
            if(entry.getKey().equals(METADATA_TAG))
            {
                LinkedTreeMap metadataTree = (LinkedTreeMap)entry.getValue();
                for (LinkedTreeMap.Entry item : metadataTree.entrySet())
                {
                    synchronized (lock)
                    {
                        if (property.containsKey(item.getKey()))
                        {
                            LinkedTreeMap itemTree = (LinkedTreeMap) item.getValue();
                            String lastUpdated = null;
                            Integer lastUpdatedVersion = null;
                            for (LinkedTreeMap.Entry metadataItem : itemTree.entrySet())
                            {
                                if (metadataItem.getKey().equals(LAST_UPDATE_TAG))
                                {
                                    lastUpdated = metadataItem.getValue().toString();
                                }
                                else if (metadataItem.getKey().equals(LAST_UPDATE_VERSION_TAG))
                                {
                                    lastUpdatedVersion = (int) ((double) metadataItem.getValue());
                                }
                            }
                            if (property.get(item.getKey()).metadata.update(lastUpdated, lastUpdatedVersion))
                            {
                                diff.put(item.getKey(), item.getValue().toString());
                            }
                        }
                    }
                }
                break;
            }
        }
        return diff;
    }

    private Map updateFields(LinkedTreeMap jsonTree) throws IllegalArgumentException
    {
        Map diff = new HashMap<>();

        for (Map.Entry entry : jsonTree.entrySet())
        {
            if(entry.getKey().isEmpty())
            {
                throw new IllegalArgumentException("Invalid Key on Json");
            }
            if(!entry.getKey().contains("$"))
            {
                synchronized (lock)
                {
                    /* Codes_SRS_TWINPARSER_21_040: [The updateTwin shall not change fields that is not reported in the json string.] */
                    if (property.containsKey(entry.getKey()))
                    {
                        if (entry.getValue() == null)
                        {
                            /* Codes_SRS_TWINPARSER_21_042: [If a valid key has a null value, the updateTwin shall delete this property.] */
                            property.remove(entry.getKey());
                            diff.put(entry.getKey(), null);
                        }
                        else if (!property.get(entry.getKey()).value.equals(entry.getValue()))
                        {
                            property.put(entry.getKey(), new Property(entry.getValue(), null));
                            diff.put(entry.getKey(), entry.getValue().toString());
                        }
                    }
                    else if (entry.getValue() != null)
                    {
                        property.put(entry.getKey(), new Property(entry.getValue(), null));
                        diff.put(entry.getKey(), entry.getValue().toString());
                    }
                }
            }
        }

        return diff;
    }

}




© 2015 - 2025 Weber Informatics LLC | Privacy Policy