org.eclipse.hono.service.registration.impl.FileBasedRegistrationService.java Source code

Java tutorial

Introduction

Here is the source code for org.eclipse.hono.service.registration.impl.FileBasedRegistrationService.java

Source

/**
 * Copyright (c) 2016, 2017 Bosch Software Innovations GmbH.
 *
 * All rights reserved. This program and the accompanying materials
 * are made available under the terms of the Eclipse Public License v1.0
 * which accompanies this distribution, and is available at
 * http://www.eclipse.org/legal/epl-v10.html
 *
 * Contributors:
 *    Bosch Software Innovations GmbH - initial creation
 */

package org.eclipse.hono.service.registration.impl;

import static java.net.HttpURLConnection.*;
import static org.eclipse.hono.util.RegistrationConstants.*;

import java.util.HashMap;
import java.util.Map;
import java.util.Map.Entry;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.atomic.AtomicInteger;

import org.eclipse.hono.config.ServiceConfigProperties;
import org.eclipse.hono.service.registration.BaseRegistrationService;
import org.eclipse.hono.util.RegistrationResult;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.context.annotation.Profile;
import org.springframework.stereotype.Repository;

import io.vertx.core.AsyncResult;
import io.vertx.core.Future;
import io.vertx.core.Handler;
import io.vertx.core.buffer.Buffer;
import io.vertx.core.file.FileSystem;
import io.vertx.core.json.JsonArray;
import io.vertx.core.json.JsonObject;

/**
 * A registration service that keeps all data in memory but is backed by a file.
 * <p>
 * On startup this adapter loads all registered devices from a file. On shutdown all
 * devices kept in memory are written to the file.
 */
@Repository
@ConfigurationProperties(prefix = "hono.registration")
@Profile({ "default", "registration-file" })
public class FileBasedRegistrationService extends BaseRegistrationService<ServiceConfigProperties> {

    /**
     * The default number of devices that can be registered for each tenant.
     */
    public static final int DEFAULT_MAX_DEVICES_PER_TENANT = 100;
    private static final String ARRAY_DEVICES = "devices";
    private static final String FIELD_TENANT = "tenant";

    // <tenantId, <deviceId, registrationData>>
    private Map<String, Map<String, JsonObject>> identities = new HashMap<>();
    // the name of the file used to persist the registry content
    private String filename = "device-identities.json";
    private boolean saveToFile = false;
    private boolean isModificationEnabled = true;
    private int maxDevicesPerTenant = DEFAULT_MAX_DEVICES_PER_TENANT;
    private boolean running = false;
    private boolean dirty = false;

    /**
     * Sets the path to the file that the content of the registry should be persisted
     * periodically.
     * <p>
     * Default value is <em>device-identities.json</em>.
     * 
     * @param filename The name of the file to persist to (can be a relative or absolute path).
     * @throws IllegalStateException if this registry is already running.
     */
    public void setFilename(final String filename) {
        if (running) {
            throw new IllegalStateException("registry already started");
        }
        this.filename = filename;
    }

    /**
     * Sets whether the content of the registry should be persisted to the file system
     * periodically.
     * <p>
     * Default value is {@code true}.
     * 
     * @param enabled {@code true} if registry content should be persisted.
     * @throws IllegalStateException if this registry is already running.
     */
    public void setSaveToFile(final boolean enabled) {
        if (running) {
            throw new IllegalStateException("registry already started");
        }
        this.saveToFile = enabled;
    }

    /**
     * Sets whether this registry allows modification and removal of registered devices.
     * <p>
     * If set to {@code false} then the methods {@link #updateDevice(String, String, JsonObject, Handler)}
     * and {@link #removeDevice(String, String, Handler)} always return a <em>403 Forbidden</em> response.
     * <p>
     * The default value of this property is {@code true}.
     * 
     * @param flag The flag.
     */
    public void setModificationEnabled(final boolean flag) {
        isModificationEnabled = flag;
    }

    /**
     * Sets the maximum nubmer of devices that can be registered for each tenant.
     * <p>
     * The default value of this property is {@link #DEFAULT_MAX_DEVICES_PER_TENANT}.
     * 
     * @param maxDevices The maximum number of devices.
     * @throws IllegalArgumentException if the number of devices is &lt;= 0.
     */
    public void setMaxDevicesPerTenant(final int maxDevices) {
        if (maxDevices <= 0) {
            throw new IllegalArgumentException("max devices must be > 0");
        }
        this.maxDevicesPerTenant = maxDevices;
    }

