Java tutorial
/** * Copyright (c) 2018 Contributors to the Eclipse Foundation * * See the NOTICE file(s) distributed with this work for additional * information regarding copyright ownership. * * This program and the accompanying materials are made available under the * terms of the Eclipse Public License 1.0 which is available at * https://www.eclipse.org/legal/epl-v10.html * * SPDX-License-Identifier: EPL-1.0 */ package org.eclipse.hono.deviceregistry; import java.net.HttpURLConnection; import java.util.HashMap; import java.util.Map; import java.util.Objects; import javax.security.auth.x500.X500Principal; import org.eclipse.hono.service.tenant.BaseTenantService; import org.eclipse.hono.util.CacheDirective; import org.eclipse.hono.util.TenantObject; import org.eclipse.hono.util.TenantResult; import org.springframework.beans.factory.annotation.Autowired; 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.json.DecodeException; import io.vertx.core.json.JsonArray; import io.vertx.core.json.JsonObject; /** * A tenant service that keeps all data in memory but is backed by a file. * <p> * On startup this adapter loads all registered tenants from a file. On shutdown all tenants kept in memory are written * to the file. */ @Repository public final class FileBasedTenantService extends BaseTenantService<FileBasedTenantsConfigProperties> { private static final long MAX_AGE_GET_TENANT = 180L; // seconds // <ID, tenant> private final Map<String, TenantObject> tenants = new HashMap<>(); private boolean running = false; private boolean dirty = false; @Autowired @Override public void setConfig(final FileBasedTenantsConfigProperties configuration) { setSpecificConfig(configuration); } @Override protected void doStart(final Future<Void> startFuture) { if (running) { startFuture.complete(); } else { if (!getConfig().isModificationEnabled()) { log.info("modification of registered tenants has been disabled"); } if (getConfig().getFilename() == null) { log.debug("tenant file name is not set, tenant information will not be loaded"); running = true; startFuture.complete(); } else { checkFileExists(getConfig().isSaveToFile()).compose(ok -> { return loadTenantData(); }).compose(s -> { if (getConfig().isSaveToFile()) { log.info("saving tenants to file every 3 seconds"); vertx.setPeriodic(3000, tid -> { saveToFile(); }); } else { log.info("persistence is disabled, will not save tenants to file"); } running = true; startFuture.complete(); }, startFuture); } } } Future<Void> loadTenantData() { if (getConfig().getFilename() == null) { return Future.succeededFuture(); } else { final Future<Buffer> readResult = Future.future(); vertx.fileSystem().readFile(getConfig().getFilename(), readResult.completer()); return readResult.compose(buffer -> { return addAll(buffer); }).recover(t -> { log.debug("cannot load tenants from file [{}]: {}", getConfig().getFilename(), t.getMessage()); return Future.succeededFuture(); }); } } private Future<Void> checkFileExists(final boolean createIfMissing) { final Future<Void> result = Future.future(); if (getConfig().getFilename() == null) { result.fail("no filename set"); } else if (vertx.fileSystem().existsBlocking(getConfig().getFilename())) { result.complete(); } else if (createIfMissing) { vertx.fileSystem().createFile(getConfig().getFilename(), result.completer()); } else { log.debug("no such file [{}]", getConfig().getFilename()); result.complete(); } return result; } private Future<Void> addAll(final Buffer tenantsBuffer) { final Future<Void> result = Future.future(); try { if (tenantsBuffer.length() > 0) { int tenantCount = 0; final JsonArray allObjects = tenantsBuffer.toJsonArray(); for (final Object obj : allObjects) { if (JsonObject.class.isInstance(obj)) { tenantCount++; addTenant((JsonObject) obj); } } log.info("successfully loaded {} tenants from file [{}]", tenantCount, getConfig().getFilename()); } result.complete(); } catch (final DecodeException e) { log.warn("cannot read malformed JSON from tenants file [{}]", getConfig().getFilename()); result.fail(e); } return result; } private void addTenant(final JsonObject tenant) { try { final TenantObject tenantObject = tenant.mapTo(TenantObject.class); log.debug("loading tenant [{}]", tenantObject.getTenantId()); tenants.put(tenantObject.getTenantId(), tenantObject); } catch (IllegalArgumentException e) { log.warn("cannot deserialize tenant", e); } } @Override protected void doStop(final Future<Void> stopFuture) { if (running) { saveToFile().compose(s -> { running = false; stopFuture.complete(); }, stopFuture); } else { stopFuture.complete(); } } Future<Void> saveToFile() { if (!getConfig().isSaveToFile()) { return Future.succeededFuture(); } else if (dirty) { return checkFileExists(true).compose(s -> { final JsonArray tenantsJson = new JsonArray(); tenants.values().stream().forEach(tenant -> { tenantsJson.add(JsonObject.mapFrom(tenant)); }); final Future<Void> writeHandler = Future.future(); vertx.fileSystem().writeFile(getConfig().getFilename(), Buffer.factory.buffer(tenantsJson.encodePrettily()), writeHandler.completer()); return writeHandler.map(ok -> { dirty = false; log.trace("successfully wrote {} tenants to file {}", tenantsJson.size(), getConfig().getFilename()); return (Void) null; }).otherwise(t -> { log.warn("could not write tenants to file {}", getConfig().getFilename(), t); return (Void) null; }); }); } else { log.trace("tenants registry does not need to be persisted"); return Future.succeededFuture(); } } @Override public void get(final String tenantId, final Handler<AsyncResult<TenantResult<JsonObject>>> resultHandler) { Objects.requireNonNull(tenantId); Objects.requireNonNull(resultHandler); resultHandler.handle(Future.succeededFuture(getTenantResult(tenantId))); } TenantResult<JsonObject> getTenantResult(final String tenantId) { final TenantObject tenant = tenants.get(tenantId); if (tenant == null) { return TenantResult.from(HttpURLConnection.HTTP_NOT_FOUND); } else { return TenantResult.from(HttpURLConnection.HTTP_OK, JsonObject.mapFrom(tenant), CacheDirective.maxAgeDirective(MAX_AGE_GET_TENANT)); } } @Override public void get(final X500Principal subjectDn, final Handler<AsyncResult<TenantResult<JsonObject>>> resultHandler) { Objects.requireNonNull(subjectDn); Objects.requireNonNull(resultHandler); resultHandler.handle(Future.succeededFuture(getForCertificateAuthority(subjectDn))); } private TenantResult<JsonObject> getForCertificateAuthority(final X500Principal subjectDn) { if (subjectDn == null) { return TenantResult.from(HttpURLConnection.HTTP_BAD_REQUEST); } else { final TenantObject tenant = getByCa(subjectDn); if (tenant == null) { return TenantResult.from(HttpURLConnection.HTTP_NOT_FOUND); } else { return TenantResult.from(HttpURLConnection.HTTP_OK, JsonObject.mapFrom(tenant), CacheDirective.maxAgeDirective(MAX_AGE_GET_TENANT)); } } } @Override public void remove(final String tenantId, final Handler<AsyncResult<TenantResult<JsonObject>>> resultHandler) { Objects.requireNonNull(tenantId); Objects.requireNonNull(resultHandler); resultHandler.handle(Future.succeededFuture(removeTenant(tenantId))); } TenantResult<JsonObject> removeTenant(final String tenantId) { Objects.requireNonNull(tenantId); if (getConfig().isModificationEnabled()) { if (tenants.remove(tenantId) != null) { dirty = true; return TenantResult.from(HttpURLConnection.HTTP_NO_CONTENT); } else { return TenantResult.from(HttpURLConnection.HTTP_NOT_FOUND); } } else { return TenantResult.from(HttpURLConnection.HTTP_FORBIDDEN); } } @Override public void add(final String tenantId, final JsonObject tenantSpec, final Handler<AsyncResult<TenantResult<JsonObject>>> resultHandler) { Objects.requireNonNull(tenantId); Objects.requireNonNull(tenantSpec); Objects.requireNonNull(resultHandler); resultHandler.handle(Future.succeededFuture(add(tenantId, tenantSpec))); } /** * Adds a tenant. * * @param tenantId The identifier of the tenant. * @param tenantSpec The information to register for the tenant. * @return The outcome of the operation indicating success or failure. * @throws NullPointerException if any of the parameters are {@code null}. */ public TenantResult<JsonObject> add(final String tenantId, final JsonObject tenantSpec) { Objects.requireNonNull(tenantId); Objects.requireNonNull(tenantSpec); if (tenants.containsKey(tenantId)) { return TenantResult.from(HttpURLConnection.HTTP_CONFLICT); } else { try { final TenantObject tenant = tenantSpec.mapTo(TenantObject.class); tenant.setTenantId(tenantId); final TenantObject conflictingTenant = getByCa(tenant.getTrustedCaSubjectDn()); if (conflictingTenant != null) { // we are trying to use the same CA as an already existing tenant return TenantResult.from(HttpURLConnection.HTTP_CONFLICT); } else { tenants.put(tenantId, tenant); dirty = true; return TenantResult.from(HttpURLConnection.HTTP_CREATED); } } catch (IllegalArgumentException e) { return TenantResult.from(HttpURLConnection.HTTP_BAD_REQUEST); } } } /** * Updates the tenant information. * * @param tenantId The tenant to update * @param tenantSpec The new tenant information * @param resultHandler The handler receiving the result of the operation. * * @throws NullPointerException if either of the input parameters is {@code null}. */ @Override public void update(final String tenantId, final JsonObject tenantSpec, final Handler<AsyncResult<TenantResult<JsonObject>>> resultHandler) { Objects.requireNonNull(tenantId); Objects.requireNonNull(tenantSpec); Objects.requireNonNull(resultHandler); resultHandler.handle(Future.succeededFuture(update(tenantId, tenantSpec))); } /** * Updates a tenant. * * @param tenantId The identifier of the tenant. * @param tenantSpec The information to update the tenant with. * @return The outcome of the operation indicating success or failure. * @throws NullPointerException if any of the parameters are {@code null}. */ public TenantResult<JsonObject> update(final String tenantId, final JsonObject tenantSpec) { Objects.requireNonNull(tenantId); Objects.requireNonNull(tenantSpec); if (getConfig().isModificationEnabled()) { if (tenants.containsKey(tenantId)) { try { final TenantObject tenant = tenantSpec.mapTo(TenantObject.class); tenant.setTenantId(tenantId); final TenantObject conflictingTenant = getByCa(tenant.getTrustedCaSubjectDn()); if (conflictingTenant != null && !tenantId.equals(conflictingTenant.getTenantId())) { // we are trying to use the same CA as another tenant return TenantResult.from(HttpURLConnection.HTTP_CONFLICT); } else { tenants.put(tenantId, tenant); dirty = true; return TenantResult.from(HttpURLConnection.HTTP_NO_CONTENT); } } catch (IllegalArgumentException e) { return TenantResult.from(HttpURLConnection.HTTP_BAD_REQUEST); } } else { return TenantResult.from(HttpURLConnection.HTTP_NOT_FOUND); } } else { return TenantResult.from(HttpURLConnection.HTTP_FORBIDDEN); } } private TenantObject getByCa(final X500Principal subjectDn) { if (subjectDn == null) { return null; } else { return tenants.values().stream().filter(t -> subjectDn.equals(t.getTrustedCaSubjectDn())).findFirst() .orElse(null); } } /** * Removes all devices from the tenant registry. */ public void clear() { tenants.clear(); dirty = true; } @Override public String toString() { return String.format("%s[filename=%s]", FileBasedTenantService.class.getSimpleName(), getConfig().getFilename()); } }