org.kaaproject.kaa.client.persistence.KaaClientPropertiesState.java Source code

Java tutorial

Introduction

Here is the source code for org.kaaproject.kaa.client.persistence.KaaClientPropertiesState.java

Source

/*
 * Copyright 2014-2016 CyberVision, Inc.
 *
 * 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 org.kaaproject.kaa.client.persistence;

import org.apache.avro.io.BinaryDecoder;
import org.apache.avro.io.BinaryEncoder;
import org.apache.avro.io.DecoderFactory;
import org.apache.avro.io.EncoderFactory;
import org.apache.avro.specific.SpecificDatumReader;
import org.apache.avro.specific.SpecificDatumWriter;
import org.apache.commons.compress.utils.Charsets;
import org.apache.commons.io.FileUtils;
import org.apache.commons.io.IOUtils;
import org.kaaproject.kaa.client.KaaClientProperties;
import org.kaaproject.kaa.client.event.EndpointAccessToken;
import org.kaaproject.kaa.client.event.EndpointKeyHash;
import org.kaaproject.kaa.client.exceptions.KaaRuntimeException;
import org.kaaproject.kaa.client.notification.TopicListHashCalculator;
import org.kaaproject.kaa.client.util.Base64;
import org.kaaproject.kaa.common.endpoint.gen.SubscriptionType;
import org.kaaproject.kaa.common.endpoint.gen.Topic;
import org.kaaproject.kaa.common.endpoint.security.KeyUtil;
import org.kaaproject.kaa.common.hash.EndpointObjectHash;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.File;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.io.InputStream;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
import java.io.OutputStream;
import java.nio.charset.Charset;
import java.security.InvalidKeyException;
import java.security.KeyPair;
import java.security.PrivateKey;
import java.security.PublicKey;
import java.util.Arrays;
import java.util.Collection;
import java.util.HashMap;
import java.util.Map;
import java.util.Objects;
import java.util.Properties;
import java.util.UUID;
import java.util.concurrent.atomic.AtomicInteger;

public class KaaClientPropertiesState implements KaaClientState {

    private static final String APP_STATE_SEQ_NUMBER = "APP_STATE_SEQ_NUMBER";
    private static final String PROFILE_HASH = "PROFILE_HASH";
    private static final String ENDPOINT_ACCESS_TOKEN = "ENDPOINT_TOKEN";

    /**
     * The Constant LOG.
     */
    private static final Logger LOG = LoggerFactory.getLogger(KaaClientPropertiesState.class);

    private static final String ATTACHED_ENDPOINTS = "attached_eps";
    private static final String NF_SUBSCRIPTIONS = "nf_subscriptions";
    private static final String IS_REGISTERED = "is_registered";
    private static final String IS_ATTACHED = "is_attached";

    private static final String EVENT_SEQ_NUM = "event.seq.num";

    private static final String TOPIC_LIST = "topic.list";
    private static final String TOPIC_LIST_HASH = "topic.list.hash";

    private static final String PROPERTIES_HASH = "properties.hash";

    private static final String NEED_PROFILE_RESYNC = "need.profile.resync";

    private final PersistentStorage storage;
    private final Base64 base64;
    private final Properties state;
    private final String stateFileLocation;
    private final String clientPrivateKeyFileLocation;
    private final String clientPublicKeyFileLocation;
    private final Map<Long, Topic> topicMap = new HashMap<>();
    private final Map<Long, Integer> nfSubscriptions = new HashMap<>();
    private final Map<EndpointAccessToken, EndpointKeyHash> attachedEndpoints = new HashMap<>();
    private final AtomicInteger eventSequence = new AtomicInteger();
    private Integer topicListHash;

    private KeyPair keyPair;
    private EndpointKeyHash keyHash;
    private boolean isConfigVersionUpdated = false;
    private boolean hasUpdate = false;

    private boolean isAutogeneratedKeys;

    public KaaClientPropertiesState(PersistentStorage storage, Base64 base64, KaaClientProperties properties) {
        this(storage, base64, properties, false);
    }

    /**
     * All needed properties for creating Kaa client
     *
     * @param storage             - in what storage platform will work
     * @param base64              - interface for Base64 type
     * @param properties          - saved properties for client
     * @param isAutogeneratedKeys - if not key pair keyPair and true, then create keys. In default it
     *                            false and used already created keys.
     */
    public KaaClientPropertiesState(PersistentStorage storage, Base64 base64, KaaClientProperties properties,
            boolean isAutogeneratedKeys) {

        super();
        this.storage = storage;
        this.base64 = base64;
        this.isAutogeneratedKeys = isAutogeneratedKeys;

        properties.setBase64(base64);

        stateFileLocation = properties.getStateFileFullName();

        clientPrivateKeyFileLocation = properties.getPrivateKeyFileFullName();

        clientPublicKeyFileLocation = properties.getPublicKeyFileFullName();

        LOG.info("Version: '{}', commit hash: '{}'", properties.getBuildVersion(), properties.getCommitHash());

        state = new Properties();
        if (storage.exists(stateFileLocation)) {
            InputStream stream = null;
            try {
                stream = storage.openForRead(stateFileLocation);
                state.load(stream);

                if (isSdkPropertiesUpdated(properties)) {
                    LOG.info("SDK properties were updated");
                    setRegistered(false);
                    setPropertiesHash(properties.getPropertiesHash());
                    //TODO: add more intelligent check by comparing part of SDK token.
                    isConfigVersionUpdated = true;
                } else {
                    LOG.info("SDK properties are up to date");
                }

                parseTopics();
                parseNfSubscriptions();

                String attachedEndpointsString = state.getProperty(ATTACHED_ENDPOINTS);
                if (attachedEndpointsString != null) {
                    String[] splittedEndpointsList = attachedEndpointsString.split(",");
                    for (String attachedEndpoint : splittedEndpointsList) {
                        if (!attachedEndpoint.isEmpty()) {
                            String[] splittedValues = attachedEndpoint.split(":");
                            attachedEndpoints.put(new EndpointAccessToken(splittedValues[0]),
                                    new EndpointKeyHash(splittedValues[1]));
                        }
                    }
                }

                String eventSeqNumStr = state.getProperty(EVENT_SEQ_NUM);
                if (eventSeqNumStr != null) {
                    Integer eventSeqNum = 0;
                    try { // NOSONAR
                        eventSeqNum = Integer.parseInt(eventSeqNumStr);
                    } catch (NumberFormatException ex) {
                        LOG.error("Unexpected exception while parsing event sequence number. "
                                + "Can not parse String: {} to Integer", eventSeqNumStr);
                    }
                    eventSequence.set(eventSeqNum);
                }
                String topicListHashStr = state.getProperty(TOPIC_LIST_HASH);
                if (topicListHashStr != null) {
                    try { // NOSONAR
                        this.topicListHash = Integer.parseInt(topicListHashStr);
                    } catch (NumberFormatException ex) {
                        LOG.error("Unexpected exception while parsing topic list hash. Can not parse String: "
                                + "{} to Integer", topicListHashStr);
                    }
                }

            } catch (Exception ex) {
                LOG.error("Can't load state file", ex);
            } finally {
                IOUtils.closeQuietly(stream);
            }
        } else {
            LOG.info("First SDK start");
            setPropertiesHash(properties.getPropertiesHash());
        }
    }

    private void parseTopics() {
        if (state.getProperty(TOPIC_LIST) != null) {
            byte[] data = base64.decodeBase64(state.getProperty(TOPIC_LIST));
            BinaryDecoder decoder = DecoderFactory.get().binaryDecoder(data, null);
            SpecificDatumReader<Topic> avroReader = new SpecificDatumReader<>(Topic.class);
            try { // NOSONAR
                Topic decodedTopic;
                while (!decoder.isEnd()) {
                    decodedTopic = avroReader.read(null, decoder);
                    LOG.debug("Loaded {}", decodedTopic);
                    topicMap.put(decodedTopic.getId(), decodedTopic);
                }
            } catch (Exception ex) {
                LOG.error("Unexpected exception occurred while reading information from decoder", ex);
            }
        } else {
            LOG.info("No topic list found in state");
        }
    }

    @SuppressWarnings("unchecked")
    private void parseNfSubscriptions() {
        if (state.getProperty(NF_SUBSCRIPTIONS) != null) {
            byte[] data = base64.decodeBase64(state.getProperty(NF_SUBSCRIPTIONS));
            ByteArrayInputStream is = new ByteArrayInputStream(data);
            try (ObjectInputStream ois = new ObjectInputStream(is)) {
                nfSubscriptions.putAll((Map<Long, Integer>) ois.readObject());
            } catch (Exception ex) {
                LOG.error("Unexpected exception occurred while reading subscription information from state", ex);
            }
        } else {
            LOG.info("No subscription info found in state");
        }
    }

    private boolean isSdkPropertiesUpdated(KaaClientProperties sdkProperties) {
        byte[] hashFromSdk = sdkProperties.getPropertiesHash();
        byte[] hashFromStateFile = base64.decodeBase64(
                state.getProperty(PROPERTIES_HASH, new String(base64.encodeBase64(new byte[0]), Charsets.UTF_8))
                        .getBytes(Charsets.UTF_8));

        return !Arrays.equals(hashFromSdk, hashFromStateFile);
    }

    private void setStateStringValue(String propertyKey, String value) {
        Object previous = state.setProperty(propertyKey, value);
        String previousString = previous == null ? null : previous.toString();
        hasUpdate |= !value.equals(previousString);
    }

    private void setStateBooleanValue(String propertyKey, boolean value) {
        Object previous = state.setProperty(propertyKey, Boolean.toString(value));
        boolean previousBoolean = previous == null ? false : Boolean.valueOf(previous.toString());
        hasUpdate |= value != previousBoolean;
    }

    private void setPropertiesHash(byte[] hash) {
        setStateStringValue(PROPERTIES_HASH, new String(base64.encodeBase64(hash), Charsets.UTF_8));
    }

    @Override
    public boolean isConfigurationVersionUpdated() {
        return isConfigVersionUpdated;
    }

    @Override
    public boolean isRegistered() {
        return Boolean.parseBoolean(state.getProperty(IS_REGISTERED, Boolean.FALSE.toString()));
    }

    @Override
    public void setRegistered(boolean registered) {
        setStateBooleanValue(IS_REGISTERED, registered);
    }

    @Override
    public boolean isNeedProfileResync() {
        return Boolean.parseBoolean(state.getProperty(NEED_PROFILE_RESYNC, Boolean.FALSE.toString()));
    }

    @Override
    public void setIfNeedProfileResync(boolean needProfileResync) {
        setStateBooleanValue(NEED_PROFILE_RESYNC, needProfileResync);
    }

    @Override
    public void persist() {
        if (hasUpdate) {
            ByteArrayOutputStream baos = new ByteArrayOutputStream();
            BinaryEncoder encoder = EncoderFactory.get().binaryEncoder(baos, null);
            SpecificDatumWriter<Topic> datumWriter = new SpecificDatumWriter<>(Topic.class);
            try {
                for (Topic topic : topicMap.values()) {
                    datumWriter.write(topic, encoder);
                    LOG.info("Persisted {}", topic);
                }
                encoder.flush();
                String base64Str = new String(base64.encodeBase64(baos.toByteArray()), Charset.forName("UTF-8"));
                state.setProperty(TOPIC_LIST, base64Str);
            } catch (IOException ex) {
                LOG.error("Can't persist topic list info", ex);
            }

            baos = new ByteArrayOutputStream();
            try (ObjectOutputStream oos = new ObjectOutputStream(baos)) {
                oos.writeObject(nfSubscriptions);
                String base64Str = new String(base64.encodeBase64(baos.toByteArray()), Charset.forName("UTF-8"));
                state.setProperty(NF_SUBSCRIPTIONS, base64Str);
            } catch (IOException ex) {
                LOG.error("Can't persist notification subscription info", ex);
            }

            StringBuilder attachedEndpointsString = new StringBuilder();
            for (Map.Entry<EndpointAccessToken, EndpointKeyHash> attached : attachedEndpoints.entrySet()) {
                attachedEndpointsString.append(attached.getKey().getToken()).append(":")
                        .append(attached.getValue().getKeyHash()).append(',');
            }
            state.setProperty(ATTACHED_ENDPOINTS, attachedEndpointsString.toString());
            state.setProperty(EVENT_SEQ_NUM, "" + eventSequence.get());
            if (topicListHash != null) {
                state.setProperty(TOPIC_LIST_HASH, "" + topicListHash);
            }

            OutputStream os = null;
            try {
                storage.renameTo(stateFileLocation, stateFileLocation + "_bckp");
                os = storage.openForWrite(stateFileLocation);
                state.store(os, null);
                hasUpdate = false;
            } catch (IOException ex) {
                LOG.error("Can't persist state file", ex);
            } finally {
                IOUtils.closeQuietly(os);
            }
        }
    }

    @Override
    public String refreshEndpointAccessToken() {
        String newAccessToken = UUID.randomUUID().toString();
        setEndpointAccessToken(newAccessToken);
        return newAccessToken;
    }

    @Override
    public PrivateKey getPrivateKey() {
        return getOrInitKeyPair(isAutogeneratedKeys).getPrivate();
    }

    @Override
    public PublicKey getPublicKey() {
        return getOrInitKeyPair(isAutogeneratedKeys).getPublic();
    }

    private KeyPair getOrInitKeyPair(boolean isAutogeneratedKeys) {
        LOG.debug("Check if key pair exists {}, {}", clientPublicKeyFileLocation, clientPrivateKeyFileLocation);
        if (keyPair != null) {
            return keyPair;
        }
        if (storage.exists(clientPublicKeyFileLocation) && storage.exists(clientPrivateKeyFileLocation)) {
            InputStream publicKeyInput = null;
            InputStream privateKeyInput = null;
            try {
                publicKeyInput = storage.openForRead(clientPublicKeyFileLocation);
                privateKeyInput = storage.openForRead(clientPrivateKeyFileLocation);

                PublicKey publicKey = KeyUtil.getPublic(publicKeyInput);
                PrivateKey privateKey = KeyUtil.getPrivate(privateKeyInput);

                if (publicKey != null && privateKey != null) {
                    keyPair = new KeyPair(publicKey, privateKey);
                    if (!KeyUtil.validateKeyPair(keyPair)) {
                        throw new InvalidKeyException();
                    }

                    return keyPair;
                }
            } catch (InvalidKeyException ex) {
                keyPair = null;
                LOG.error("Unable to parse client RSA keypair. Generating new keys.. Reason {}", ex);
            } catch (Exception ex) {
                LOG.error("Error loading client RSA keypair. Reason {}", ex);
                throw new RuntimeException(ex); // NOSONAR
            } finally {
                IOUtils.closeQuietly(publicKeyInput);
                IOUtils.closeQuietly(privateKeyInput);
            }
        }
        if (isAutogeneratedKeys) {
            LOG.debug("Generating Client Key pair");
            OutputStream privateKeyOutput = null;
            OutputStream publicKeyOutput = null;
            try {
                privateKeyOutput = storage.openForWrite(clientPrivateKeyFileLocation);
                publicKeyOutput = storage.openForWrite(clientPublicKeyFileLocation);
                keyPair = KeyUtil.generateKeyPair(privateKeyOutput, publicKeyOutput);
            } catch (IOException ex) {
                LOG.error("Error generating Client Key pair", ex);
                throw new RuntimeException(ex);
            } finally {
                IOUtils.closeQuietly(privateKeyOutput);
                IOUtils.closeQuietly(publicKeyOutput);
            }
        } else {
            LOG.debug("Error loading key pair!", "Key pair is not found and key strategy is default");
            throw new KaaRuntimeException("Key pair is not found and your key strategy is default");
        }
        return keyPair;
    }

    @Override
    public EndpointKeyHash getEndpointKeyHash() {
        if (keyHash == null) {
            EndpointObjectHash publicKeyHash = EndpointObjectHash
                    .fromSha1(getOrInitKeyPair(isAutogeneratedKeys).getPublic().getEncoded());
            keyHash = new EndpointKeyHash(new String(base64.encodeBase64(publicKeyHash.getData())));
        }
        return keyHash;
    }

    @Override
    public int getAppStateSeqNumber() {
        return Integer.parseInt(state.getProperty(APP_STATE_SEQ_NUMBER, "1"));
    }

    @Override
    public void setAppStateSeqNumber(int appStateSeqNumber) {
        setStateStringValue(APP_STATE_SEQ_NUMBER, Integer.toString(appStateSeqNumber));
    }

    @Override
    public EndpointObjectHash getProfileHash() {
        return EndpointObjectHash.fromBytes(base64.decodeBase64(
                state.getProperty(PROFILE_HASH, new String(base64.encodeBase64(new byte[0]), Charsets.UTF_8))
                        .getBytes(Charsets.UTF_8)));
    }

    @Override
    public void setProfileHash(EndpointObjectHash hash) {
        setStateStringValue(PROFILE_HASH, new String(base64.encodeBase64(hash.getData()), Charsets.UTF_8));
    }

    @Override
    public void addTopic(Topic topic) {
        if (topicMap.get(topic.getId()) == null) {
            topicMap.put(topic.getId(), topic);
            if (topic.getSubscriptionType() == SubscriptionType.MANDATORY_SUBSCRIPTION) {
                nfSubscriptions.put(topic.getId(), 0);
                LOG.info("Adding new seqNumber 0 for {} subscription", topic.getId());
            }
            hasUpdate = true;
            LOG.info("Adding new topic with id {}", topic.getId());
        }
    }

    @Override
    public void removeTopic(Long topicId) {
        if (topicMap.remove(topicId) != null) {
            if (nfSubscriptions.remove(topicId) != null) {
                LOG.info("Removed subscription info for {}", topicId);
            }
            hasUpdate = true;
            LOG.info("Removed topic with id {}", topicId);
        }
    }

    @Override
    public void addTopicSubscription(Long topicId) {
        Integer seqNum = nfSubscriptions.get(topicId);
        if (seqNum == null) {
            nfSubscriptions.put(topicId, 0);
            LOG.info("Adding new seqNumber 0 for {} subscription", topicId);
            hasUpdate = true;
        }
    }

    @Override
    public void removeTopicSubscription(Long topicId) {
        if (nfSubscriptions.remove(topicId) != null) {
            LOG.info("Removed subscription info for {}", topicId);
            hasUpdate = true;
        }
    }

    @Override
    public boolean updateTopicSubscriptionInfo(Long topicId, Integer sequenceNumber) {
        Integer seqNum = nfSubscriptions.get(topicId);
        boolean updated = false;
        if (seqNum != null) {
            if (sequenceNumber > seqNum) {
                updated = true;
                nfSubscriptions.put(topicId, sequenceNumber);
                hasUpdate = true;
                LOG.debug("Updated seqNumber to {} for {} subscription", sequenceNumber, topicId);
            }
        }
        return updated;
    }

    @Override
    public Map<Long, Integer> getNfSubscriptions() {
        return nfSubscriptions;
    }

    @Override
    public Collection<Topic> getTopics() {
        return topicMap.values();
    }

    @Override
    public Integer getTopicListHash() {
        if (topicListHash == null) {
            return TopicListHashCalculator.NULL_LIST_HASH;
        } else {
            return topicListHash;
        }
    }

    @Override
    public void setTopicListHash(Integer topicListHash) {
        if (!Objects.equals(this.topicListHash, topicListHash)) {
            this.topicListHash = topicListHash;
            hasUpdate = true;
        }
    }

    @Override
    public Map<EndpointAccessToken, EndpointKeyHash> getAttachedEndpointsList() {
        return attachedEndpoints;
    }

    @Override
    public void setAttachedEndpointsList(Map<EndpointAccessToken, EndpointKeyHash> attachedEndpoints) {
        this.attachedEndpoints.clear();
        this.attachedEndpoints.putAll(attachedEndpoints);
        hasUpdate = true;
    }

    @Override
    public String getEndpointAccessToken() {
        return state.getProperty(ENDPOINT_ACCESS_TOKEN, "");
    }

    @Override
    public void setEndpointAccessToken(String token) {
        setStateStringValue(ENDPOINT_ACCESS_TOKEN, token);
    }

    @Override
    public int getAndIncrementEventSeqNum() {
        hasUpdate = true;
        return eventSequence.getAndIncrement();
    }

    @Override
    public int getEventSeqNum() {
        return eventSequence.get();
    }

    @Override
    public void setEventSeqNum(int newSeqNum) {
        if (eventSequence.get() != newSeqNum) {
            eventSequence.set(newSeqNum);
            hasUpdate = true;
        }
    }

    @Override
    public boolean isAttachedToUser() {
        return Boolean.parseBoolean(state.getProperty(IS_ATTACHED, Boolean.FALSE.toString()));
    }

    @Override
    public void setAttachedToUser(boolean isAttached) {
        setStateBooleanValue(IS_ATTACHED, isAttached);
    }

    @Override
    public void clean() {
        setRegistered(false);
        setIfNeedProfileResync(false);
        saveFileDelete(stateFileLocation);
        saveFileDelete(stateFileLocation + "_bckp");
        keyPair = null;
        hasUpdate = true;
    }

    private void saveFileDelete(String fileName) {
        try {
            FileUtils.forceDelete(new File(fileName));
        } catch (FileNotFoundException ex) {
            LOG.trace("File {} wasn't deleted, as it hadn't existed :", fileName, ex);
        } catch (IOException ex) {
            LOG.debug("An error occurred during deletion of the file [{}] :", fileName, ex);
        }
    }

}