diff --git a/ext/hivemq-edge-openapi-2025.14.yaml b/ext/hivemq-edge-openapi-2025.14.yaml index bd3e2fb118..e26a7b021e 100644 --- a/ext/hivemq-edge-openapi-2025.14.yaml +++ b/ext/hivemq-edge-openapi-2025.14.yaml @@ -5103,6 +5103,18 @@ components: $ref: '#/components/schemas/Link' required: - items + ConfidentialityAgreement: + type: object + description: A list of resources to render + properties: + enabled: + type: boolean + description: Whether the CA should be shown prior to login + content: + type: string + description: The text of the CA + required: + - enabled EnvironmentProperties: type: object description: A map of properties relating to the installation @@ -5256,6 +5268,8 @@ components: trackingAllowed: type: boolean description: Is the tracking of user actions allowed. + confidentialityAgreement: + $ref: '#/components/schemas/ConfidentialityAgreement' Notification: type: object description: List of result items that are returned by this endpoint diff --git a/ext/openAPI/components/schemas/ConfidentialityAgreement.yaml b/ext/openAPI/components/schemas/ConfidentialityAgreement.yaml new file mode 100644 index 0000000000..f781912941 --- /dev/null +++ b/ext/openAPI/components/schemas/ConfidentialityAgreement.yaml @@ -0,0 +1,11 @@ +type: object +description: A list of resources to render +properties: + enabled: + type: boolean + description: Whether the CA should be shown prior to login + content: + type: string + description: The text of the CA +required: + - enabled diff --git a/ext/openAPI/components/schemas/GatewayConfiguration.yaml b/ext/openAPI/components/schemas/GatewayConfiguration.yaml index 23327287f9..0be92d6d06 100644 --- a/ext/openAPI/components/schemas/GatewayConfiguration.yaml +++ b/ext/openAPI/components/schemas/GatewayConfiguration.yaml @@ -24,3 +24,5 @@ properties: trackingAllowed: type: boolean description: Is the tracking of user actions allowed. + confidentialityAgreement: + $ref: ./ConfidentialityAgreement.yaml diff --git a/hivemq-edge/src/main/java/com/hivemq/api/model/components/ConfidentialityAgreement.java b/hivemq-edge/src/main/java/com/hivemq/api/model/components/ConfidentialityAgreement.java new file mode 100644 index 0000000000..0ad9c9c710 --- /dev/null +++ b/hivemq-edge/src/main/java/com/hivemq/api/model/components/ConfidentialityAgreement.java @@ -0,0 +1,41 @@ +package com.hivemq.api.model.components; + +import com.fasterxml.jackson.annotation.JsonCreator; +import com.fasterxml.jackson.annotation.JsonProperty; +import io.swagger.v3.oas.annotations.media.Schema; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; + +import static java.util.Objects.requireNonNullElse; + +public class ConfidentialityAgreement { + + @JsonProperty("enabled") + @Schema(description = "Whether the confidentiality agreement should be shown prior to login in") + private final @NotNull Boolean enabled; + + @JsonProperty("content") + @Schema(description = "The confidentiality agreement") + private final @Nullable String content; + + public ConfidentialityAgreement() { + this(false, null); + } + + @JsonCreator(mode = JsonCreator.Mode.PROPERTIES) + public ConfidentialityAgreement(final @Nullable Boolean enabled, final @Nullable String content) { + this.enabled = requireNonNullElse(enabled, false); + if (this.enabled && (content == null || content.isEmpty())) { + throw new IllegalArgumentException("Content cannot be null or empty when enabled"); + } + this.content = content; + } + + public boolean getEnabled() { + return enabled; + } + + public @Nullable String getContent() { + return content; + } +} diff --git a/hivemq-edge/src/main/java/com/hivemq/api/model/components/GatewayConfiguration.java b/hivemq-edge/src/main/java/com/hivemq/api/model/components/GatewayConfiguration.java index 78d920c025..bb61576356 100644 --- a/hivemq-edge/src/main/java/com/hivemq/api/model/components/GatewayConfiguration.java +++ b/hivemq-edge/src/main/java/com/hivemq/api/model/components/GatewayConfiguration.java @@ -18,57 +18,58 @@ import com.fasterxml.jackson.annotation.JsonCreator; import com.fasterxml.jackson.annotation.JsonProperty; import com.hivemq.api.model.firstuse.FirstUseInformation; -import org.jetbrains.annotations.NotNull; import io.swagger.v3.oas.annotations.media.Schema; +import org.jetbrains.annotations.NotNull; -/** - * @author Simon L Johnson - */ public class GatewayConfiguration { @JsonProperty("environment") @Schema(description = "A map of properties relating to the installation", nullable = true) - private @NotNull EnvironmentProperties environment; + private final @NotNull EnvironmentProperties environment; @JsonProperty("cloudLink") @Schema(description = "A referral link to HiveMQ Cloud") - private @NotNull Link cloudLink; + private final @NotNull Link cloudLink; @JsonProperty("gitHubLink") @Schema(description = "A link to the GitHub Project") - private @NotNull Link gitHubLink; + private final @NotNull Link gitHubLink; @JsonProperty("documentationLink") @Schema(description = "A link to the documentation Project") - private @NotNull Link documentationLink; + private final @NotNull Link documentationLink; @JsonProperty("firstUseInformation") @Schema(description = "Information relating to the firstuse experience") - private @NotNull FirstUseInformation firstUseInformation; + private final @NotNull FirstUseInformation firstUseInformation; @JsonProperty("ctas") @Schema(description = "The calls main to action") - private @NotNull LinkList ctas; + private final @NotNull LinkList ctas; @JsonProperty("resources") @Schema(description = "A list of resources to render") - private @NotNull LinkList resources; + private final @NotNull LinkList resources; @JsonProperty("modules") @Schema(description = "The modules available for installation") - private @NotNull ModuleList modules; + private final @NotNull ModuleList modules; @JsonProperty("extensions") @Schema(description = "The extensions available for installation") - private @NotNull ExtensionList extensions; + private final @NotNull ExtensionList extensions; @JsonProperty("hivemqId") @Schema(description = "The current id of hivemq edge. Changes at restart.") - private @NotNull String hivemqId; + private final @NotNull String hivemqId; @JsonProperty("trackingAllowed") @Schema(description = "Is the tracking of user actions allowed.") - private boolean trackingAllowed; + private final boolean trackingAllowed; + + @JsonProperty("confidentialityAgreement") + @Schema(description = "CA configuration") + private final @NotNull ConfidentialityAgreement confidentialityAgreement; @JsonCreator(mode = JsonCreator.Mode.PROPERTIES) public GatewayConfiguration( @@ -82,7 +83,8 @@ public GatewayConfiguration( @JsonProperty("modules") final @NotNull ModuleList modules, @JsonProperty("extensions") final @NotNull ExtensionList extensions, @JsonProperty("hivemqId") final @NotNull String hivemqId, - @JsonProperty("trackingAllowed")final boolean trackingAllowed) { + @JsonProperty("trackingAllowed") final boolean trackingAllowed, + @JsonProperty("confidentialityAgreement") final ConfidentialityAgreement confidentialityAgreement) { this.environment = environment; this.cloudLink = cloudLink; this.gitHubLink = gitHubLink; @@ -94,6 +96,7 @@ public GatewayConfiguration( this.extensions = extensions; this.hivemqId = hivemqId; this.trackingAllowed = trackingAllowed; + this.confidentialityAgreement = confidentialityAgreement; } public @NotNull EnvironmentProperties getEnvironment() { @@ -131,4 +134,16 @@ public GatewayConfiguration( public @NotNull ExtensionList getExtensions() { return extensions; } + + public @NotNull String getHivemqId() { + return hivemqId; + } + + public boolean isTrackingAllowed() { + return trackingAllowed; + } + + public @NotNull ConfidentialityAgreement getConfidentialityAgreement() { + return confidentialityAgreement; + } } diff --git a/hivemq-edge/src/main/java/com/hivemq/api/resources/impl/FrontendResourceImpl.java b/hivemq-edge/src/main/java/com/hivemq/api/resources/impl/FrontendResourceImpl.java index f2c9b5261c..d64a7104ee 100644 --- a/hivemq-edge/src/main/java/com/hivemq/api/resources/impl/FrontendResourceImpl.java +++ b/hivemq-edge/src/main/java/com/hivemq/api/resources/impl/FrontendResourceImpl.java @@ -17,6 +17,7 @@ import com.google.common.collect.ImmutableList; import com.hivemq.api.AbstractApi; +import com.hivemq.api.model.components.ConfidentialityAgreement; import com.hivemq.api.model.components.EnvironmentProperties; import com.hivemq.api.model.components.ExtensionList; import com.hivemq.api.model.components.GatewayConfiguration; @@ -32,7 +33,6 @@ import com.hivemq.configuration.info.SystemInformation; import com.hivemq.configuration.service.ConfigurationService; import com.hivemq.edge.HiveMQCapabilityService; -import com.hivemq.edge.HiveMQEdgeConstants; import com.hivemq.edge.HiveMQEdgeRemoteService; import com.hivemq.edge.ModulesAndExtensionsService; import com.hivemq.edge.api.FrontendApi; @@ -40,19 +40,18 @@ import com.hivemq.edge.api.model.CapabilityList; import com.hivemq.http.core.UsernamePasswordRoles; import com.hivemq.protocols.ProtocolAdapterManager; -import org.jetbrains.annotations.NotNull; - import jakarta.inject.Inject; import jakarta.ws.rs.core.Response; -import java.util.HashMap; +import org.jetbrains.annotations.NotNull; + import java.util.List; import java.util.Map; import java.util.Optional; -import java.util.stream.Collectors; -/** - * @author Simon L Johnson - */ +import static com.hivemq.edge.HiveMQEdgeConstants.CONFIGURATION_EXPORT_ENABLED; +import static com.hivemq.edge.HiveMQEdgeConstants.MUTABLE_CONFIGURAION_ENABLED; +import static com.hivemq.edge.HiveMQEdgeConstants.VERSION_PROPERTY; + public class FrontendResourceImpl extends AbstractApi implements FrontendApi { private final @NotNull ConfigurationService configurationService; @@ -81,10 +80,17 @@ public FrontendResourceImpl( this.hivemqId = hivemqId; } + private static @NotNull Capability fromModel(final @NotNull com.hivemq.api.model.capabilities.Capability cap) { + return Capability.builder() + .id(Capability.IdEnum.fromString(cap.getId())) + .description(cap.getDescription()) + .displayName(cap.getDisplayName()) + .build(); + } + @Override public @NotNull Response getConfiguration() { - - final GatewayConfiguration configuration = new GatewayConfiguration(getEnvironmentProperties(), + return Response.ok(new GatewayConfiguration(getEnvironmentProperties(), getCloudLink(), getGitHubLink(), getDocumentationLink(), @@ -94,13 +100,48 @@ public FrontendResourceImpl( getModules(), getExtensions(), hivemqId.get(), - configurationService.usageTrackingConfiguration().isUsageTrackingEnabled()); - return Response.ok(configuration).build(); + configurationService.usageTrackingConfiguration().isUsageTrackingEnabled(), + configurationService.apiConfiguration().getConfidentialityAgreement())).build(); } + @Override + public @NotNull Response getNotifications() { + final ImmutableList.Builder<@NotNull Notification> notifs = new ImmutableList.Builder<>(); + final Optional lastUpdate = configurationService.getLastUpdateTime(); + if (!configurationService.gatewayConfiguration().isMutableConfigurationEnabled() && + configurationService.gatewayConfiguration().isConfigurationExportEnabled() && + lastUpdate.isPresent() && + lastUpdate.get() > System.currentTimeMillis() - (60000 * 5)) { + notifs.add(new Notification(Notification.LEVEL.NOTICE, + "Configuration Has Changed", + "The gateway configuration has recently been modify. In order to persist these changes across runtimes, please export your configuration for use in your containers.", + new Link("Download XML Configuration", + "/configuration-download", + null, + null, + null, + Boolean.FALSE))); + } + if (ApiUtils.hasDefaultUser(configurationService.apiConfiguration().getUserList())) { + notifs.add(new Notification(Notification.LEVEL.WARNING, + "Default Credentials Need Changing!", + "Your gateway access is configured to use the default username/password combination. This is a security risk. Please ensure you modify your access credentials in your config.xml file.", + null)); + } + return Response.ok(new NotificationList(notifs.build())).build(); + } + + @Override + public @NotNull Response getCapabilities() { + return Response.ok(new CapabilityList(capabilityService.getList() + .getItems() + .stream() + .map(FrontendResourceImpl::fromModel) + .toList())).build(); + } - public @NotNull LinkList getDashboardCTAs() { - final ImmutableList.Builder links = new ImmutableList.Builder().add(new Link("Connect My First Device", + private @NotNull LinkList getDashboardCTAs() { + return new LinkList(List.of(new Link("Connect My First Device", "./protocol-adapters?from=dashboard-cta", LoremIpsum.generate(40), null, @@ -117,35 +158,38 @@ public FrontendResourceImpl( LoremIpsum.generate(40), null, null, - Boolean.FALSE)); - return new LinkList(links.build()); + Boolean.FALSE))); + } + + private @NotNull ConfidentialityAgreement getConfidentialityAgreement() { + return configurationService.apiConfiguration().getConfidentialityAgreement(); } - protected @NotNull Link getCloudLink() { + private @NotNull Link getCloudLink() { return hiveMQEdgeRemoteConfigurationService.getConfiguration().getCloudLink(); } - protected @NotNull Link getGitHubLink() { + private @NotNull Link getGitHubLink() { return hiveMQEdgeRemoteConfigurationService.getConfiguration().getGitHubLink(); } - protected @NotNull Link getDocumentationLink() { + private @NotNull Link getDocumentationLink() { return hiveMQEdgeRemoteConfigurationService.getConfiguration().getDocumentationLink(); } - protected @NotNull LinkList getResources() { + private @NotNull LinkList getResources() { return new LinkList(hiveMQEdgeRemoteConfigurationService.getConfiguration().getResources()); } - protected @NotNull ExtensionList getExtensions() { + private @NotNull ExtensionList getExtensions() { return new ExtensionList(modulesAndExtensionsService.getExtensions()); } - protected @NotNull ModuleList getModules() { + private @NotNull ModuleList getModules() { return new ModuleList(modulesAndExtensionsService.getModules()); } - protected @NotNull FirstUseInformation getFirstUse() { + private @NotNull FirstUseInformation getFirstUse() { //-- First use is determined by zero configuration final boolean firstUse = configurationService.bridgeExtractor().getBridges().isEmpty() && protocolAdapterManager.getProtocolAdapters().isEmpty(); @@ -164,54 +208,12 @@ public FrontendResourceImpl( return new FirstUseInformation(firstUse, prefillUsername, prefillPassword, firstUseTitle, firstUseDescription); } - @Override - public @NotNull Response getNotifications() { - final ImmutableList.Builder notifs = new ImmutableList.Builder<>(); - final Optional lastUpdate = configurationService.getLastUpdateTime(); - if (!configurationService.gatewayConfiguration().isMutableConfigurationEnabled() && - configurationService.gatewayConfiguration().isConfigurationExportEnabled() && - lastUpdate.isPresent() && - lastUpdate.get() > System.currentTimeMillis() - (60000 * 5)) { - final Link xmlDownload = - new Link("Download XML Configuration", "/configuration-download", null, null, null, Boolean.FALSE); - notifs.add(new Notification(Notification.LEVEL.NOTICE, - "Configuration Has Changed", - "The gateway configuration has recently been modify. In order to persist these changes across runtimes, please export your configuration for use in your containers.", - xmlDownload)); - } - if (ApiUtils.hasDefaultUser(configurationService.apiConfiguration().getUserList())) { - notifs.add(new Notification(Notification.LEVEL.WARNING, - "Default Credentials Need Changing!", - "Your gateway access is configured to use the default username/password combination. This is a security risk. Please ensure you modify your access credentials in your config.xml file.", - null)); - } - return Response.ok(new NotificationList(notifs.build())).build(); - } - - @Override - public @NotNull Response getCapabilities() { - final List capabilities = capabilityService.getList() - .getItems() - .stream() - .map(cap -> (Capability) Capability.builder() - .id(Capability.IdEnum.fromString(cap.getId())) - .description(cap.getDescription()) - .displayName(cap.getDisplayName()) - .build()).collect(Collectors.toList()); - - return Response.ok(CapabilityList.builder().items(capabilities).build()).build(); - } - - - protected @NotNull EnvironmentProperties getEnvironmentProperties() { - final Map env = new HashMap<>(); - env.put(HiveMQEdgeConstants.VERSION_PROPERTY, systemInformation.getHiveMQVersion()); - env.put(HiveMQEdgeConstants.MUTABLE_CONFIGURAION_ENABLED, - String.valueOf(configurationService.gatewayConfiguration().isMutableConfigurationEnabled())); - env.put(HiveMQEdgeConstants.CONFIGURATION_EXPORT_ENABLED, - String.valueOf(configurationService.gatewayConfiguration().isConfigurationExportEnabled())); - return new EnvironmentProperties(env); + private @NotNull EnvironmentProperties getEnvironmentProperties() { + return new EnvironmentProperties(Map.of(VERSION_PROPERTY, + systemInformation.getHiveMQVersion(), + MUTABLE_CONFIGURAION_ENABLED, + String.valueOf(configurationService.gatewayConfiguration().isMutableConfigurationEnabled()), + CONFIGURATION_EXPORT_ENABLED, + String.valueOf(configurationService.gatewayConfiguration().isConfigurationExportEnabled()))); } - - } diff --git a/hivemq-edge/src/main/java/com/hivemq/configuration/ConfigurationBootstrap.java b/hivemq-edge/src/main/java/com/hivemq/configuration/ConfigurationBootstrap.java index 8cca90be26..a5593f76a1 100644 --- a/hivemq-edge/src/main/java/com/hivemq/configuration/ConfigurationBootstrap.java +++ b/hivemq-edge/src/main/java/com/hivemq/configuration/ConfigurationBootstrap.java @@ -47,9 +47,6 @@ import java.util.List; -/** - * @author Christoph Schäbel - */ public class ConfigurationBootstrap { public static @NotNull ConfigurationService bootstrapConfig(final @NotNull SystemInformation systemInformation) { @@ -94,5 +91,4 @@ public class ConfigurationBootstrap { configurationService.setConfigFileReaderWriter(configFileReader); return configurationService; } - } diff --git a/hivemq-edge/src/main/java/com/hivemq/configuration/entity/EnabledEntity.java b/hivemq-edge/src/main/java/com/hivemq/configuration/entity/EnabledEntity.java index b0fe5ae54e..61a1593346 100644 --- a/hivemq-edge/src/main/java/com/hivemq/configuration/entity/EnabledEntity.java +++ b/hivemq-edge/src/main/java/com/hivemq/configuration/entity/EnabledEntity.java @@ -18,12 +18,10 @@ import jakarta.xml.bind.annotation.XmlAccessType; import jakarta.xml.bind.annotation.XmlAccessorType; import jakarta.xml.bind.annotation.XmlElement; +import org.jetbrains.annotations.Nullable; + import java.util.Objects; -/** - * @author Florian Limpöck - * @since 4.0.0 - */ @XmlAccessorType(XmlAccessType.NONE) @SuppressWarnings({"FieldMayBeFinal", "FieldCanBeLocal"}) public abstract class EnabledEntity { @@ -40,15 +38,18 @@ public void setEnabled(final boolean enabled) { } @Override - public boolean equals(final Object o) { - if (this == o) return true; - if (o == null || getClass() != o.getClass()) return false; - final EnabledEntity that = (EnabledEntity) o; - return isEnabled() == that.isEnabled(); + public boolean equals(final @Nullable Object o) { + if (this == o) { + return true; + } + if (o instanceof final EnabledEntity that) { + return enabled == that.enabled; + } + return false; } @Override public int hashCode() { - return Objects.hashCode(isEnabled()); + return Objects.hashCode(enabled); } } diff --git a/hivemq-edge/src/main/java/com/hivemq/configuration/entity/api/AdminApiEntity.java b/hivemq-edge/src/main/java/com/hivemq/configuration/entity/api/AdminApiEntity.java index ab4c7488dc..ee38c1c6cc 100644 --- a/hivemq-edge/src/main/java/com/hivemq/configuration/entity/api/AdminApiEntity.java +++ b/hivemq-edge/src/main/java/com/hivemq/configuration/entity/api/AdminApiEntity.java @@ -16,21 +16,19 @@ package com.hivemq.configuration.entity.api; import com.hivemq.configuration.entity.EnabledEntity; -import org.jetbrains.annotations.NotNull; - import jakarta.xml.bind.annotation.XmlAccessType; import jakarta.xml.bind.annotation.XmlAccessorType; import jakarta.xml.bind.annotation.XmlElementRef; import jakarta.xml.bind.annotation.XmlElementRefs; import jakarta.xml.bind.annotation.XmlElementWrapper; import jakarta.xml.bind.annotation.XmlRootElement; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; + import java.util.ArrayList; import java.util.List; import java.util.Objects; -/** - * @author Simon L Johnson - */ @XmlRootElement(name = "admin-api") @XmlAccessorType(XmlAccessType.NONE) @SuppressWarnings({"FieldMayBeFinal", "FieldCanBeLocal"}) @@ -40,48 +38,68 @@ public class AdminApiEntity extends EnabledEntity { @XmlElementRefs({ @XmlElementRef(required = false, type = HttpListenerEntity.class), @XmlElementRef(required = false, type = HttpsListenerEntity.class)}) - private @NotNull List listeners = new ArrayList<>(); + private @NotNull List listeners; @XmlElementRef(required = false) - private @NotNull ApiTlsEntity tls; + private @NotNull ApiJwsEntity jws; + @XmlElementWrapper(name = "users") @XmlElementRef(required = false) - private @NotNull ApiJwsEntity jws = new ApiJwsEntity(); + private @NotNull List users; - @XmlElementWrapper(name = "users") @XmlElementRef(required = false) - private @NotNull List users = new ArrayList<>(); + private @Nullable ApiTlsEntity tls; + + @XmlElementRef(required = false) + private @NotNull CAEntity confidentialityAgreement; + + public AdminApiEntity() { + this.listeners = new ArrayList<>(); + this.jws = new ApiJwsEntity(); + this.users = new ArrayList<>(); + this.confidentialityAgreement = new CAEntity(); + } public @NotNull List getListeners() { return listeners; } - public ApiJwsEntity getJws() { + public @NotNull ApiJwsEntity getJws() { return jws; } - public List getUsers() { + public @NotNull List getUsers() { return users; } - public ApiTlsEntity getTls() { + public @Nullable ApiTlsEntity getTls() { return tls; } + public @NotNull CAEntity getConfidentialityAgreement() { + return confidentialityAgreement; + } + @Override - public boolean equals(final Object o) { - if (this == o) return true; - if (o == null || getClass() != o.getClass()) return false; - if (!super.equals(o)) return false; - final AdminApiEntity that = (AdminApiEntity) o; - return Objects.equals(getListeners(), that.getListeners()) && - Objects.equals(getTls(), that.getTls()) && - Objects.equals(getJws(), that.getJws()) && - Objects.equals(getUsers(), that.getUsers()); + public boolean equals(final @Nullable Object o) { + if (this == o) { + return true; + } + if (o instanceof final AdminApiEntity that) { + if (!super.equals(o)) { + return false; + } + return Objects.equals(listeners, that.listeners) && + Objects.equals(tls, that.tls) && + Objects.equals(jws, that.jws) && + Objects.equals(users, that.users) && + Objects.equals(confidentialityAgreement, that.confidentialityAgreement); + } + return false; } @Override public int hashCode() { - return Objects.hash(super.hashCode(), getListeners(), getTls(), getJws(), getUsers()); + return Objects.hash(super.hashCode(), listeners, tls, jws, users, confidentialityAgreement); } } diff --git a/hivemq-edge/src/main/java/com/hivemq/configuration/entity/api/ApiJwsEntity.java b/hivemq-edge/src/main/java/com/hivemq/configuration/entity/api/ApiJwsEntity.java index 98262b2835..79847375af 100644 --- a/hivemq-edge/src/main/java/com/hivemq/configuration/entity/api/ApiJwsEntity.java +++ b/hivemq-edge/src/main/java/com/hivemq/configuration/entity/api/ApiJwsEntity.java @@ -15,14 +15,15 @@ */ package com.hivemq.configuration.entity.api; +import jakarta.xml.bind.annotation.XmlAccessType; +import jakarta.xml.bind.annotation.XmlAccessorType; +import jakarta.xml.bind.annotation.XmlElement; +import jakarta.xml.bind.annotation.XmlRootElement; import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; -import jakarta.xml.bind.annotation.*; import java.util.Objects; -/** - * @author Simon L Johnson - */ @XmlRootElement(name = "generated-tokens") @XmlAccessorType(XmlAccessType.NONE) @SuppressWarnings({"FieldMayBeFinal", "FieldCanBeLocal"}) @@ -30,12 +31,16 @@ public class ApiJwsEntity { @XmlElement(name = "keySize", required = true) private int keySize = 2048; + @XmlElement(name = "issuer", required = true, defaultValue = "HiveMQ-Edge") private @NotNull String issuer = "HiveMQ-Edge"; + @XmlElement(name = "audience", required = true, defaultValue = "HiveMQ-Edge-Api") private @NotNull String audience = "HiveMQ-Edge-Api"; + @XmlElement(name = "expiryTimeMinutes", required = true, defaultValue = "30") private int expiryTimeMinutes = 30; + @XmlElement(name = "tokenEarlyEpochThresholdMinutes", required = true, defaultValue = "2") private int tokenEarlyEpochThresholdMinutes = 2; @@ -43,11 +48,11 @@ public int getKeySize() { return keySize; } - public String getIssuer() { + public @NotNull String getIssuer() { return issuer; } - public String getAudience() { + public @NotNull String getAudience() { return audience; } @@ -60,23 +65,23 @@ public int getTokenEarlyEpochThresholdMinutes() { } @Override - public boolean equals(final Object o) { - if (this == o) return true; - if (o == null || getClass() != o.getClass()) return false; - final ApiJwsEntity that = (ApiJwsEntity) o; - return getKeySize() == that.getKeySize() && - getExpiryTimeMinutes() == that.getExpiryTimeMinutes() && - getTokenEarlyEpochThresholdMinutes() == that.getTokenEarlyEpochThresholdMinutes() && - Objects.equals(getIssuer(), that.getIssuer()) && - Objects.equals(getAudience(), that.getAudience()); + public boolean equals(final @Nullable Object o) { + if (this == o) { + return true; + } + if (o instanceof final ApiJwsEntity that) { + return keySize == that.keySize && + expiryTimeMinutes == that.expiryTimeMinutes && + tokenEarlyEpochThresholdMinutes == that.tokenEarlyEpochThresholdMinutes && + Objects.equals(issuer, that.issuer) && + Objects.equals(audience, that.audience); + } + return false; + } @Override public int hashCode() { - return Objects.hash(getKeySize(), - getIssuer(), - getAudience(), - getExpiryTimeMinutes(), - getTokenEarlyEpochThresholdMinutes()); + return Objects.hash(keySize, issuer, audience, expiryTimeMinutes, tokenEarlyEpochThresholdMinutes); } } diff --git a/hivemq-edge/src/main/java/com/hivemq/configuration/entity/api/ApiTlsEntity.java b/hivemq-edge/src/main/java/com/hivemq/configuration/entity/api/ApiTlsEntity.java index 573d44feca..e5ec5e612c 100644 --- a/hivemq-edge/src/main/java/com/hivemq/configuration/entity/api/ApiTlsEntity.java +++ b/hivemq-edge/src/main/java/com/hivemq/configuration/entity/api/ApiTlsEntity.java @@ -16,37 +16,37 @@ package com.hivemq.configuration.entity.api; import com.hivemq.configuration.entity.listener.tls.KeystoreEntity; -import org.jetbrains.annotations.NotNull; -import org.jetbrains.annotations.Nullable; - import jakarta.xml.bind.annotation.XmlAccessType; import jakarta.xml.bind.annotation.XmlAccessorType; import jakarta.xml.bind.annotation.XmlElement; import jakarta.xml.bind.annotation.XmlElementRef; import jakarta.xml.bind.annotation.XmlElementWrapper; import jakarta.xml.bind.annotation.XmlRootElement; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; + import java.util.ArrayList; import java.util.List; import java.util.Objects; -/** - * @author Simon L Johnson - */ @XmlRootElement(name = "tls") @XmlAccessorType(XmlAccessType.NONE) @SuppressWarnings({"FieldMayBeFinal", "FieldCanBeLocal"}) public class ApiTlsEntity { - @XmlElementRef - private @Nullable KeystoreEntity keystoreEntity; - @XmlElementWrapper(name = "protocols") @XmlElement(name = "protocol") - private @NotNull List protocols = new ArrayList<>(); - + private final @NotNull List protocols; @XmlElementWrapper(name = "cipher-suites") @XmlElement(name = "cipher-suite") - private @NotNull List cipherSuites = new ArrayList<>(); + private final @NotNull List cipherSuites; + @XmlElementRef + private @Nullable KeystoreEntity keystoreEntity; + + public ApiTlsEntity() { + protocols = new ArrayList<>(); + cipherSuites = new ArrayList<>(); + } public @Nullable KeystoreEntity getKeystoreEntity() { return keystoreEntity; @@ -61,17 +61,20 @@ public class ApiTlsEntity { } @Override - public boolean equals(final Object o) { - if (this == o) return true; - if (o == null || getClass() != o.getClass()) return false; - final ApiTlsEntity that = (ApiTlsEntity) o; - return Objects.equals(getKeystoreEntity(), that.getKeystoreEntity()) && - Objects.equals(getProtocols(), that.getProtocols()) && - Objects.equals(getCipherSuites(), that.getCipherSuites()); + public boolean equals(final @Nullable Object o) { + if (this == o) { + return true; + } + if (o instanceof final ApiTlsEntity that) { + return Objects.equals(keystoreEntity, that.keystoreEntity) && + Objects.equals(protocols, that.protocols) && + Objects.equals(cipherSuites, that.cipherSuites); + } + return false; } @Override public int hashCode() { - return Objects.hash(getKeystoreEntity(), getProtocols(), getCipherSuites()); + return Objects.hash(keystoreEntity, protocols, cipherSuites); } } diff --git a/hivemq-edge/src/main/java/com/hivemq/configuration/entity/api/CAEntity.java b/hivemq-edge/src/main/java/com/hivemq/configuration/entity/api/CAEntity.java new file mode 100644 index 0000000000..2e4c49091b --- /dev/null +++ b/hivemq-edge/src/main/java/com/hivemq/configuration/entity/api/CAEntity.java @@ -0,0 +1,66 @@ +/* + * Copyright 2019-present HiveMQ GmbH + * + * 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 com.hivemq.configuration.entity.api; + +import jakarta.xml.bind.annotation.XmlAccessType; +import jakarta.xml.bind.annotation.XmlAccessorType; +import jakarta.xml.bind.annotation.XmlElement; +import jakarta.xml.bind.annotation.XmlRootElement; +import org.jetbrains.annotations.Nullable; + +import java.util.Objects; + + +@SuppressWarnings({"FieldMayBeFinal", "FieldCanBeLocal"}) +@XmlRootElement(name = "confidentiality-agreement") +@XmlAccessorType(XmlAccessType.NONE) +public class CAEntity { + + @XmlElement(name = "enabled") + private boolean enabled; + + @XmlElement(name = "content") + private @Nullable String content; + + public CAEntity() { + this(false, null); + } + + public CAEntity(final boolean enabled, final @Nullable String content) { + this.enabled = enabled; + this.content = content; + } + + public boolean isEnabled() { + return enabled; + } + + public @Nullable String getContent() { + return content; + } + + @Override + public boolean equals(final @Nullable Object o) { + return o instanceof final CAEntity that && + Objects.equals(enabled, that.enabled) && + Objects.equals(content, that.content); + } + + @Override + public int hashCode() { + return Objects.hash(enabled, content); + } +} diff --git a/hivemq-edge/src/main/java/com/hivemq/configuration/entity/api/UserEntity.java b/hivemq-edge/src/main/java/com/hivemq/configuration/entity/api/UserEntity.java index ac812963be..83a0fb6895 100644 --- a/hivemq-edge/src/main/java/com/hivemq/configuration/entity/api/UserEntity.java +++ b/hivemq-edge/src/main/java/com/hivemq/configuration/entity/api/UserEntity.java @@ -15,13 +15,14 @@ */ package com.hivemq.configuration.entity.api; -import org.jetbrains.annotations.NotNull; - import jakarta.xml.bind.annotation.XmlAccessType; import jakarta.xml.bind.annotation.XmlAccessorType; import jakarta.xml.bind.annotation.XmlElement; import jakarta.xml.bind.annotation.XmlElementWrapper; import jakarta.xml.bind.annotation.XmlRootElement; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; + import java.util.ArrayList; import java.util.List; import java.util.Objects; @@ -30,38 +31,43 @@ @XmlRootElement(name = "user") @XmlAccessorType(XmlAccessType.NONE) public class UserEntity { - @XmlElement(name = "username") - private String userName = null; - @XmlElement(name = "password") - private String password = null; @XmlElementWrapper(name = "roles") @XmlElement(name = "role") - private @NotNull List roles = new ArrayList<>(); + private final @NotNull List roles = new ArrayList<>(); + + @XmlElement(name = "username") + private @Nullable String userName = null; + + @XmlElement(name = "password") + private @Nullable String password = null; - public String getUserName() { + public @Nullable String getUserName() { return userName; } - public String getPassword() { + public @Nullable String getPassword() { return password; } - public List getRoles() { + public @NotNull List getRoles() { return roles; } @Override - public boolean equals(final Object o) { - if (this == o) return true; - if (o == null || getClass() != o.getClass()) return false; - final UserEntity that = (UserEntity) o; - return Objects.equals(getUserName(), that.getUserName()) && - Objects.equals(getPassword(), that.getPassword()) && - Objects.equals(getRoles(), that.getRoles()); + public boolean equals(final @Nullable Object o) { + if (this == o) { + return true; + } + if (o instanceof final UserEntity that) { + return Objects.equals(userName, that.userName) && + Objects.equals(password, that.password) && + Objects.equals(roles, that.roles); + } + return false; } @Override public int hashCode() { - return Objects.hash(getUserName(), getPassword(), getRoles()); + return Objects.hash(userName, password, roles); } } diff --git a/hivemq-edge/src/main/java/com/hivemq/configuration/entity/listener/tls/KeystoreEntity.java b/hivemq-edge/src/main/java/com/hivemq/configuration/entity/listener/tls/KeystoreEntity.java index 236a53a881..ee25c16a64 100644 --- a/hivemq-edge/src/main/java/com/hivemq/configuration/entity/listener/tls/KeystoreEntity.java +++ b/hivemq-edge/src/main/java/com/hivemq/configuration/entity/listener/tls/KeystoreEntity.java @@ -15,17 +15,15 @@ */ package com.hivemq.configuration.entity.listener.tls; -import org.jetbrains.annotations.NotNull; - import jakarta.xml.bind.annotation.XmlAccessType; import jakarta.xml.bind.annotation.XmlAccessorType; import jakarta.xml.bind.annotation.XmlElement; import jakarta.xml.bind.annotation.XmlRootElement; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; + import java.util.Objects; -/** - * @author Georg Held - */ @XmlRootElement(name = "keystore") @XmlAccessorType(XmlAccessType.NONE) @SuppressWarnings({"FieldMayBeFinal", "FieldCanBeLocal"}) @@ -44,38 +42,42 @@ public class KeystoreEntity { return path; } - public @NotNull String getPassword() { - return password; + public void setPath(final @NotNull String path) { + this.path = path; } - public @NotNull String getPrivateKeyPassword() { - return privateKeyPassword; + public @NotNull String getPassword() { + return password; } - public void setPath(final String path) { - this.path = path; + public void setPassword(final @NotNull String password) { + this.password = password; } - public void setPassword(final String password) { - this.password = password; + public @NotNull String getPrivateKeyPassword() { + return privateKeyPassword; } - public void setPrivateKeyPassword(final String privateKeyPassword) { + public void setPrivateKeyPassword(final @NotNull String privateKeyPassword) { this.privateKeyPassword = privateKeyPassword; } @Override - public boolean equals(final Object o) { - if (this == o) return true; - if (o == null || getClass() != o.getClass()) return false; - final KeystoreEntity that = (KeystoreEntity) o; - return Objects.equals(getPath(), that.getPath()) && - Objects.equals(getPassword(), that.getPassword()) && - Objects.equals(getPrivateKeyPassword(), that.getPrivateKeyPassword()); + public boolean equals(final @Nullable Object o) { + if (this == o) { + return true; + } + if (o instanceof final @Nullable KeystoreEntity that) { + return Objects.equals(path, that.path) && + Objects.equals(password, that.password) && + Objects.equals(privateKeyPassword, that.privateKeyPassword); + } + return false; + } @Override public int hashCode() { - return Objects.hash(getPath(), getPassword(), getPrivateKeyPassword()); + return Objects.hash(path, password, privateKeyPassword); } } diff --git a/hivemq-edge/src/main/java/com/hivemq/configuration/reader/ApiConfigurator.java b/hivemq-edge/src/main/java/com/hivemq/configuration/reader/ApiConfigurator.java index 2fbf744c1d..31725b4c6f 100644 --- a/hivemq-edge/src/main/java/com/hivemq/configuration/reader/ApiConfigurator.java +++ b/hivemq-edge/src/main/java/com/hivemq/configuration/reader/ApiConfigurator.java @@ -20,94 +20,94 @@ import com.hivemq.api.config.ApiListener; import com.hivemq.api.config.HttpListener; import com.hivemq.api.config.HttpsListener; +import com.hivemq.api.model.components.ConfidentialityAgreement; import com.hivemq.configuration.entity.HiveMQConfigEntity; import com.hivemq.configuration.entity.api.AdminApiEntity; import com.hivemq.configuration.entity.api.ApiJwsEntity; import com.hivemq.configuration.entity.api.ApiListenerEntity; import com.hivemq.configuration.entity.api.ApiTlsEntity; +import com.hivemq.configuration.entity.api.CAEntity; import com.hivemq.configuration.entity.api.HttpListenerEntity; import com.hivemq.configuration.entity.api.HttpsListenerEntity; +import com.hivemq.configuration.entity.api.UserEntity; import com.hivemq.configuration.entity.listener.tls.KeystoreEntity; import com.hivemq.configuration.service.ApiConfigurationService; import com.hivemq.exceptions.UnrecoverableException; import com.hivemq.http.core.UsernamePasswordRoles; +import jakarta.inject.Inject; import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; import org.slf4j.Logger; import org.slf4j.LoggerFactory; -import jakarta.inject.Inject; import java.util.List; import java.util.Set; -import java.util.stream.Collectors; -public class ApiConfigurator implements Configurator{ +import static com.hivemq.http.core.UsernamePasswordRoles.DEFAULT_PASSWORD; +import static com.hivemq.http.core.UsernamePasswordRoles.DEFAULT_USERNAME; - private static final Logger log = LoggerFactory.getLogger(ApiConfigurator.class); +public class ApiConfigurator implements Configurator { - private final @NotNull ApiConfigurationService apiConfigurationService; + private static final @NotNull List DEFAULT_LISTENERS = List.of(new HttpListener(8080, "127.0.0.1")); + private static final @NotNull Logger log = LoggerFactory.getLogger(ApiConfigurator.class); + private static final @NotNull List DEFAULT_USERS = + List.of(new UsernamePasswordRoles(DEFAULT_USERNAME, DEFAULT_PASSWORD, Set.of("ADMIN"))); - private volatile AdminApiEntity configEntity; - private volatile boolean initialized = false; + private final @NotNull ApiConfigurationService apiCfgService; + private volatile @Nullable AdminApiEntity configEntity; @Inject - public ApiConfigurator( - final @NotNull ApiConfigurationService apiConfigurationService) { - this.apiConfigurationService = apiConfigurationService; + public ApiConfigurator(final @NotNull ApiConfigurationService apiCfgService) { + this.apiCfgService = apiCfgService; } - @Override - public boolean needsRestartWithConfig(final HiveMQConfigEntity config) { - if(initialized && hasChanged(this.configEntity, config.getApiConfig())) { - return true; - } - return false; + private static @NotNull UsernamePasswordRoles fromModel(final @NotNull UserEntity userEntity) { + return new UsernamePasswordRoles(userEntity.getUserName(), + userEntity.getPassword(), + Set.copyOf(userEntity.getRoles())); } //-- Converts XML entity types to bean types @Override - public ConfigResult applyConfig(final @NotNull HiveMQConfigEntity config) { - this.configEntity = config.getApiConfig(); - this.initialized = true; - - apiConfigurationService.setEnabled(configEntity.isEnabled()); - - //Users - if (configEntity.getUsers() != null && !configEntity.getUsers().isEmpty()) { - apiConfigurationService.setUserList(configEntity.getUsers() - .stream() - .map(userEntity -> new UsernamePasswordRoles(userEntity.getUserName(), - userEntity.getPassword(), - Set.copyOf(userEntity.getRoles()))) - .collect(Collectors.toList())); - } else { - apiConfigurationService.setUserList(List.of( - new UsernamePasswordRoles(UsernamePasswordRoles.DEFAULT_USERNAME, - UsernamePasswordRoles.DEFAULT_PASSWORD, Set.of("ADMIN")))); - } + public boolean needsRestartWithConfig(final @NotNull HiveMQConfigEntity config) { + final AdminApiEntity entity = configEntity; + return entity != null && hasChanged(entity, config.getApiConfig()); + } + + @Override + public @NotNull ConfigResult applyConfig(final @NotNull HiveMQConfigEntity config) { + final AdminApiEntity entity = config.getApiConfig(); + configEntity = entity; + apiCfgService.setEnabled(entity.isEnabled()); - //JWT - ApiJwsEntity jwsEntity = configEntity.getJws(); - ApiJwtConfiguration.Builder apiJwtConfigurationBuilder = new ApiJwtConfiguration.Builder(); - if (jwsEntity != null) { - apiJwtConfigurationBuilder.withAudience(jwsEntity.getAudience()) - .withIssuer(jwsEntity.getIssuer()) - .withKeySize(jwsEntity.getKeySize()) - .withExpiryTimeMinutes(jwsEntity.getExpiryTimeMinutes()) - .withTokenEarlyEpochThresholdMinutes(jwsEntity.getTokenEarlyEpochThresholdMinutes()); + // Users + final List users = entity.getUsers(); + if (!users.isEmpty()) { + apiCfgService.setUserList(users.stream().map(ApiConfigurator::fromModel).toList()); + } else { + apiCfgService.setUserList(DEFAULT_USERS); } - apiConfigurationService.setApiJwtConfiguration(apiJwtConfigurationBuilder.build()); - if (configEntity.getListeners().isEmpty()) { + // JWT + final ApiJwsEntity jwsEntity = entity.getJws(); + apiCfgService.setApiJwtConfiguration(new ApiJwtConfiguration.Builder().withAudience(jwsEntity.getAudience()) + .withIssuer(jwsEntity.getIssuer()) + .withKeySize(jwsEntity.getKeySize()) + .withExpiryTimeMinutes(jwsEntity.getExpiryTimeMinutes()) + .withTokenEarlyEpochThresholdMinutes(jwsEntity.getTokenEarlyEpochThresholdMinutes()) + .build()); + + if (entity.getListeners().isEmpty()) { //set default listener - apiConfigurationService.setListeners(List.of(new HttpListener(8080, "127.0.0.1"))); + apiCfgService.setListeners(DEFAULT_LISTENERS); } else { - final ImmutableList.Builder builder = ImmutableList.builder(); - for (ApiListenerEntity listener : configEntity.getListeners()) { + final ImmutableList.Builder<@NotNull ApiListener> listenersBld = ImmutableList.builder(); + for (final ApiListenerEntity listener : entity.getListeners()) { if (listener instanceof HttpListenerEntity) { - builder.add(new HttpListener(listener.getPort(), listener.getBindAddress())); + listenersBld.add(new HttpListener(listener.getPort(), listener.getBindAddress())); } else if (listener instanceof HttpsListenerEntity) { final ApiTlsEntity tls = ((HttpsListenerEntity) listener).getTls(); final KeystoreEntity keystoreEntity = tls.getKeystoreEntity(); @@ -115,7 +115,7 @@ public ConfigResult applyConfig(final @NotNull HiveMQConfigEntity config) { log.error("Keystore can not be emtpy for HTTPS listener"); throw new UnrecoverableException(false); } - builder.add(new HttpsListener(listener.getPort(), + listenersBld.add(new HttpsListener(listener.getPort(), listener.getBindAddress(), tls.getProtocols(), tls.getCipherSuites(), @@ -123,13 +123,17 @@ public ConfigResult applyConfig(final @NotNull HiveMQConfigEntity config) { keystoreEntity.getPassword(), keystoreEntity.getPrivateKeyPassword())); } else { - log.error("Unkown API listener type"); + log.error("Unknown API listener type"); throw new UnrecoverableException(false); } } - apiConfigurationService.setListeners(builder.build()); + apiCfgService.setListeners(listenersBld.build()); } + // confidentiality agreement + final CAEntity ca = entity.getConfidentialityAgreement(); + apiCfgService.setConfidentialityAgreement(new ConfidentialityAgreement(ca.isEnabled(), ca.getContent())); + return ConfigResult.SUCCESS; } } diff --git a/hivemq-edge/src/main/java/com/hivemq/configuration/reader/ConfigFileReaderWriter.java b/hivemq-edge/src/main/java/com/hivemq/configuration/reader/ConfigFileReaderWriter.java index 48f200fec4..93d6ef186d 100644 --- a/hivemq-edge/src/main/java/com/hivemq/configuration/reader/ConfigFileReaderWriter.java +++ b/hivemq-edge/src/main/java/com/hivemq/configuration/reader/ConfigFileReaderWriter.java @@ -34,7 +34,6 @@ import com.hivemq.exceptions.UnrecoverableException; import com.hivemq.util.ThreadFactoryUtil; import com.hivemq.util.render.EnvVarUtil; -import com.hivemq.util.render.FileFragmentUtil; import com.hivemq.util.render.IfUtil; import jakarta.xml.bind.JAXBContext; import jakarta.xml.bind.JAXBElement; @@ -49,7 +48,6 @@ import org.jetbrains.annotations.VisibleForTesting; import org.slf4j.Logger; import org.slf4j.LoggerFactory; -import org.xml.sax.SAXException; import javax.xml.XMLConstants; import javax.xml.transform.stream.StreamSource; @@ -71,188 +69,137 @@ import java.nio.file.attribute.BasicFileAttributeView; import java.util.ArrayList; import java.util.Arrays; +import java.util.Collections; import java.util.Comparator; import java.util.HashMap; import java.util.List; import java.util.Map; import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.ConcurrentMap; import java.util.concurrent.Executors; import java.util.concurrent.ScheduledExecutorService; -import java.util.concurrent.ThreadFactory; import java.util.concurrent.TimeUnit; import java.util.concurrent.atomic.AtomicLong; -import java.util.stream.Collectors; - +import java.util.concurrent.atomic.AtomicReference; +import java.util.concurrent.locks.Lock; +import java.util.concurrent.locks.ReentrantLock; +import java.util.function.Supplier; + +import static com.hivemq.util.Files.getFileExtension; +import static com.hivemq.util.Files.getFileNameExcludingExtension; +import static com.hivemq.util.Files.getFilePathExcludingFile; +import static com.hivemq.util.render.FileFragmentUtil.replaceFragmentPlaceHolders; import static java.util.Objects.requireNonNullElse; public class ConfigFileReaderWriter { - private static final Logger log = LoggerFactory.getLogger(ConfigFileReaderWriter.class); - static final String XSD_SCHEMA = "config.xsd"; - public static final String CONFIG_FRAGMENT_PATH = "/fragment/config"; + private static final @NotNull Logger log = LoggerFactory.getLogger(ConfigFileReaderWriter.class); + private static final @NotNull String CONFIG_FRAGMENT_PATH = "/fragment/config"; + private static final @NotNull String XSD_SCHEMA = "config.xsd"; + private static final int MAX_BACK_FILES = 5; + private static final @Nullable Schema CONFIG_XSD; + private static final @NotNull JAXBContext CONFIG_JAXB_CONTEXT; - private final @NotNull ConfigurationFile configurationFile; - protected volatile HiveMQConfigEntity configEntity; - private final Object lock = new Object(); - private boolean defaultBackupConfig = true; - private volatile @Nullable ScheduledExecutorService scheduledExecutorService = null; - private final @NotNull List> configurators; - private final @NotNull Map fragmentToModificationTime = new ConcurrentHashMap<>(); + static { + // load config.xsd + final URL resource = ConfigFileReaderWriter.class.getResource("/" + XSD_SCHEMA); + if (resource != null) { + try { + final URLConnection urlConnection = resource.openConnection(); + urlConnection.setUseCaches(false); + try (final InputStream is = urlConnection.getInputStream()) { + CONFIG_XSD = SchemaFactory.newInstance(XMLConstants.W3C_XML_SCHEMA_NS_URI) + .newSchema(new StreamSource(is)); + } + } catch (final Throwable e) { + log.error("Cannot load configuration schema:", e); + throw new UnrecoverableException(false); + } + } else { + log.warn("No schema loaded for validation of config xml."); + CONFIG_XSD = null; + } + // create Jaxb context and marshaller + try { + CONFIG_JAXB_CONTEXT = + JAXBContext.newInstance(ImmutableList.>builder() + .add(HiveMQConfigEntity.class) + // inherited + .add(TCPListenerEntity.class) + .add(WebsocketListenerEntity.class) + .add(TlsTCPListenerEntity.class) + .add(TlsWebsocketListenerEntity.class) + .add(UDPListenerEntity.class) + .add(UDPBroadcastListenerEntity.class) + + .add(FieldMappingEntity.class) + .build() + .toArray(new Class[0])); + } catch (final Throwable e) { + log.error("Cannot create the jaxb context:", e); + throw new UnrecoverableException(false); + } + } + + private final @NotNull ConfigurationFile configFile; + private final @NotNull List> configurators; + private final @NotNull ConcurrentMap fragmentToModificationTime; private final @NotNull BridgeExtractor bridgeExtractor; private final @NotNull ProtocolAdapterExtractor protocolAdapterExtractor; private final @NotNull DataCombiningExtractor dataCombiningExtractor; private final @NotNull UnsExtractor unsExtractor; - private final @NotNull List> reloadableExtractors; - private final @NotNull SystemInformation systemInformation; - - private final @NotNull AtomicLong lastWrite = new AtomicLong(0L); + private final @NotNull List> extractors; + private final @NotNull SystemInformation sysInfo; + private final @NotNull AtomicLong lastWrite; + private final @NotNull AtomicReference configEntity; + private final @NotNull Lock lock; + private final @NotNull AtomicReference executorService; + private boolean defaultBackupConfig; public ConfigFileReaderWriter( - final @NotNull SystemInformation systemInformation, - final @NotNull ConfigurationFile configurationFile, + final @NotNull SystemInformation sysInfo, + final @NotNull ConfigurationFile configFile, final @NotNull List> configurators) { - this.configurationFile = configurationFile; + this.sysInfo = sysInfo; + this.configFile = configFile; this.configurators = configurators; - this.bridgeExtractor = new BridgeExtractor(this); - this.protocolAdapterExtractor = new ProtocolAdapterExtractor(this); - this.dataCombiningExtractor = new DataCombiningExtractor(this); - this.unsExtractor = new UnsExtractor(this); - this.systemInformation = systemInformation; - reloadableExtractors = List.of( - bridgeExtractor, - protocolAdapterExtractor, - dataCombiningExtractor, - unsExtractor); - } - - public HiveMQConfigEntity applyConfig() { - if (configurationFile.file().isEmpty()) { - log.error("No configuration file present. Shutting down HiveMQ Edge."); - throw new UnrecoverableException(false); - } - - final File configFile = configurationFile.file().get(); - final HiveMQConfigEntity hiveMQConfigEntity = readConfigFromXML(configFile); - this.configEntity = hiveMQConfigEntity; - if(!setConfiguration(hiveMQConfigEntity)) { - log.error("Unable to apply the given configuration."); - throw new UnrecoverableException(false); - } - - return hiveMQConfigEntity; - } - - public @NotNull DataCombiningExtractor getDataCombiningExtractor() { - return dataCombiningExtractor; - } - - public @NotNull BridgeExtractor getBridgeExtractor() { - return bridgeExtractor; - } - - public @NotNull ProtocolAdapterExtractor getProtocolAdapterExtractor() { - return protocolAdapterExtractor; - } - - public @NotNull UnsExtractor getUnsExtractor() { - return unsExtractor; + this.extractors = List.of(this.bridgeExtractor = new BridgeExtractor(this), + this.protocolAdapterExtractor = new ProtocolAdapterExtractor(this), + this.dataCombiningExtractor = new DataCombiningExtractor(this), + this.unsExtractor = new UnsExtractor(this)); + this.fragmentToModificationTime = new ConcurrentHashMap<>(); + this.configEntity = new AtomicReference<>(); + this.lastWrite = new AtomicLong(); + this.lock = new ReentrantLock(); + this.executorService = new AtomicReference<>(); + this.defaultBackupConfig = true; } - public void applyConfigAndWatch(final long checkIntervalInMs) { - if(scheduledExecutorService != null) { - throw new IllegalStateException("Config watch was already started"); - } - if (configurationFile.file().isEmpty()) { - log.error("No configuration file present. Shutting down HiveMQ Edge."); - throw new UnrecoverableException(false); - } - - final File configFile = configurationFile.file().get(); - final long interval = (checkIntervalInMs > 0) ? checkIntervalInMs : 0; - log.info("Rereading config file every {} ms", interval); - - final AtomicLong fileModified = new AtomicLong(); - final Map fileModificationTimestamps; - - final HiveMQConfigEntity entity = applyConfig(); - fileModificationTimestamps = findFilesToWatch(entity); - final AtomicLong fileModifiedTimestamp = new AtomicLong(); - try { - fileModifiedTimestamp.set(Files.getLastModifiedTime(configFile.toPath()).toMillis()); - } catch (final IOException e) { - throw new RuntimeException("Unable to read last modified time from " + configFile.getAbsolutePath(), e); - } - - final ThreadFactory threadFactory = ThreadFactoryUtil.create("hivemq-edge-config-watch-%d"); - final ScheduledExecutorService scheduledExecutorService = Executors.newSingleThreadScheduledExecutor(threadFactory); - scheduledExecutorService.scheduleAtFixedRate( - () -> checkMonitoredFilesForChanges(configFile, fileModified, fileModificationTimestamps) - , 0, interval, TimeUnit.MILLISECONDS); - this.scheduledExecutorService = scheduledExecutorService; - Runtime.getRuntime().addShutdownHook(new Thread(this::stopWatching)); - } - - private void checkMonitoredFilesForChanges( - final File configFile, - final @NotNull AtomicLong fileModified, - final @NotNull Map fileModificationTimestamps) { - try { - final boolean devmode = "true".equals(System.getProperty(HiveMQEdgeConstants.DEVELOPMENT_MODE)); - - if(!devmode) { - final Map pathsToCheck = new HashMap<>(fragmentToModificationTime); - - pathsToCheck.putAll(fileModificationTimestamps); - - pathsToCheck.forEach((key, value) -> { - try { - if (!key.toString().equals(CONFIG_FRAGMENT_PATH) && - Files.getFileAttributeView(key.toRealPath(LinkOption.NOFOLLOW_LINKS), - BasicFileAttributeView.class).readAttributes().lastModifiedTime().toMillis() > - value) { - log.error("Restarting because a required file was updated: {}", key); - System.exit(0); - } - } catch (final IOException e) { - throw new RuntimeException("Unable to read last modified time for " + key, e); - } - }); - } - final long modified; - - if(new File(CONFIG_FRAGMENT_PATH).exists()) { - modified = Files.getLastModifiedTime(new File(CONFIG_FRAGMENT_PATH).toPath()).toMillis(); - } else { - log.warn("No fragment found, checking the full config, only used for testing"); - modified = Files.getLastModifiedTime(configFile.toPath()).toMillis(); - } - if (modified > fileModified.get()) { - fileModified.set(modified); - final HiveMQConfigEntity hiveMQConfigEntity = readConfigFromXML(configFile); - this.configEntity = hiveMQConfigEntity; - if(!setConfiguration(hiveMQConfigEntity)) { - if(!devmode) { - log.error("Restarting because new config can't be hot-reloaded"); - System.exit(0); - } else { - log.error("TESTMODE, NOT RESTARTING"); - } - } - } - } catch (final IOException e) { - throw new RuntimeException(e); + private static @NotNull String toValidationMessage(final @NotNull ValidationEvent event) { + final StringBuilder sb = new StringBuilder(); + final ValidationEventLocator locator = event.getLocator(); + if (locator == null) { + sb.append("\t- XML schema violation caused by: \"").append(event.getMessage()).append("\""); + } else { + sb.append("\t- XML schema violation in line '") + .append(locator.getLineNumber()) + .append("' and column '") + .append(locator.getColumnNumber()) + .append("' caused by: \"") + .append(event.getMessage()) + .append("\""); } + return sb.toString(); } - public static Map findFilesToWatch(final HiveMQConfigEntity entity) { + private static @NotNull Map findFilesToWatch(final @NotNull HiveMQConfigEntity entity) { final Map paths = new ConcurrentHashMap<>(); - entity.getBridgeConfig().forEach(cfg -> { final BridgeTlsEntity tls = cfg.getRemoteBroker().getTls(); - if(tls != null) { - final KeystoreEntity keyStore = cfg.getRemoteBroker().getTls().getKeyStore(); - if(keyStore != null) { + if (tls != null) { + final KeystoreEntity keyStore = tls.getKeyStore(); + if (keyStore != null) { final Path path = Paths.get(keyStore.getPath()); try { paths.put(path, Files.getLastModifiedTime(path).toMillis()); @@ -260,8 +207,8 @@ public static Map findFilesToWatch(final HiveMQConfigEntity entity) throw new RuntimeException(e); } } - final TruststoreEntity trustStore = cfg.getRemoteBroker().getTls().getTrustStore(); - if(trustStore != null) { + final TruststoreEntity trustStore = tls.getTrustStore(); + if (trustStore != null) { final Path path = Paths.get(trustStore.getPath()); try { paths.put(path, Files.getLastModifiedTime(path).toMillis()); @@ -280,19 +227,69 @@ public static Map findFilesToWatch(final HiveMQConfigEntity entity) throw new RuntimeException(e); } } - return paths; } - public void stopWatching() { - final ScheduledExecutorService scheduledExecutorService = this.scheduledExecutorService; - if(scheduledExecutorService != null) { - scheduledExecutorService.shutdownNow(); + private static @NotNull Marshaller createMarshaller() throws JAXBException { + final Marshaller marshaller = CONFIG_JAXB_CONTEXT.createMarshaller(); + if (CONFIG_XSD != null) { + marshaller.setSchema(CONFIG_XSD); + marshaller.setProperty(Marshaller.JAXB_SCHEMA_LOCATION, XSD_SCHEMA); } + marshaller.setProperty(Marshaller.JAXB_FORMATTED_OUTPUT, Boolean.TRUE); + return marshaller; } - public void writeConfig() { - writeConfigToXML(configurationFile, defaultBackupConfig); + private static @NotNull Unmarshaller createUnmarshaller(final @Nullable List validationErrors) + throws JAXBException { + final Unmarshaller unmarshaller = CONFIG_JAXB_CONTEXT.createUnmarshaller(); + if (CONFIG_XSD != null) { + unmarshaller.setSchema(CONFIG_XSD); + } + if (validationErrors != null) { + unmarshaller.setEventHandler(e -> { + if (e.getSeverity() >= ValidationEvent.ERROR) { + validationErrors.add(e); + } + return true; + }); + } + return unmarshaller; + } + + public @NotNull DataCombiningExtractor getDataCombiningExtractor() { + return dataCombiningExtractor; + } + + public @NotNull BridgeExtractor getBridgeExtractor() { + return bridgeExtractor; + } + + public @NotNull ProtocolAdapterExtractor getProtocolAdapterExtractor() { + return protocolAdapterExtractor; + } + + public @NotNull UnsExtractor getUnsExtractor() { + return unsExtractor; + } + + public void setDefaultBackupConfig(final boolean defaultBackupConfig) { + this.defaultBackupConfig = defaultBackupConfig; + } + + public @NotNull HiveMQConfigEntity applyConfig() { + if (!loadConfigFromXML(getConfigFileOrFail())) { + log.error("Unable to apply the given configuration."); + throw new UnrecoverableException(false); + } + return configEntity.get(); + } + + public void applyConfigAndWatch(final long checkIntervalInMs) { + startWatching(getConfigFileOrFail(), + (checkIntervalInMs > 0) ? checkIntervalInMs : 1000, + this::applyConfig, + this::checkMonitoredFilesForChanges); } public void writeConfigWithSync() { @@ -300,252 +297,171 @@ public void writeConfigWithSync() { log.trace("flushing configuration changes to entity layer"); } try { - syncConfiguration(); - if (configEntity.getGatewayConfig().isMutableConfigurationEnabled()) { - writeConfig(); + // sync config + final HiveMQConfigEntity entity = this.configEntity.get(); + Preconditions.checkNotNull(entity, "Configuration must be loaded to be synchronized"); + configurators.stream() + .filter(Syncable.class::isInstance) + .map(Syncable.class::cast) + .forEach(syncable -> syncable.sync(entity)); + extractors.forEach(extractor -> extractor.sync(entity)); + if (entity.getGatewayConfig().isMutableConfigurationEnabled()) { + writeConfigToXML(); } - } catch (final Exception e){ + } catch (final Exception e) { log.error("Configuration file sync failed: ", e); } finally { lastWrite.set(System.currentTimeMillis()); } } - public @NotNull Long getLastWrite() { + public long getLastWrite() { return lastWrite.get(); } - public void setDefaultBackupConfig(final boolean defaultBackupConfig) { - this.defaultBackupConfig = defaultBackupConfig; - } - - public void writeConfig(final @NotNull ConfigurationFile file, final boolean rollConfig) { - writeConfigToXML(file, rollConfig); - } - - @NotNull Class getConfigEntityClass() { - return HiveMQConfigEntity.class; - } - - @NotNull List> getInheritedEntityClasses() { - return ImmutableList.of( - /* ListenerEntity */ - TCPListenerEntity.class, - WebsocketListenerEntity.class, - TlsTCPListenerEntity.class, - TlsWebsocketListenerEntity.class, - UDPListenerEntity.class, - UDPBroadcastListenerEntity.class); + public void writeConfigToXML(final @NotNull Writer writer) { + lock.lock(); + try { + createMarshaller().marshal(configEntity.get(), writer); + } catch (final Throwable e) { + log.error("Original error message:", e); + throw new UnrecoverableException(false); + } finally { + lock.unlock(); + } } - protected JAXBContext createContext() throws JAXBException { - final Class[] classes = ImmutableList.>builder() - .add(getConfigEntityClass()) - .addAll(getInheritedEntityClasses()) - .add(FieldMappingEntity.class) - .build() - .toArray(new Class[0]); - - return JAXBContext.newInstance(classes); + @VisibleForTesting + void writeConfigToXML() { + writeConfigToXML(getConfigFileOrFail()); } - private void writeConfigToXML(final @NotNull ConfigurationFile outputFile, final boolean rollConfig) { - - synchronized (lock) { - - //-- Checks need to be inside sync block as could be created by the initialisation - if (configEntity == null) { + @VisibleForTesting + public void writeConfigToXML(final @NotNull File file) { + if (!file.exists() && !file.canWrite()) { + log.error("Unable to write to supplied configuration file {}", file); + throw new UnrecoverableException(false); + } + if (log.isDebugEnabled()) { + log.debug("Writing configuration file {}", file.getAbsolutePath()); + } + lock.lock(); + try { + final HiveMQConfigEntity entity = this.configEntity.get(); + if (entity == null) { log.error("Unable to write uninitialized configuration."); throw new UnrecoverableException(false); } - if (outputFile.file().isEmpty()) { - log.error("No configuration file present."); - throw new UnrecoverableException(false); + backupConfig(file); // write the backup of the file before rewriting + try (final FileWriter writer = new FileWriter(file, StandardCharsets.UTF_8)) { + writeConfigToXML(writer); } - if (outputFile.file().get().exists() && !outputFile.file().get().canWrite()) { - log.error("Unable to write to supplied configuration file {}", outputFile.file().get()); - throw new UnrecoverableException(false); - } - - try { - final File configFile = outputFile.file().get(); - log.debug("Writing configuration file {}", configFile.getAbsolutePath()); - //write the backup of the file before rewriting - if (rollConfig) { - backupConfig(configFile, 5); - } - try (final FileWriter fileWriter = new FileWriter(outputFile.file().get(), StandardCharsets.UTF_8)) { - writeConfigToXML(fileWriter); - } - } catch (final IOException e) { - log.error("Error writing file:", e); - throw new UnrecoverableException(false); - } - - } - } - - protected void backupConfig(final @NotNull File configFile, final int maxBackFiles) throws IOException { - int idx = 0; - final String fileNameExclExt = com.hivemq.util.Files.getFileNameExcludingExtension(configFile.getName()); - final String fileExtension = com.hivemq.util.Files.getFileExtension(configFile.getName()); - final String copyPath = com.hivemq.util.Files.getFilePathExcludingFile(configFile.getAbsolutePath()); - - String copyFilename = null; - File copyFile = null; - do { - copyFilename = String.format("%s_%d.%s", fileNameExclExt, ++idx, fileExtension); - copyFile = new File(copyPath, copyFilename); - } while(idx < maxBackFiles && copyFile.exists()); - - if(copyFile.exists()){ - //-- use the oldest available backup index - final File[] backupFiles = new File(copyPath) - .listFiles(child -> - child.isFile() && - child.getName().startsWith(fileNameExclExt) && - child.getName().endsWith(fileExtension)); - Arrays.sort(backupFiles, Comparator.comparingLong(File::lastModified)); - copyFile = backupFiles[0]; - } - if(log.isDebugEnabled()){ - log.debug("Rolling backup of configuration file to {}", copyFile.getName()); + } catch (final IOException e) { + log.error("Error writing file:", e); + throw new UnrecoverableException(false); + } finally { + lock.unlock(); } - FileUtils.copyFile(configFile, copyFile); } - public void writeConfigToXML(final @NotNull Writer writer) { - synchronized (lock) { - try { - final JAXBContext context = createContext(); - final Marshaller marshaller = context.createMarshaller(); - final Schema schema = loadSchema(); - if (schema != null) { - marshaller.setSchema(schema); - marshaller.setProperty(Marshaller.JAXB_SCHEMA_LOCATION, XSD_SCHEMA); - } - marshaller.setProperty(Marshaller.JAXB_FORMATTED_OUTPUT, Boolean.TRUE); - marshaller.marshal(configEntity, writer); - } catch (final JAXBException | IOException | SAXException e) { - log.error("Original error message:", e); - throw new UnrecoverableException(false); - } - } + private @NotNull File getConfigFileOrFail() { + return configFile.file().orElseGet(() -> { + log.error("No configuration file present. Shutting down HiveMQ Edge."); + throw new UnrecoverableException(false); + }); } @VisibleForTesting - public @NotNull HiveMQConfigEntity readConfigFromXML(final @NotNull File configFile) { - + boolean loadConfigFromXML(final @NotNull File configFile) { log.info("Reading configuration file {}", configFile); - final List validationErrors = new ArrayList<>(); - - synchronized (lock) { - try { - final JAXBContext context = createContext(); - final Unmarshaller unmarshaller = context.createUnmarshaller(); - final Schema schema = loadSchema(); - if (schema != null) { - unmarshaller.setSchema(schema); - } - - //replace environment variable placeholders - String configFileContent = Files.readString(configFile.toPath()); - final var fragmentResult = FileFragmentUtil - .replaceFragmentPlaceHolders( - configFileContent, - systemInformation.isConfigFragmentBase64Zip()); - - fragmentToModificationTime.putAll(fragmentResult.getFragmentToModificationTime()); - - configFileContent = fragmentResult.getRenderResult(); //must happen before env rendering so templates can be used with envs - configFileContent = IfUtil.replaceIfPlaceHolders(configFileContent); - configFileContent = EnvVarUtil.replaceEnvironmentVariablePlaceholders(configFileContent); - - try(final ByteArrayInputStream is = - new ByteArrayInputStream(configFileContent.getBytes(StandardCharsets.UTF_8))) { - - final StreamSource streamSource = new StreamSource(is); - - unmarshaller.setEventHandler(e -> { - if (e.getSeverity() >= ValidationEvent.ERROR) { - validationErrors.add(e); - } - return true; - - }); - final JAXBElement result = - unmarshaller.unmarshal(streamSource, getConfigEntityClass()); - - if (!validationErrors.isEmpty()) { - throw new JAXBException("Parsing failed"); - } - - final HiveMQConfigEntity configEntity = result.getValue(); + final List validationErrors = Collections.synchronizedList(new ArrayList<>()); - if (configEntity == null) { - throw new JAXBException("Result is null"); - } - - configEntity.getProtocolAdapterConfig().forEach(e -> e.validate(validationErrors)); + lock.lock(); + try { - configEntity.getDataCombinerEntities().forEach(e -> e.validate(validationErrors)); + // replace environment variable placeholders + String content = Files.readString(configFile.toPath()); + final var fragment = replaceFragmentPlaceHolders(content, sysInfo.isConfigFragmentBase64Zip()); + content = fragment.getRenderResult(); //must happen before env rendering so templates can be used with envs + content = IfUtil.replaceIfPlaceHolders(content); + content = EnvVarUtil.replaceEnvironmentVariablePlaceholders(content); + fragmentToModificationTime.putAll(fragment.getFragmentToModificationTime()); - if (!validationErrors.isEmpty()) { - throw new JAXBException("Parsing failed"); - } - return configEntity; + try (final ByteArrayInputStream is = new ByteArrayInputStream(content.getBytes(StandardCharsets.UTF_8))) { + final JAXBElement unmarshalled = + createUnmarshaller(validationErrors).unmarshal(new StreamSource(is), HiveMQConfigEntity.class); + if (!validationErrors.isEmpty()) { + throw new JAXBException("Parsing failed"); } - } catch (final JAXBException | IOException e) { - final StringBuilder messageBuilder = new StringBuilder(); - - if (validationErrors.isEmpty()) { - messageBuilder.append("of the following error: "); - messageBuilder.append(requireNonNullElse(e.getCause(), e)); - } else { - messageBuilder.append("of the following errors:"); - for (final ValidationEvent validationError : validationErrors) { - messageBuilder.append(System.lineSeparator()).append(toValidationMessage(validationError)); - } + final HiveMQConfigEntity entity = unmarshalled.getValue(); + if (entity == null) { + throw new JAXBException("Result is null"); + } + entity.getProtocolAdapterConfig().forEach(e -> e.validate(validationErrors)); + entity.getDataCombinerEntities().forEach(e -> e.validate(validationErrors)); + if (!validationErrors.isEmpty()) { + throw new JAXBException("Parsing failed"); } - log.error("Not able to parse configuration file because {}", messageBuilder); - throw new UnrecoverableException(false); - } catch (final Exception e) { - if (e.getCause() instanceof UnrecoverableException) { - if (((UnrecoverableException) e.getCause()).isShowException()) { - log.error("An unrecoverable Exception occurred. Exiting HiveMQ", e); - log.debug("Original error message:", e); - } - System.exit(1); + configEntity.set(entity); + return internalApplyConfig(entity); + } + } catch (final JAXBException | IOException e) { + final StringBuilder sb = new StringBuilder(); + if (validationErrors.isEmpty()) { + sb.append("of the following error: "); + sb.append(requireNonNullElse(e.getCause(), e)); + } else { + sb.append("of the following errors:"); + for (final ValidationEvent validationError : validationErrors) { + sb.append(System.lineSeparator()).append(toValidationMessage(validationError)); } - log.error("Could not read the configuration file {}. Exiting HiveMQ Edge.", - configFile.getAbsolutePath()); + } + log.error("Not able to parse configuration file because {}", sb); + throw new UnrecoverableException(false); + } catch (final Exception e) { + if (e.getCause() instanceof UnrecoverableException) { + if (((UnrecoverableException) e.getCause()).isShowException()) { + log.error("An unrecoverable Exception occurred. Exiting HiveMQ", e); + log.debug("Original error message:", e); + } + System.exit(1); + } + log.error("Could not read the configuration file {}. Exiting HiveMQ Edge.", configFile.getAbsolutePath()); + if (log.isDebugEnabled()) { log.debug("Original error message:", e); - throw new UnrecoverableException(false); } + throw new UnrecoverableException(false); + } finally { + lock.unlock(); } } - boolean setConfiguration(final @NotNull HiveMQConfigEntity config) { - - final List requiresRestart = - configurators.stream() - .filter(c -> c.needsRestartWithConfig(config)) - .map(c -> c.getClass().getSimpleName()) - .collect(Collectors.toList()); - - if (requiresRestart.isEmpty()) { + @VisibleForTesting + boolean internalApplyConfig(final @NotNull HiveMQConfigEntity entity) { + final List requiresRestart = configurators.stream() + .filter(c -> c.needsRestartWithConfig(entity)) + .map(c -> c.getClass().getSimpleName()) + .toList(); + if (!requiresRestart.isEmpty()) { + log.error("Config requires restart because of: {}", requiresRestart); + return false; + } + if (log.isDebugEnabled()) { log.debug("Config can be applied"); + } + + try { for (final Configurator configurator : configurators) { - final @Nullable Configurator.ConfigResult configResult = configurator.applyConfig(config); - if (configResult == null) { + final Configurator.ConfigResult result = configurator.applyConfig(entity); + if (result == null) { log.error("Config {} can not be applied because the result is not found.", configurator.getClass().getSimpleName()); return false; } - switch (configResult) { + switch (result) { case ERROR -> { log.error("Config {} can not be applied because an unrecoverable error is found.", configurator.getClass().getSimpleName()); @@ -558,76 +474,155 @@ boolean setConfiguration(final @NotNull HiveMQConfigEntity config) { } } } - for (final ReloadableExtractor reloadableExtractor : reloadableExtractors) { - final @Nullable Configurator.ConfigResult configResult = reloadableExtractor.updateConfig(config); - if (configResult == null) { + + for (final ReloadableExtractor extractor : extractors) { + final Configurator.ConfigResult result = extractor.updateConfig(entity); + if (result == null) { log.error("Reloadable config {} can not be applied because the result is not found.", - reloadableExtractor.getClass().getSimpleName()); + extractor.getClass().getSimpleName()); return false; } - switch (configResult) { + switch (result) { case ERROR -> { log.error("Reloadable config {} can not be applied because an unrecoverable error is found.", - reloadableExtractor.getClass().getSimpleName()); + extractor.getClass().getSimpleName()); return false; } case NEEDS_RESTART -> { log.error("Reloadable config {} can not be applied because it requires restart.", - reloadableExtractor.getClass().getSimpleName()); + extractor.getClass().getSimpleName()); return false; } } } return true; - } else { - log.error("Config requires restart because of: {}", requiresRestart); + } catch (final Throwable t) { + log.error("An error occurred while applying the configuration.", t); return false; } - } - public void syncConfiguration() { - Preconditions.checkNotNull(configEntity, "Configuration must be loaded to be synchronized"); - configurators.stream() - .filter(c -> c instanceof Syncable) - .forEach(c -> ((Syncable)c).sync(configEntity)); - reloadableExtractors.forEach(reloadableExtractor -> reloadableExtractor.sync(configEntity)); + private void backupConfig(final @NotNull File configFile) throws IOException { + if (!defaultBackupConfig) { + return; + } + final String fileNameNoExt = getFileNameExcludingExtension(configFile.getName()); + final String fileExt = getFileExtension(configFile.getName()); + final File copyPath = new File(getFilePathExcludingFile(configFile.getAbsolutePath())); + if (copyPath.exists() && copyPath.isDirectory()) { + int idx = 1; + File copyFile; + do { + final String copyFilename = fileNameNoExt + '_' + (idx++) + (fileExt != null ? "." + fileExt : ""); + copyFile = new File(copyPath, copyFilename); + } while (idx < MAX_BACK_FILES && copyFile.exists()); + + if (copyFile.exists()) { + //-- use the oldest available backup index + final File[] backupFiles = copyPath.listFiles(child -> child.isFile() && + child.getName().startsWith(fileNameNoExt) && + (fileExt == null || child.getName().endsWith(fileExt))); + assert backupFiles != null; + Arrays.sort(backupFiles, Comparator.comparingLong(File::lastModified)); + copyFile = backupFiles[0]; + } + if (log.isDebugEnabled()) { + log.debug("Rolling backup of configuration file to {}", copyFile.getName()); + } + FileUtils.copyFile(configFile, copyFile); + } else { + log.error("Configuration folder {} does not exist or is not a directory", copyPath.getAbsolutePath()); + throw new UnrecoverableException(false); + } } - protected Schema loadSchema() throws IOException, SAXException { - final URL resource = ConfigFileReaderWriter.class.getResource("/" + XSD_SCHEMA); - if (resource != null) { - try (final InputStream is = uncachedStream(resource)) { - final SchemaFactory sf = SchemaFactory.newInstance(XMLConstants.W3C_XML_SCHEMA_NS_URI); - return sf.newSchema(new StreamSource(is)); + private void startWatching( + final @NotNull File configFile, + final long interval, + final @NotNull Supplier entitySupplier, + final @NotNull ScheduledTask scheduledTask) { + if (executorService.compareAndSet(null, + Executors.newSingleThreadScheduledExecutor(ThreadFactoryUtil.create("hivemq-edge-config-watch-%d")))) { + + final HiveMQConfigEntity entity = entitySupplier.get(); + final Map fileModificationTimestamps = findFilesToWatch(entity); + final AtomicLong fileModified = new AtomicLong(); + try { + fileModified.set(Files.getLastModifiedTime(configFile.toPath()).toMillis()); + } catch (final IOException e) { + throw new RuntimeException("Unable to read last modified time from " + configFile.getAbsolutePath(), e); } + + log.info("Rereading config file every {} ms", interval); + executorService.get() + .scheduleAtFixedRate(() -> scheduledTask.executePeriodicTask(configFile, + fileModified, + fileModificationTimestamps), 0, interval, TimeUnit.MILLISECONDS); + Runtime.getRuntime().addShutdownHook(new Thread(this::stopWatching)); + } else { + throw new IllegalStateException("Config watch was already started"); } - log.warn("No schema loaded for validation of config xml."); - return null; } - private @NotNull InputStream uncachedStream(final @NotNull URL xsd) throws IOException { - final URLConnection urlConnection = xsd.openConnection(); - urlConnection.setUseCaches(false); - return urlConnection.getInputStream(); + private void stopWatching() { + final ScheduledExecutorService es = executorService.getAndSet(null); + if (es != null) { + es.shutdownNow(); + } } - private @NotNull String toValidationMessage(final @NotNull ValidationEvent validationEvent) { - final StringBuilder validationMessageBuilder = new StringBuilder(); - final ValidationEventLocator locator = validationEvent.getLocator(); - if (locator == null) { - validationMessageBuilder.append("\t- XML schema violation caused by: \"") - .append(validationEvent.getMessage()) - .append("\""); - } else { - validationMessageBuilder.append("\t- XML schema violation in line '") - .append(locator.getLineNumber()) - .append("' and column '") - .append(locator.getColumnNumber()) - .append("' caused by: \"") - .append(validationEvent.getMessage()) - .append("\""); + private void checkMonitoredFilesForChanges( + final @NotNull File configFile, + final @NotNull AtomicLong fileModified, + final @NotNull Map fileModificationTimestamps) { + try { + final boolean isDevMode = "true".equals(System.getProperty(HiveMQEdgeConstants.DEVELOPMENT_MODE)); + if (!isDevMode) { + final Map pathsToCheck = new HashMap<>(fragmentToModificationTime); + pathsToCheck.putAll(fileModificationTimestamps); + pathsToCheck.forEach((key, value) -> { + try { + if (!key.toString().equals(CONFIG_FRAGMENT_PATH) && + Files.getFileAttributeView(key.toRealPath(LinkOption.NOFOLLOW_LINKS), + BasicFileAttributeView.class).readAttributes().lastModifiedTime().toMillis() > + value) { + log.error("Restarting because a required file was updated: {}", key); + System.exit(0); + } + } catch (final IOException e) { + throw new RuntimeException("Unable to read last modified time for " + key, e); + } + }); + } + + final long modified; + if (new File(CONFIG_FRAGMENT_PATH).exists()) { + modified = Files.getLastModifiedTime(new File(CONFIG_FRAGMENT_PATH).toPath()).toMillis(); + } else { + log.warn("No fragment found, checking the full config, only used for testing"); + modified = Files.getLastModifiedTime(configFile.toPath()).toMillis(); + } + if (modified > fileModified.get()) { + fileModified.set(modified); + if (!loadConfigFromXML(configFile)) { + if (!isDevMode) { + log.error("Restarting because new config can't be hot-reloaded"); + System.exit(0); + } else { + log.error("TEST MODE, NOT RESTARTING"); + } + } + } + } catch (final IOException e) { + throw new RuntimeException(e); } - return validationMessageBuilder.toString(); + } + + @FunctionalInterface + private interface ScheduledTask { + void executePeriodicTask( + final @NotNull File configFile, + final @NotNull AtomicLong fileModified, + final @NotNull Map fileModificationTimestamps); } } diff --git a/hivemq-edge/src/main/java/com/hivemq/configuration/service/ApiConfigurationService.java b/hivemq-edge/src/main/java/com/hivemq/configuration/service/ApiConfigurationService.java index e69c22e920..0e23bce67a 100644 --- a/hivemq-edge/src/main/java/com/hivemq/configuration/service/ApiConfigurationService.java +++ b/hivemq-edge/src/main/java/com/hivemq/configuration/service/ApiConfigurationService.java @@ -18,9 +18,10 @@ import com.hivemq.api.config.ApiJwtConfiguration; import com.hivemq.api.config.ApiListener; import com.hivemq.api.config.ApiStaticResourcePath; +import com.hivemq.api.model.components.ConfidentialityAgreement; +import com.hivemq.http.core.UsernamePasswordRoles; import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; -import com.hivemq.http.core.UsernamePasswordRoles; import java.util.List; @@ -31,22 +32,25 @@ public interface ApiConfigurationService { @NotNull List getListeners(); + void setListeners(final @NotNull List listeners); + boolean isEnabled(); - @NotNull List getResourcePaths(); + void setEnabled(boolean enabled); - @Nullable ApiJwtConfiguration getApiJwtConfiguration(); + @NotNull List getResourcePaths(); - @NotNull List getUserList(); + void setResourcePaths(final @NotNull List resourcePaths); - void setEnabled(boolean enabled); + @Nullable ApiJwtConfiguration getApiJwtConfiguration(); - void setResourcePaths(@NotNull List resourcePaths); + void setApiJwtConfiguration(final @NotNull ApiJwtConfiguration apiJwtConfiguration); - void setUserList(@NotNull List userList); + @NotNull List getUserList(); - void setListeners(@NotNull List listeners); + void setUserList(final @NotNull List userList); - void setApiJwtConfiguration(@NotNull ApiJwtConfiguration apiJwtConfiguration); + @NotNull ConfidentialityAgreement getConfidentialityAgreement(); + void setConfidentialityAgreement(final @NotNull ConfidentialityAgreement confidentialityAgreement); } diff --git a/hivemq-edge/src/main/java/com/hivemq/configuration/service/ConfigurationService.java b/hivemq-edge/src/main/java/com/hivemq/configuration/service/ConfigurationService.java index bad43f8e91..e357280780 100644 --- a/hivemq-edge/src/main/java/com/hivemq/configuration/service/ConfigurationService.java +++ b/hivemq-edge/src/main/java/com/hivemq/configuration/service/ConfigurationService.java @@ -29,7 +29,6 @@ /** * The Configuration Service interface which allows to change HiveMQ configuration programmatically. * - * @author Dominik Obermaier * @since 3.0 */ diff --git a/hivemq-edge/src/main/java/com/hivemq/configuration/service/impl/ApiConfigurationServiceImpl.java b/hivemq-edge/src/main/java/com/hivemq/configuration/service/impl/ApiConfigurationServiceImpl.java index 805193cee3..c020b687c8 100644 --- a/hivemq-edge/src/main/java/com/hivemq/configuration/service/impl/ApiConfigurationServiceImpl.java +++ b/hivemq-edge/src/main/java/com/hivemq/configuration/service/impl/ApiConfigurationServiceImpl.java @@ -18,18 +18,16 @@ import com.hivemq.api.config.ApiJwtConfiguration; import com.hivemq.api.config.ApiListener; import com.hivemq.api.config.ApiStaticResourcePath; +import com.hivemq.api.model.components.ConfidentialityAgreement; import com.hivemq.configuration.service.ApiConfigurationService; +import com.hivemq.http.core.UsernamePasswordRoles; +import jakarta.inject.Singleton; import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; -import com.hivemq.http.core.UsernamePasswordRoles; -import jakarta.inject.Singleton; import java.util.List; import java.util.concurrent.CopyOnWriteArrayList; -/** - * @author Simon L Johnson - */ @Singleton public class ApiConfigurationServiceImpl implements ApiConfigurationService { @@ -38,50 +36,60 @@ public class ApiConfigurationServiceImpl implements ApiConfigurationService { private @NotNull List userList = new CopyOnWriteArrayList<>(); private @NotNull List listeners = new CopyOnWriteArrayList<>(); private @Nullable ApiJwtConfiguration apiJwtConfiguration; + private @NotNull ConfidentialityAgreement confidentialityAgreement = new ConfidentialityAgreement(); @Override public @NotNull List getListeners() { return listeners; } + public void setListeners(final @NotNull List listeners) { + this.listeners = listeners; + } + @Override public boolean isEnabled() { return enabled; } + public void setEnabled(final boolean enabled) { + this.enabled = enabled; + } + @Override public @NotNull List getResourcePaths() { return resourcePaths; } + public void setResourcePaths(final @NotNull List resourcePaths) { + this.resourcePaths = resourcePaths; + } + @Override public @Nullable ApiJwtConfiguration getApiJwtConfiguration() { return apiJwtConfiguration; } + public void setApiJwtConfiguration(final @NotNull ApiJwtConfiguration apiJwtConfiguration) { + this.apiJwtConfiguration = apiJwtConfiguration; + } + @Override public @NotNull List getUserList() { return userList; } - - public void setEnabled(final boolean enabled) { - this.enabled = enabled; - } - - public void setResourcePaths(final @NotNull List resourcePaths) { - this.resourcePaths = resourcePaths; - } - public void setUserList(final @NotNull List userList) { this.userList = userList; } - public void setListeners(final @NotNull List listeners) { - this.listeners = listeners; + @Override + public @NotNull ConfidentialityAgreement getConfidentialityAgreement() { + return confidentialityAgreement; } - public void setApiJwtConfiguration(final @NotNull ApiJwtConfiguration apiJwtConfiguration) { - this.apiJwtConfiguration = apiJwtConfiguration; + @Override + public void setConfidentialityAgreement(final @NotNull ConfidentialityAgreement confidentialityAgreement) { + this.confidentialityAgreement = confidentialityAgreement; } } diff --git a/hivemq-edge/src/main/java/com/hivemq/configuration/service/impl/ConfigurationServiceImpl.java b/hivemq-edge/src/main/java/com/hivemq/configuration/service/impl/ConfigurationServiceImpl.java index bb7a719ff4..ec792645d1 100644 --- a/hivemq-edge/src/main/java/com/hivemq/configuration/service/impl/ConfigurationServiceImpl.java +++ b/hivemq-edge/src/main/java/com/hivemq/configuration/service/impl/ConfigurationServiceImpl.java @@ -49,9 +49,6 @@ /** * The implementation of the {@link ConfigurationService} - * - * @author Dominik Obermaier - * @author Christoph Schäbel */ public class ConfigurationServiceImpl implements ConfigurationService { diff --git a/hivemq-edge/src/main/java/com/hivemq/configuration/service/impl/GatewayConfigurationServiceImpl.java b/hivemq-edge/src/main/java/com/hivemq/configuration/service/impl/GatewayConfigurationServiceImpl.java index 672f16e517..7225f9ff32 100644 --- a/hivemq-edge/src/main/java/com/hivemq/configuration/service/impl/GatewayConfigurationServiceImpl.java +++ b/hivemq-edge/src/main/java/com/hivemq/configuration/service/impl/GatewayConfigurationServiceImpl.java @@ -17,9 +17,6 @@ import com.hivemq.configuration.service.DynamicConfigurationService; -/** - * @author Simon L Johnson - */ public class GatewayConfigurationServiceImpl implements DynamicConfigurationService { private boolean mutableConfigurationEnabled; @@ -42,5 +39,4 @@ public void setMutableConfigurationEnabled(final boolean mutableConfigurationEna public void setConfigurationExportEnabled(final boolean configurationExportEnabled) { this.configurationExportEnabled = configurationExportEnabled; } - } diff --git a/hivemq-edge/src/main/java/com/hivemq/edge/HiveMQEdgeRemoteService.java b/hivemq-edge/src/main/java/com/hivemq/edge/HiveMQEdgeRemoteService.java index b1650ca0a9..f784d12c12 100644 --- a/hivemq-edge/src/main/java/com/hivemq/edge/HiveMQEdgeRemoteService.java +++ b/hivemq-edge/src/main/java/com/hivemq/edge/HiveMQEdgeRemoteService.java @@ -15,17 +15,14 @@ */ package com.hivemq.edge; -import com.hivemq.edge.model.HiveMQEdgeRemoteEvent; import com.hivemq.edge.model.HiveMQEdgeRemoteConfiguration; +import com.hivemq.edge.model.HiveMQEdgeRemoteEvent; import org.jetbrains.annotations.NotNull; -/** - * @author Simon L Johnson - */ public interface HiveMQEdgeRemoteService { - HiveMQEdgeRemoteConfiguration getConfiguration(); + @NotNull HiveMQEdgeRemoteConfiguration getConfiguration(); - void fireUsageEvent(@NotNull HiveMQEdgeRemoteEvent event); + void fireUsageEvent(final @NotNull HiveMQEdgeRemoteEvent event); } diff --git a/hivemq-edge/src/main/java/com/hivemq/edge/impl/remote/HiveMQRemoteServiceImpl.java b/hivemq-edge/src/main/java/com/hivemq/edge/impl/remote/HiveMQRemoteServiceImpl.java index 8a3f881904..a37fed58fa 100644 --- a/hivemq-edge/src/main/java/com/hivemq/edge/impl/remote/HiveMQRemoteServiceImpl.java +++ b/hivemq-edge/src/main/java/com/hivemq/edge/impl/remote/HiveMQRemoteServiceImpl.java @@ -21,130 +21,139 @@ import com.hivemq.configuration.info.SystemInformation; import com.hivemq.configuration.service.ConfigurationService; import com.hivemq.edge.HiveMQEdgeRemoteService; -import com.hivemq.edge.model.HiveMQEdgeRemoteEvent; import com.hivemq.edge.model.HiveMQEdgeRemoteConfiguration; +import com.hivemq.edge.model.HiveMQEdgeRemoteEvent; import com.hivemq.edge.utils.HiveMQEdgeEnvironmentUtils; +import jakarta.inject.Inject; import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; import org.slf4j.Logger; import org.slf4j.LoggerFactory; -import jakarta.inject.Inject; import java.io.IOException; import java.io.InputStream; import java.util.Optional; +import java.util.concurrent.locks.Lock; +import java.util.concurrent.locks.ReentrantLock; + +import static java.util.Objects.requireNonNull; /** * A service that (optionally) connects to remote endpoints to provision configuration * that helps/augments the runtime. Also provides endpoints for tracking usage to allow * HiveMQ to provide a better open-source product. - * - * @author Simon L Johnson */ public class HiveMQRemoteServiceImpl implements HiveMQEdgeRemoteService, HiveMQShutdownHook { - static final String LOCAL_RESOURCE_CONFIGURATION = "/hivemq-edge-configuration.json"; - static final int TIMEOUT = 5000; - static final int REFRESH = 60000; - private static final Logger logger = LoggerFactory.getLogger(HiveMQRemoteServiceImpl.class); - private final @NotNull ObjectMapper objectMapper; - private @NotNull HiveMQEdgeHttpServiceImpl hiveMQEdgeHttpService; - private @NotNull HiveMQEdgeRemoteConfiguration localConfiguration; - private @NotNull SystemInformation systemInformation; + private static final @NotNull Logger log = LoggerFactory.getLogger(HiveMQRemoteServiceImpl.class); + private static final @NotNull String LOCAL_RESOURCE_CONFIGURATION = "/hivemq-edge-configuration.json"; + private static final int TIMEOUT = 5000; + private static final int REFRESH = 60000; - private final Object lock = new Object(); + private final @NotNull ObjectMapper mapper; + private final @NotNull Lock lock; + private final @NotNull SystemInformation sysInfo; + private @Nullable HiveMQEdgeHttpServiceImpl httpService; + private @Nullable HiveMQEdgeRemoteConfiguration localConfig; @Inject public HiveMQRemoteServiceImpl( - final @NotNull SystemInformation systemInformation, - final @NotNull ConfigurationService configurationService, - final @NotNull ObjectMapper objectMapper, + final @NotNull SystemInformation sysInfo, + final @NotNull ConfigurationService configService, + final @NotNull ObjectMapper mapper, final @NotNull ShutdownHooks shutdownHooks) { - this.objectMapper = objectMapper; - this.systemInformation = systemInformation; + this.lock = new ReentrantLock(); + this.mapper = mapper; + this.sysInfo = sysInfo; //-- We should only init the remote service (and thus the overhead of the threads) when //-- remote tracking is enabled. - if(configurationService.usageTrackingConfiguration().isUsageTrackingEnabled()){ + if (configService.usageTrackingConfiguration().isUsageTrackingEnabled()) { initHttpService(); shutdownHooks.add(this); } } - protected final void initHttpService() { - try { - hiveMQEdgeHttpService = new HiveMQEdgeHttpServiceImpl(systemInformation.getHiveMQVersion(), - objectMapper, HiveMQEdgeHttpServiceImpl.SERVICE_DISCOVERY_URL, TIMEOUT, TIMEOUT, REFRESH, - true); - HiveMQEdgeRemoteEvent event = new HiveMQEdgeRemoteEvent(HiveMQEdgeRemoteEvent.EVENT_TYPE.EDGE_STARTED); - event.addAll(HiveMQEdgeEnvironmentUtils.generateEnvironmentMap()); - fireUsageEvent(event); - } finally { - if(logger.isTraceEnabled()){ - logger.trace("Initialized remote HTTP service(s), usage tracking enabled (this can be disabled in configuration)"); - } - } - } - @Override - public HiveMQEdgeRemoteConfiguration getConfiguration() { + public @NotNull HiveMQEdgeRemoteConfiguration getConfiguration() { //-- If enabled (and available), load the configuration from a remote endpoint, else //-- load the config from the local classpath - Optional optional = readConfigurationFromRemote(); - return optional.orElse(loadLocalConfiguration()); + return readConfigurationFromRemote().orElse(loadLocalConfiguration()); } @Override - public void fireUsageEvent(final HiveMQEdgeRemoteEvent event) { - if(hiveMQEdgeHttpService != null){ + public void fireUsageEvent(final @NotNull HiveMQEdgeRemoteEvent event) { + if (httpService != null) { //only queue if its a startup event - hiveMQEdgeHttpService.fireEvent(event, event.getEventType() == HiveMQEdgeRemoteEvent.EVENT_TYPE.EDGE_STARTED); + httpService.fireEvent(event, event.getEventType() == HiveMQEdgeRemoteEvent.EVENT_TYPE.EDGE_STARTED); + } + } + + private void initHttpService() { + try { + httpService = new HiveMQEdgeHttpServiceImpl(sysInfo.getHiveMQVersion(), + mapper, + HiveMQEdgeHttpServiceImpl.SERVICE_DISCOVERY_URL, + TIMEOUT, + TIMEOUT, + REFRESH, + true); + final HiveMQEdgeRemoteEvent event = + new HiveMQEdgeRemoteEvent(HiveMQEdgeRemoteEvent.EVENT_TYPE.EDGE_STARTED); + event.addAll(HiveMQEdgeEnvironmentUtils.generateEnvironmentMap()); + fireUsageEvent(event); + } finally { + if (log.isTraceEnabled()) { + log.trace( + "Initialized remote HTTP service(s), usage tracking enabled (this can be disabled in configuration)"); + } } } - protected HiveMQEdgeRemoteConfiguration loadLocalConfiguration() { + private @NotNull HiveMQEdgeRemoteConfiguration loadLocalConfiguration() { + lock.lock(); try { - if (localConfiguration == null) { - synchronized (lock) { - if (localConfiguration == null) { - try (final InputStream is = HiveMQRemoteServiceImpl.class.getResourceAsStream(LOCAL_RESOURCE_CONFIGURATION)) { - localConfiguration = objectMapper.readValue(is, HiveMQEdgeRemoteConfiguration.class); - if(logger.isTraceEnabled()){ - logger.trace("Loaded HiveMQEdge Configuration From Local Classpath {}", localConfiguration); - } - } + if (localConfig == null) { + try (final InputStream is = HiveMQRemoteServiceImpl.class.getResourceAsStream( + LOCAL_RESOURCE_CONFIGURATION)) { + localConfig = mapper.readValue(is, HiveMQEdgeRemoteConfiguration.class); + if (log.isTraceEnabled()) { + log.trace("Loaded HiveMQEdge Configuration From Local Classpath {}", localConfig); } } } - return localConfiguration; - } catch (IOException e) { - logger.error("Error Loading HiveMQEdge Configuration From Local Classpath", e); + return requireNonNull(localConfig); + } catch (final IOException e) { + log.error("Error Loading HiveMQEdge Configuration From Local Classpath", e); throw new RuntimeException("Error Loading HiveMQEdge Configuration from Classpath"); + } finally { + lock.unlock(); } } - protected @NotNull Optional readConfigurationFromRemote(){ - Optional remoteConfiguration = hiveMQEdgeHttpService != null ? - hiveMQEdgeHttpService.getRemoteConfiguration() : Optional.empty(); - if(logger.isTraceEnabled()){ - logger.trace("Loaded HiveMQ Edge Configuration Remote Available ? {}", - hiveMQEdgeHttpService != null && remoteConfiguration.isPresent()); + private @NotNull Optional readConfigurationFromRemote() { + final Optional remote = + httpService != null ? httpService.getRemoteConfiguration() : Optional.empty(); + if (log.isTraceEnabled()) { + log.trace("Loaded HiveMQ Edge Configuration Remote Available ? {}", + httpService != null && remote.isPresent()); } - return remoteConfiguration; + return remote; } @Override - public String name() { + public @NotNull String name() { return "HiveMQ Edge Remote Configuration Shutdown"; } @Override public void run() { try { - if(hiveMQEdgeHttpService != null) { - hiveMQEdgeHttpService.stop(); + if (httpService != null) { + httpService.stop(); } - } catch (Exception e) { - logger.error("Error shutting down remote configuration service", e); + } catch (final Throwable e) { + log.error("Error shutting down remote configuration service", e); } } } diff --git a/hivemq-edge/src/main/java/com/hivemq/edge/model/HiveMQEdgeRemoteConfiguration.java b/hivemq-edge/src/main/java/com/hivemq/edge/model/HiveMQEdgeRemoteConfiguration.java index aeac71ad6d..a812dbbacc 100644 --- a/hivemq-edge/src/main/java/com/hivemq/edge/model/HiveMQEdgeRemoteConfiguration.java +++ b/hivemq-edge/src/main/java/com/hivemq/edge/model/HiveMQEdgeRemoteConfiguration.java @@ -19,27 +19,32 @@ import com.fasterxml.jackson.annotation.JsonIgnoreProperties; import com.fasterxml.jackson.annotation.JsonProperty; import com.hivemq.api.model.components.Extension; -import com.hivemq.api.model.components.Module; import com.hivemq.api.model.components.Link; +import com.hivemq.api.model.components.Module; import org.jetbrains.annotations.NotNull; import java.util.List; import java.util.Map; -/** - * @author Simon L Johnson - */ @JsonIgnoreProperties(ignoreUnknown = true) public class HiveMQEdgeRemoteConfiguration { - private @JsonProperty("ctas") final @NotNull List ctas; - private @JsonProperty("resources") final @NotNull List resources; - private @JsonProperty("extensions") final @NotNull List extensions; - private @JsonProperty("modules") final @NotNull List modules; - private @JsonProperty("properties") final @NotNull Map properties; - private @JsonProperty("cloudLink") final @NotNull Link cloudLink; - private @JsonProperty("gitHubLink") final @NotNull Link gitHubLink; - private @JsonProperty("documentationLink") final @NotNull Link documentationLink; + private @JsonProperty("ctas") + final @NotNull List ctas; + private @JsonProperty("resources") + final @NotNull List resources; + private @JsonProperty("extensions") + final @NotNull List extensions; + private @JsonProperty("modules") + final @NotNull List modules; + private @JsonProperty("properties") + final @NotNull Map properties; + private @JsonProperty("cloudLink") + final @NotNull Link cloudLink; + private @JsonProperty("gitHubLink") + final @NotNull Link gitHubLink; + private @JsonProperty("documentationLink") + final @NotNull Link documentationLink; @JsonCreator(mode = JsonCreator.Mode.PROPERTIES) public HiveMQEdgeRemoteConfiguration( @@ -61,50 +66,57 @@ public HiveMQEdgeRemoteConfiguration( this.documentationLink = documentationLink; } - public List getCtas() { + public @NotNull List getCtas() { return ctas; } - public List getResources() { + public @NotNull List getResources() { return resources; } - public List getExtensions() { + public @NotNull List getExtensions() { return extensions; } - public List getModules() { + public @NotNull List getModules() { return modules; } - public Link getCloudLink() { + public @NotNull Link getCloudLink() { return cloudLink; } - public Link getGitHubLink() { + public @NotNull Link getGitHubLink() { return gitHubLink; } - public Link getDocumentationLink() { + public @NotNull Link getDocumentationLink() { return documentationLink; } - public Map getProperties() { + public @NotNull Map getProperties() { return properties; } @Override - public String toString() { - final StringBuilder sb = new StringBuilder("HiveMQEdgeRemoteConfiguration{"); - sb.append("ctas=").append(ctas); - sb.append(", resources=").append(resources); - sb.append(", extensions=").append(extensions); - sb.append(", modules=").append(modules); - sb.append(", properties=").append(properties); - sb.append(", cloudLink=").append(cloudLink); - sb.append(", documentationLink=").append(documentationLink); - sb.append(", gitHubLink=").append(gitHubLink); - sb.append('}'); - return sb.toString(); + public @NotNull String toString() { + return "HiveMQEdgeRemoteConfiguration{" + + "ctas=" + + ctas + + ", resources=" + + resources + + ", extensions=" + + extensions + + ", modules=" + + modules + + ", properties=" + + properties + + ", cloudLink=" + + cloudLink + + ", documentationLink=" + + documentationLink + + ", gitHubLink=" + + gitHubLink + + '}'; } } diff --git a/hivemq-edge/src/main/java/com/hivemq/util/Files.java b/hivemq-edge/src/main/java/com/hivemq/util/Files.java index 150f121bc9..5b2a7de4cb 100644 --- a/hivemq-edge/src/main/java/com/hivemq/util/Files.java +++ b/hivemq-edge/src/main/java/com/hivemq/util/Files.java @@ -17,12 +17,10 @@ import com.google.common.base.Preconditions; import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; import java.io.File; -/** - * @author Simon L Johnson - */ public class Files { static final String PERIOD = "."; @@ -38,13 +36,10 @@ private Files() { * @param filePath - the file path e.g. /some/file/location.txt * @return fileName - the fileName e.g. location.txt */ - public static String getFileName(@NotNull String filePath){ + public static String getFileName(final @NotNull String filePath) { Preconditions.checkNotNull(filePath); - int idx = filePath.lastIndexOf(File.separator); - if(idx > -1){ - filePath = filePath.substring(idx + 1); - } - return filePath; + final int idx = filePath.lastIndexOf(File.separator); + return idx > -1 ? filePath.substring(idx + 1) : filePath; } /** @@ -54,16 +49,13 @@ public static String getFileName(@NotNull String filePath){ * @param filePath - the file path e.g. /some/file/location.txt * @return fileName - the fileName e.g. location */ - public static String getFileNameExcludingExtension(final @NotNull String filePath){ + public static String getFileNameExcludingExtension(final @NotNull String filePath) { Preconditions.checkNotNull(filePath); - String name = getFileName(filePath); - if(name.contains(PERIOD)){ - name = name.substring(0, name.lastIndexOf(PERIOD) ); - } - return name; + final String name = getFileName(filePath); + final int idx = name.lastIndexOf(PERIOD); + return idx > -1 ? name.substring(0, idx) : name; } - /** * Given a file path, will return the extension of the file determined by the last * location of a period character @@ -71,29 +63,23 @@ public static String getFileNameExcludingExtension(final @NotNull String filePat * @param filePath - the file path e.g. /some/file/location.txt * @return fileName - the fileName e.g. txt */ - public static String getFileExtension(final @NotNull String filePath){ + public static @Nullable String getFileExtension(final @NotNull String filePath) { Preconditions.checkNotNull(filePath); - String name = getFileName(filePath); - int idx = name.lastIndexOf(PERIOD); - String ext = null; - if(idx > -1){ - ext = name.substring(idx + 1); - } - return ext; + final String name = getFileName(filePath); + final int idx = name.lastIndexOf(PERIOD); + return idx > -1 ? name.substring(idx + 1) : null; } /** * Given a file path, will return the directory of the file, determined by * the last location of the File.separator + * * @param filePath - the file path e.g. /some/file/location.txt * @return the directory of the file e.g. /some/file */ - public static String getFilePathExcludingFile(@NotNull String filePath){ + public static @NotNull String getFilePathExcludingFile(final @NotNull String filePath) { Preconditions.checkNotNull(filePath); - int idx = filePath.lastIndexOf(File.separator); - if(idx > -1){ - filePath = filePath.substring(0, idx); - } - return filePath; + final int idx = filePath.lastIndexOf(File.separator); + return idx > -1 ? filePath.substring(0, idx) : filePath; } } diff --git a/hivemq-edge/src/main/resources/config.xsd b/hivemq-edge/src/main/resources/config.xsd index 72b3918d0f..b591c5a320 100644 --- a/hivemq-edge/src/main/resources/config.xsd +++ b/hivemq-edge/src/main/resources/config.xsd @@ -1049,6 +1049,14 @@ + + + + + + + + diff --git a/hivemq-edge/src/test/java/com/hivemq/api/JaxrsResourceTests.java b/hivemq-edge/src/test/java/com/hivemq/api/JaxrsResourceTests.java index 1063acc1f6..13a0953d40 100644 --- a/hivemq-edge/src/test/java/com/hivemq/api/JaxrsResourceTests.java +++ b/hivemq-edge/src/test/java/com/hivemq/api/JaxrsResourceTests.java @@ -36,16 +36,13 @@ import static org.mockito.Mockito.mock; -/** - * @author Simon L Johnson - */ public class JaxrsResourceTests { protected final Logger logger = LoggerFactory.getLogger(JaxrsResourceTests.class); static final int TEST_HTTP_PORT = RandomPortGenerator.get(); - static final int CONNECT_TIMEOUT = 1000; - static final int READ_TIMEOUT = 1000; + static final int CONNECT_TIMEOUT = 5000; + static final int READ_TIMEOUT = 5000; static final String HTTP = "http"; static final String JSON_ENTITY = "{\"key\":\"value\"}"; diff --git a/hivemq-edge/src/test/java/com/hivemq/configuration/reader/ConfigFileReaderTest.java b/hivemq-edge/src/test/java/com/hivemq/configuration/reader/ConfigFileReaderTest.java index 122be8ef67..119d7f0182 100644 --- a/hivemq-edge/src/test/java/com/hivemq/configuration/reader/ConfigFileReaderTest.java +++ b/hivemq-edge/src/test/java/com/hivemq/configuration/reader/ConfigFileReaderTest.java @@ -132,7 +132,7 @@ public void whenUserPropertie_thenMapCorrectlyFilled() throws Exception { assertTrue(userProperties.contains(new MqttUserPropertyEntity("my-name", "my-value2"))); assertTrue(userProperties.contains(new MqttUserPropertyEntity("my-name", "my-value2"))); - configFileReader.writeConfig(); + configFileReader.writeConfigToXML(); final String afterReload = FileUtils.readFileToString(tempFile, UTF_8); assertThat(afterReload).contains("mqttUserProperty"); final @NotNull List config2 = hiveMQConfigEntity.getProtocolAdapterConfig(); diff --git a/hivemq-edge/src/test/java/com/hivemq/configuration/reader/ConfigFileReaderWriterTest.java b/hivemq-edge/src/test/java/com/hivemq/configuration/reader/ConfigFileReaderWriterTest.java index 50ee6b8da6..0d41665b9d 100644 --- a/hivemq-edge/src/test/java/com/hivemq/configuration/reader/ConfigFileReaderWriterTest.java +++ b/hivemq-edge/src/test/java/com/hivemq/configuration/reader/ConfigFileReaderWriterTest.java @@ -28,22 +28,22 @@ class ConfigFileReaderWriterTest { @Test - public void test_alltags() throws Exception{ + public void test_all_tags() throws Exception { final var systemInformation = mock(SystemInformation.class); when(systemInformation.isConfigFragmentBase64Zip()).thenReturn(false); final var reader = new ConfigFileReaderWriter(systemInformation, null, List.of()); final var configFile = new File(getClass().getClassLoader().getResource("configs/testing/alltags.xml").toURI()); - final var configEntity = reader.readConfigFromXML(configFile); + final var configEntity = reader.loadConfigFromXML(configFile); assertThat(configEntity).isNotNull(); } @Test - public void test_empty() throws Exception{ + public void test_empty() throws Exception { final var systemInformation = mock(SystemInformation.class); when(systemInformation.isConfigFragmentBase64Zip()).thenReturn(false); final var reader = new ConfigFileReaderWriter(systemInformation, null, List.of()); final var configFile = new File(getClass().getClassLoader().getResource("configs/testing/empty.xml").toURI()); - final var configEntity = reader.readConfigFromXML(configFile); + final var configEntity = reader.loadConfigFromXML(configFile); assertThat(configEntity).isNotNull(); } @@ -53,9 +53,8 @@ public void test_datacombiners_no_source() throws Exception{ when(systemInformation.isConfigFragmentBase64Zip()).thenReturn(false); final var reader = new ConfigFileReaderWriter(systemInformation, null, List.of()); final var configFile = new File(getClass().getClassLoader().getResource("configs/testing/datacombiners_no_source.xml").toURI()); - final var configEntity = reader.readConfigFromXML(configFile); + final var configEntity = reader.loadConfigFromXML(configFile); //This will break as soon as the xsd is fixed assertThat(configEntity).isNotNull(); } - } diff --git a/hivemq-edge/src/test/java/com/hivemq/configuration/reader/ProtocolAdapterExtractorTest.java b/hivemq-edge/src/test/java/com/hivemq/configuration/reader/ProtocolAdapterExtractorTest.java index aef406b339..eb386f5136 100644 --- a/hivemq-edge/src/test/java/com/hivemq/configuration/reader/ProtocolAdapterExtractorTest.java +++ b/hivemq-edge/src/test/java/com/hivemq/configuration/reader/ProtocolAdapterExtractorTest.java @@ -390,7 +390,7 @@ public void whenNoMappingsNoTags_setConfigurationShouldReturnTrue() throws IOExc final ProtocolAdapterEntity protocolAdapterEntity = new ProtocolAdapterEntity("adapterId", "protocolId", 1, Map.of(), List.of(), List.of(), List.of()); entity.getProtocolAdapterConfig().add(protocolAdapterEntity); - assertThat(configFileReader.setConfiguration(entity)).isTrue(); + assertThat(configFileReader.internalApplyConfig(entity)).isTrue(); } @Test @@ -406,7 +406,7 @@ public void whenNoMappings_setConfigurationShouldReturnTrue() throws IOException List.of(), List.of(new TagEntity("abc", "def", Map.of()))); entity.getProtocolAdapterConfig().add(protocolAdapterEntity); - assertThat(configFileReader.setConfiguration(entity)).isTrue(); + assertThat(configFileReader.internalApplyConfig(entity)).isTrue(); } @Test @@ -430,7 +430,7 @@ public void whenNoTags_setConfigurationShouldReturnFalse() throws IOException { List.of(), List.of()); entity.getProtocolAdapterConfig().add(protocolAdapterEntity); - assertThat(configFileReader.setConfiguration(entity)).isFalse(); + assertThat(configFileReader.internalApplyConfig(entity)).isFalse(); } @Test @@ -454,7 +454,7 @@ public void whenNorthboundMappingTagNameAreNotFound_setConfigurationShouldReturn List.of(), List.of(new TagEntity("abc", "def", Map.of()))); entity.getProtocolAdapterConfig().add(protocolAdapterEntity); - assertThat(configFileReader.setConfiguration(entity)).isFalse(); + assertThat(configFileReader.internalApplyConfig(entity)).isFalse(); } @ParameterizedTest @@ -482,7 +482,7 @@ public void whenNorthboundMappingTagNameOrTopicIsEmpty_setConfigurationShouldRet List.of(), List.of()); entity.getProtocolAdapterConfig().add(protocolAdapterEntity); - assertThat(configFileReader.setConfiguration(entity)).isFalse(); + assertThat(configFileReader.internalApplyConfig(entity)).isFalse(); } @Test @@ -500,7 +500,7 @@ public void whenSouthboundMappingTagNameIsNotFound_setConfigurationShouldReturnF List.of(southboundMappingEntity), List.of(new TagEntity("abc", "def", Map.of()))); entity.getProtocolAdapterConfig().add(protocolAdapterEntity); - assertThat(configFileReader.setConfiguration(entity)).isFalse(); + assertThat(configFileReader.internalApplyConfig(entity)).isFalse(); } @ParameterizedTest @@ -523,6 +523,6 @@ public void whenSouthboundMappingTagNameOrTopicFilterOrSchemaIsEmpty_setConfigur List.of(southboundMappingEntity), List.of()); entity.getProtocolAdapterConfig().add(protocolAdapterEntity); - assertThat(configFileReader.setConfiguration(entity)).isFalse(); + assertThat(configFileReader.internalApplyConfig(entity)).isFalse(); } } diff --git a/hivemq-edge/src/test/java/com/hivemq/configuration/writer/ConfigFileWriterTest.java b/hivemq-edge/src/test/java/com/hivemq/configuration/writer/ConfigFileWriterTest.java index 317492399d..21dc6bed47 100644 --- a/hivemq-edge/src/test/java/com/hivemq/configuration/writer/ConfigFileWriterTest.java +++ b/hivemq-edge/src/test/java/com/hivemq/configuration/writer/ConfigFileWriterTest.java @@ -45,11 +45,11 @@ public void rewriteUnchangedConfigurationYieldsSameXML() throws IOException, SAX final File tempCopyFile = new File(System.getProperty("java.io.tmpdir"), "copy-config.xml"); tempFile.deleteOnExit(); - configFileReader.writeConfig(new ConfigurationFile(tempCopyFile), false); + configFileReader.writeConfigToXML(new ConfigurationFile(tempCopyFile).file().get()); final String copiedFileContent = FileUtils.readFileToString(tempCopyFile, UTF_8); final Diff diff = XMLUnit.compareXML(originalXml, copiedFileContent); - if(!diff.identical()){ + if (!diff.identical()) { System.err.println("xml diff found " + diff); System.err.println(originalXml); System.err.println(copiedFileContent); diff --git a/hivemq-edge/src/test/resources/test-config.xml b/hivemq-edge/src/test/resources/test-config.xml index 41055f64dc..db01ceba2d 100644 --- a/hivemq-edge/src/test/resources/test-config.xml +++ b/hivemq-edge/src/test/resources/test-config.xml @@ -190,6 +190,9 @@ + + false +