    @Override
    protected void doStart(Future<Void> startFuture) {

        if (!running) {
            if (!isModificationEnabled) {
                log.info("modification of registered devices has been disabled");
            }
            if (filename != null) {
                loadRegistrationData();
                if (saveToFile) {
                    log.info("saving device identities to file every 3 seconds");
                    vertx.setPeriodic(3000, saveIdentities -> {
                        saveToFile(Future.future());
                    });
                } else {
                    log.info("persistence is disabled, will not safe device identities to file");
                }
            }
        }
        running = true;
        startFuture.complete();
    }

    private void loadRegistrationData() {
        if (filename != null) {
            final FileSystem fs = vertx.fileSystem();
            log.debug("trying to load device registration information from file {}", filename);
            if (fs.existsBlocking(filename)) {
                final AtomicInteger deviceCount = new AtomicInteger();
                fs.readFile(filename, readAttempt -> {
                    if (readAttempt.succeeded()) {
                        JsonArray allObjects = new JsonArray(new String(readAttempt.result().getBytes()));
                        for (Object obj : allObjects) {
                            JsonObject tenant = (JsonObject) obj;
                            String tenantId = tenant.getString(FIELD_TENANT);
                            Map<String, JsonObject> deviceMap = new HashMap<>();
                            for (Object deviceObj : tenant.getJsonArray(ARRAY_DEVICES)) {
                                JsonObject device = (JsonObject) deviceObj;
                                deviceMap.put(device.getString(FIELD_HONO_ID), device.getJsonObject(FIELD_DATA));
                                deviceCount.incrementAndGet();
                            }
                            identities.put(tenantId, deviceMap);
                        }
                        log.info("successfully loaded {} device identities from file [{}]", deviceCount.get(),
                                filename);
                    } else {
                        log.warn("could not load device identities from file [{}]", filename, readAttempt.cause());
                    }
                });
            } else {
                log.debug("device identity file {} does not exist (yet)", filename);
            }
        }
    }

    @Override
    protected void doStop(Future<Void> stopFuture) {

        if (running) {
            Future<Void> stopTracker = Future.future();
            stopTracker.setHandler(stopAttempt -> {
                running = false;
                stopFuture.complete();
            });

            if (saveToFile) {
                saveToFile(stopTracker);
            } else {
                stopTracker.complete();
            }
        } else {
            stopFuture.complete();
        }
    }

    private void saveToFile(final Future<Void> writeResult) {

        if (!dirty) {
            log.trace("registry does not need to be persisted");
            return;
        }

        final FileSystem fs = vertx.fileSystem();
        if (!fs.existsBlocking(filename)) {
            fs.createFileBlocking(filename);
        }
        final AtomicInteger idCount = new AtomicInteger();
        JsonArray tenants = new JsonArray();
        for (Entry<String, Map<String, JsonObject>> entry : identities.entrySet()) {
            JsonArray devices = new JsonArray();
            for (Entry<String, JsonObject> deviceEntry : entry.getValue().entrySet()) {
                devices.add(new JsonObject().put(FIELD_HONO_ID, deviceEntry.getKey()).put(FIELD_DATA,
                        deviceEntry.getValue()));
                idCount.incrementAndGet();
            }
            tenants.add(new JsonObject().put(FIELD_TENANT, entry.getKey()).put(ARRAY_DEVICES, devices));
        }
        fs.writeFile(filename, Buffer.factory.buffer(tenants.encodePrettily()), writeAttempt -> {
            if (writeAttempt.succeeded()) {
                dirty = false;
                log.trace("successfully wrote {} device identities to file {}", idCount.get(), filename);
                writeResult.complete();
            } else {
                log.warn("could not write device identities to file {}", filename, writeAttempt.cause());
                writeResult.fail(writeAttempt.cause());
            }
        });
    }

    @Override
    public void getDevice(final String tenantId, final String deviceId,
            final Handler<AsyncResult<RegistrationResult>> resultHandler) {
        resultHandler.handle(Future.succeededFuture(getDevice(tenantId, deviceId)));
    }

    RegistrationResult getDevice(final String tenantId, final String deviceId) {
        JsonObject data = getRegistrationData(tenantId, deviceId);
        if (data != null) {
            return RegistrationResult.from(HTTP_OK, getResultPayload(deviceId, data));
        } else {
            return RegistrationResult.from(HTTP_NOT_FOUND);
        }
    }

    private JsonObject getRegistrationData(final String tenantId, final String deviceId) {

        final Map<String, JsonObject> devices = identities.get(tenantId);
        if (devices != null) {
            return devices.get(deviceId);
        } else {
            return null;
        }
    }

    @Override
    public void findDevice(final String tenantId, final String key, final String value,
            final Handler<AsyncResult<RegistrationResult>> resultHandler) {
        resultHandler.handle(Future.succeededFuture(findDevice(tenantId, key, value)));
    }

    RegistrationResult findDevice(final String tenantId, final String key, final String value) {
        final Map<String, JsonObject> devices = identities.get(tenantId);
        if (devices != null) {
            for (Entry<String, JsonObject> entry : devices.entrySet()) {
                if (value.equals(entry.getValue().getString(key))) {
                    return RegistrationResult.from(HTTP_OK, getResultPayload(entry.getKey(), entry.getValue()));
                }
            }
        }
        return RegistrationResult.from(HTTP_NOT_FOUND);
    }

    @Override
    public void removeDevice(final String tenantId, final String deviceId,
            final Handler<AsyncResult<RegistrationResult>> resultHandler) {

        resultHandler.handle(Future.succeededFuture(removeDevice(tenantId, deviceId)));
    }

    RegistrationResult removeDevice(final String tenantId, final String deviceId) {

        if (isModificationEnabled) {
            final Map<String, JsonObject> devices = identities.get(tenantId);
            if (devices != null && devices.containsKey(deviceId)) {
                dirty = true;
                return RegistrationResult.from(HTTP_OK, getResultPayload(deviceId, devices.remove(deviceId)));
            } else {
                return RegistrationResult.from(HTTP_NOT_FOUND);
            }
        } else {
            return RegistrationResult.from(HTTP_FORBIDDEN);
        }
    }

    @Override
    public void addDevice(final String tenantId, final String deviceId, final JsonObject data,
            final Handler<AsyncResult<RegistrationResult>> resultHandler) {

        resultHandler.handle(Future.succeededFuture(addDevice(tenantId, deviceId, data)));
    }

    /**
     * Adds a device to this registry.
     * 
     * @param tenantId The tenant the device belongs to.
     * @param deviceId The ID of the device to add.
     * @param data Additional data to register with the device (may be {@code null}).
     * @return The outcome of the operation indicating success or failure.
     */
    public RegistrationResult addDevice(final String tenantId, final String deviceId, final JsonObject data) {

        JsonObject obj = data != null ? data : new JsonObject().put(FIELD_ENABLED, Boolean.TRUE);
        Map<String, JsonObject> devices = getDevicesForTenant(tenantId);
        if (devices.size() < maxDevicesPerTenant) {
            if (devices.putIfAbsent(deviceId, obj) == null) {
                dirty = true;
                return RegistrationResult.from(HTTP_CREATED);
            } else {
                return RegistrationResult.from(HTTP_CONFLICT);
            }
        } else {
            return RegistrationResult.from(HTTP_FORBIDDEN);
        }
    }

    @Override
    public void updateDevice(final String tenantId, final String deviceId, final JsonObject data,
            final Handler<AsyncResult<RegistrationResult>> resultHandler) {

        resultHandler.handle(Future.succeededFuture(updateDevice(tenantId, deviceId, data)));
    }

    RegistrationResult updateDevice(final String tenantId, final String deviceId, final JsonObject data) {

        if (isModificationEnabled) {
            JsonObject obj = data != null ? data : new JsonObject().put(FIELD_ENABLED, Boolean.TRUE);
            final Map<String, JsonObject> devices = identities.get(tenantId);
            if (devices != null && devices.containsKey(deviceId)) {
                dirty = true;
                return RegistrationResult.from(HTTP_OK, getResultPayload(deviceId, devices.put(deviceId, obj)));
            } else {
                return RegistrationResult.from(HTTP_NOT_FOUND);
            }
        } else {
            return RegistrationResult.from(HTTP_FORBIDDEN);
        }
    }

    private Map<String, JsonObject> getDevicesForTenant(final String tenantId) {
        return identities.computeIfAbsent(tenantId, id -> new ConcurrentHashMap<>());
    }

    /**
     * Removes all devices from the registry.
     */
    public void clear() {
        dirty = true;
        identities.clear();
    }

    @Override
    public String toString() {
        return String.format("%s[filename=%s]", FileBasedRegistrationService.class.getSimpleName(), filename);
    }
}