diff --git a/src/main/java/io/mapsmessaging/logging/ServerLogMessages.java b/src/main/java/io/mapsmessaging/logging/ServerLogMessages.java index be84f30b9..c2251f52d 100644 --- a/src/main/java/io/mapsmessaging/logging/ServerLogMessages.java +++ b/src/main/java/io/mapsmessaging/logging/ServerLogMessages.java @@ -933,6 +933,7 @@ public enum ServerLogMessages implements LogMessage { STATE_MANAGER_PUBLISH_ENABLED(LEVEL.INFO, SERVER_CATEGORY.STATE, "Twin JSON publisher enabled with topic {}"), STATE_MANAGER_PUBLISH_FAILED(LEVEL.ERROR, SERVER_CATEGORY.STATE, "Failed to start Twin JSON publisher"), STATE_MANAGER_SCHEDULER_ERROR(LEVEL.ERROR, SERVER_CATEGORY.STATE, "Scheduler task failed"), + STATE_MANAGER_AUDIT_INIT_FAILED(LEVEL.ERROR, SERVER_CATEGORY.STATE, "Failed to initialize audit context - auditing will be disabled"), // //------------------------------------------------------------------------------------------------------------- diff --git a/src/main/java/io/mapsmessaging/state/GsonStanagHelper.java b/src/main/java/io/mapsmessaging/state/StateJsonHelper.java similarity index 66% rename from src/main/java/io/mapsmessaging/state/GsonStanagHelper.java rename to src/main/java/io/mapsmessaging/state/StateJsonHelper.java index bd2313dac..ae6bb0d5b 100644 --- a/src/main/java/io/mapsmessaging/state/GsonStanagHelper.java +++ b/src/main/java/io/mapsmessaging/state/StateJsonHelper.java @@ -24,17 +24,16 @@ import io.mapsmessaging.rest.translation.GsonDateTimeDeserialiser; import io.mapsmessaging.rest.translation.GsonDateTimeSerialiser; import io.mapsmessaging.rest.translation.InstantTypeAdapter; -import io.mapsmessaging.state.config.capability.*; -import io.mapsmessaging.state.stanag.messages.core.MessageType; -import io.mapsmessaging.state.stanag.messages.task.admin.TaskAdminActionEnum; -import io.mapsmessaging.state.stanag.messages.task.result.ResultReason; -import io.mapsmessaging.state.stanag.messages.TaskState; - +import io.mapsmessaging.state.config.capability.PlanTaskType; +import io.mapsmessaging.state.config.capability.PrefixedEnumTypeAdapter; +import io.mapsmessaging.state.config.capability.TaskConditionMode; +import io.mapsmessaging.state.config.capability.TaskSpecialization; +import io.mapsmessaging.state.config.capability.TaskTemplateMode; import java.time.Instant; import java.time.LocalDate; import java.time.LocalDateTime; -public class GsonStanagHelper { +public class StateJsonHelper { public static Gson createGson() { return new GsonBuilder() @@ -60,30 +59,10 @@ public static Gson createGson() { TaskTemplateMode.class, new PrefixedEnumTypeAdapter<>(TaskTemplateMode.class, "TaskTemplateModeEnum_") ) - .registerTypeAdapter( - MessageType.class, - new PrefixedEnumTypeAdapter<>(MessageType.class, "MessageTypeEnum_") - ) - .registerTypeAdapter( - TaskState.class, - new PrefixedEnumTypeAdapter<>(TaskState.class, "TaskStateEnum_") - ) - .registerTypeAdapter( - ResultReason.class, - new PrefixedEnumTypeAdapter<>(ResultReason.class, "ResultReasonEnum_") - ) - .registerTypeAdapter( - TaskAdminActionEnum.class, - new PrefixedEnumTypeAdapter<>(TaskAdminActionEnum.class, "TaskAdminActionEnum_") - ) - .registerTypeAdapter( - ResultReason.class, - new PrefixedEnumTypeAdapter<>(ResultReason.class, "ResultReasonEnum_") - ) .create(); } - private GsonStanagHelper() { + private StateJsonHelper() { // helper only } } diff --git a/src/main/java/io/mapsmessaging/state/StateManagerAgent.java b/src/main/java/io/mapsmessaging/state/StateManagerAgent.java index ba8ad184a..28db55e96 100644 --- a/src/main/java/io/mapsmessaging/state/StateManagerAgent.java +++ b/src/main/java/io/mapsmessaging/state/StateManagerAgent.java @@ -29,10 +29,11 @@ import io.mapsmessaging.dto.rest.system.SubSystemStatusDTO; import io.mapsmessaging.logging.Logger; import io.mapsmessaging.logging.LoggerFactory; +import io.mapsmessaging.state.adapter.StateMessageAdapterContext; +import io.mapsmessaging.state.adapter.StateMessageAdapterFactory; +import io.mapsmessaging.state.auditor.StateAuditContext; import io.mapsmessaging.state.drone.core.TwinLifecycleStatus; import io.mapsmessaging.state.drone.core.TwinManager; -import io.mapsmessaging.state.stanag.StanagSession; -import io.mapsmessaging.state.stanag.audit.Auditor; import io.mapsmessaging.utilities.Agent; import io.mapsmessaging.utilities.Lifecycle; import io.mapsmessaging.utilities.configuration.ConfigurationManager; @@ -42,6 +43,7 @@ import java.nio.file.Path; import java.util.ArrayList; import java.util.List; +import java.util.ServiceLoader; import static io.mapsmessaging.logging.ServerLogMessages.*; @@ -59,32 +61,28 @@ public class StateManagerAgent implements Agent { private TwinManager twinManager; @Getter - private Auditor auditor; + private StateAuditContext auditContext; private AuditorFactory.AuditorInstance auditorInstance; public StateManagerAgent() { TwinManagerConfigDTO config = ConfigurationManager.getInstance().getConfiguration(TwinManagerConfig.class); DroneInfoRegistry registry; TakProtocolDTO takConfig; - StanagConfig stanagConfig; if(config != null){ try { AuditorFactory factory = new AuditorFactory(); auditorInstance = factory.build(Path.of( EnvironmentConfig.getInstance().getPathLookups().get("MAPS_DATA"))); - auditor = auditorInstance.getAuditor(); + auditContext = auditorInstance.getAuditContext(); } catch (IOException e) { - e.printStackTrace(); + logger.log(STATE_MANAGER_AUDIT_INIT_FAILED, e); } - twinManager = new TwinManager(config.isRemoveExpiredTwins(), config.getStaleTimeoutMillis(), config.getHeartbeatTimeoutMillis(), config.getRetentionTimeoutMillis(), auditor); + twinManager = new TwinManager(config.isRemoveExpiredTwins(), config.getStaleTimeoutMillis(), config.getHeartbeatTimeoutMillis(), config.getRetentionTimeoutMillis(), auditContext); registry = new DroneInfoRegistry(config.getDroneInfo()); takConfig = config.getTak(); - stanagConfig = config.getStanagConfig(); lifecycleList.add(new SchedulerManager(twinManager)); lifecycleList.add(new TakManager(twinManager, takConfig)); lifecycleList.add(new MavlinkTwinManager(twinManager, registry, config)); - if(stanagConfig != null && stanagConfig.isEnable()) { - lifecycleList.add(new StanagSession(twinManager, stanagConfig)); - } + loadStateMessageAdapters(config); lifecycleList.add(new TwinPublisherManager(twinManager, config.getPublish())); aisManager = new AISN2KManager(twinManager, config.getN2KTwinConfig()); lifecycleList.add(aisManager); @@ -104,6 +102,14 @@ public String getDescription() { return "Manages state of known objects within memory and maintains a digital twin"; } + private void loadStateMessageAdapters(TwinManagerConfigDTO config) { + StateMessageAdapterContext context = new StateMessageAdapterContext(twinManager, config); + ServiceLoader adapterFactories = ServiceLoader.load(StateMessageAdapterFactory.class); + for (StateMessageAdapterFactory adapterFactory : adapterFactories) { + adapterFactory.create(context).ifPresent(lifecycleList::add); + } + } + @Override public synchronized void start() { try { @@ -141,4 +147,4 @@ public SubSystemStatusDTO getStatus() { status.setStatus(Status.OK); return status; } -} \ No newline at end of file +} diff --git a/src/main/java/io/mapsmessaging/state/stanag/tasks/monitor/TaskMonitorState.java b/src/main/java/io/mapsmessaging/state/adapter/StateMessageAdapter.java similarity index 81% rename from src/main/java/io/mapsmessaging/state/stanag/tasks/monitor/TaskMonitorState.java rename to src/main/java/io/mapsmessaging/state/adapter/StateMessageAdapter.java index 56ac571bb..fcd3c3910 100644 --- a/src/main/java/io/mapsmessaging/state/stanag/tasks/monitor/TaskMonitorState.java +++ b/src/main/java/io/mapsmessaging/state/adapter/StateMessageAdapter.java @@ -17,13 +17,11 @@ * limitations under the License. */ -package io.mapsmessaging.state.stanag.tasks.monitor; +package io.mapsmessaging.state.adapter; -public enum TaskMonitorState { - ACCEPTED, - IN_PROGRESS, - COMPLETE, - FAILED, - TIMEOUT, - LOST -} \ No newline at end of file +import io.mapsmessaging.utilities.Lifecycle; + +public interface StateMessageAdapter extends Lifecycle { + + String getName(); +} diff --git a/src/main/java/io/mapsmessaging/state/stanag/messages/node/common/Altitude.java b/src/main/java/io/mapsmessaging/state/adapter/StateMessageAdapterContext.java similarity index 62% rename from src/main/java/io/mapsmessaging/state/stanag/messages/node/common/Altitude.java rename to src/main/java/io/mapsmessaging/state/adapter/StateMessageAdapterContext.java index ff58d2159..c80afe0d9 100644 --- a/src/main/java/io/mapsmessaging/state/stanag/messages/node/common/Altitude.java +++ b/src/main/java/io/mapsmessaging/state/adapter/StateMessageAdapterContext.java @@ -17,16 +17,24 @@ * limitations under the License. */ -package io.mapsmessaging.state.stanag.messages.node.common; +package io.mapsmessaging.state.adapter; -import lombok.Builder; +import io.mapsmessaging.state.config.TwinManagerConfigDTO; +import io.mapsmessaging.state.drone.core.TwinManager; import lombok.Getter; +import lombok.NonNull; +import lombok.RequiredArgsConstructor; +import org.jetbrains.annotations.NotNull; @Getter -@Builder -public class Altitude { +@RequiredArgsConstructor +public class StateMessageAdapterContext { - private final Double value; + @NonNull + @NotNull + private final TwinManager twinManager; - private final String type; -} \ No newline at end of file + @NonNull + @NotNull + private final TwinManagerConfigDTO config; +} diff --git a/src/main/java/io/mapsmessaging/state/stanag/TaskMessageSender.java b/src/main/java/io/mapsmessaging/state/adapter/StateMessageAdapterFactory.java similarity index 77% rename from src/main/java/io/mapsmessaging/state/stanag/TaskMessageSender.java rename to src/main/java/io/mapsmessaging/state/adapter/StateMessageAdapterFactory.java index 21515b066..552a717ac 100644 --- a/src/main/java/io/mapsmessaging/state/stanag/TaskMessageSender.java +++ b/src/main/java/io/mapsmessaging/state/adapter/StateMessageAdapterFactory.java @@ -17,10 +17,13 @@ * limitations under the License. */ -package io.mapsmessaging.state.stanag; +package io.mapsmessaging.state.adapter; -import io.mapsmessaging.api.message.Message; +import java.util.Optional; -public interface TaskMessageSender { - void sendTaskMessage(String taskTopic, Message message); +public interface StateMessageAdapterFactory { + + String getName(); + + Optional create(StateMessageAdapterContext context); } diff --git a/src/main/java/io/mapsmessaging/state/auditor/AuditorFactory.java b/src/main/java/io/mapsmessaging/state/auditor/AuditorFactory.java index 3cd8590ac..c5579c3f9 100644 --- a/src/main/java/io/mapsmessaging/state/auditor/AuditorFactory.java +++ b/src/main/java/io/mapsmessaging/state/auditor/AuditorFactory.java @@ -33,7 +33,6 @@ import java.security.PrivateKey; import java.security.interfaces.EdECPublicKey; -import io.mapsmessaging.state.stanag.audit.Auditor; import lombok.Getter; public class AuditorFactory { @@ -110,13 +109,13 @@ public AuditorInstance build( AuditLogger auditLogger = new AuditLogger(auditJournal); AuditPayloadStore auditPayloadStore = new AuditPayloadStore(payloadRootDirectory); - Auditor auditor = new Auditor( + StateAuditContext auditContext = new StateAuditContext( auditLogger, auditPayloadStore ); return new AuditorInstance( - auditor, + auditContext, auditJournal, auditRootDirectory, journalRootDirectory, @@ -179,7 +178,7 @@ private record AuditKeys( @Getter public static class AuditorInstance implements AutoCloseable { - private final Auditor auditor; + private final StateAuditContext auditContext; private final AuditJournal auditJournal; private final Path auditRootDirectory; private final Path journalRootDirectory; @@ -188,7 +187,7 @@ public static class AuditorInstance implements AutoCloseable { private final Path publicKeyPath; private AuditorInstance( - Auditor auditor, + StateAuditContext auditContext, AuditJournal auditJournal, Path auditRootDirectory, Path journalRootDirectory, @@ -196,7 +195,7 @@ private AuditorInstance( Path privateKeyPath, Path publicKeyPath ) { - this.auditor = auditor; + this.auditContext = auditContext; this.auditJournal = auditJournal; this.auditRootDirectory = auditRootDirectory; this.journalRootDirectory = journalRootDirectory; @@ -210,4 +209,4 @@ public void close() throws IOException { auditJournal.close(); } } -} \ No newline at end of file +} diff --git a/src/main/java/io/mapsmessaging/state/stanag/messages/FlexibleEnumeration.java b/src/main/java/io/mapsmessaging/state/auditor/StateAuditContext.java similarity index 76% rename from src/main/java/io/mapsmessaging/state/stanag/messages/FlexibleEnumeration.java rename to src/main/java/io/mapsmessaging/state/auditor/StateAuditContext.java index f401b1c59..4535e6445 100644 --- a/src/main/java/io/mapsmessaging/state/stanag/messages/FlexibleEnumeration.java +++ b/src/main/java/io/mapsmessaging/state/auditor/StateAuditContext.java @@ -17,15 +17,10 @@ * limitations under the License. */ -package io.mapsmessaging.state.stanag.messages; +package io.mapsmessaging.state.auditor; -import lombok.AllArgsConstructor; -import lombok.Getter; -import lombok.Setter; +import io.mapsmessaging.audit.AuditLogger; +import io.mapsmessaging.audit.AuditPayloadStore; -@Getter -@Setter -@AllArgsConstructor -public class FlexibleEnumeration { - private String name; -} \ No newline at end of file +public record StateAuditContext(AuditLogger auditLogger, AuditPayloadStore auditPayloadStore) { +} diff --git a/src/main/java/io/mapsmessaging/state/config/StanagConfig.java b/src/main/java/io/mapsmessaging/state/config/StanagConfig.java deleted file mode 100644 index 2c867a013..000000000 --- a/src/main/java/io/mapsmessaging/state/config/StanagConfig.java +++ /dev/null @@ -1,34 +0,0 @@ -/* - * - * Copyright [ 2020 - 2024 ] Matthew Buckton - * Copyright [ 2024 - 2026 ] MapsMessaging B.V. - * - * Licensed under the Apache License, Version 2.0 with the Commons Clause - * (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 - * https://commonsclause.com/ - * - * 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 io.mapsmessaging.state.config; - -import lombok.Getter; -import lombok.Setter; - -@Getter -@Setter -public class StanagConfig { - private boolean enable = false; - private String taskTopic; - private String chatTopic; - private String taskTopicTemplate; - private int descriptionIntervalSec = 30; - private int statusIntervalSec = 5; -} diff --git a/src/main/java/io/mapsmessaging/state/config/TwinManagerConfig.java b/src/main/java/io/mapsmessaging/state/config/TwinManagerConfig.java index 2a5b939ea..4c7f787ae 100644 --- a/src/main/java/io/mapsmessaging/state/config/TwinManagerConfig.java +++ b/src/main/java/io/mapsmessaging/state/config/TwinManagerConfig.java @@ -33,6 +33,7 @@ import io.mapsmessaging.utilities.configuration.ConfigurationManager; import java.io.IOException; import java.util.ArrayList; +import java.util.LinkedHashMap; import java.util.List; import java.util.UUID; @@ -41,6 +42,8 @@ @NoArgsConstructor public class TwinManagerConfig extends TwinManagerConfigDTO implements Config, ConfigManager { + private static final String STATE_ADAPTERS_CONFIG_KEY = "stateAdapters"; + private TwinManagerConfig(ConfigurationProperties properties) { this.heartbeatTimeoutMillis = properties.getLongProperty("heartbeatTimeoutMillis", heartbeatTimeoutMillis); this.staleTimeoutMillis = properties.getLongProperty("staleTimeoutMillis", staleTimeoutMillis); @@ -74,21 +77,8 @@ private TwinManagerConfig(ConfigurationProperties properties) { this.mavlink = parseMavlinkConfigs(properties.get("mavlink")); } - if(properties.containsKey("stanag")) { - ConfigurationProperties stanagProps = (ConfigurationProperties) properties.get("stanag"); - this.stanagConfig = new StanagConfig(); - stanagConfig.setEnable(stanagProps.getBooleanProperty("enabled", stanagConfig.isEnable())); - stanagConfig.setChatTopic(stanagProps.getProperty("chatTopic", null)); - stanagConfig.setTaskTopic(stanagProps.getProperty("taskTopic", null)); - stanagConfig.setTaskTopicTemplate(stanagProps.getProperty("taskTopicTemplate", null)); - stanagConfig.setDescriptionIntervalSec(stanagProps.getIntProperty("descriptionIntervalSec", stanagConfig.getDescriptionIntervalSec())); - stanagConfig.setStatusIntervalSec(stanagProps.getIntProperty("statusIntervalSec", stanagConfig.getStatusIntervalSec())); - } - else{ - stanagConfig = new StanagConfig(); - stanagConfig.setChatTopic(null); - stanagConfig.setTaskTopic(null); - stanagConfig.setTaskTopicTemplate(null); + if (properties.containsKey(STATE_ADAPTERS_CONFIG_KEY)) { + parseAdapterConfigs(properties.get(STATE_ADAPTERS_CONFIG_KEY)); } n2KTwinConfig = new N2KTwinConfig(); @@ -154,6 +144,11 @@ public ConfigurationProperties toConfigurationProperties() { if (this.mavlink != null && !this.mavlink.isEmpty()) { props.put("mavlink", toMavlinkConfigurationProperties(this.mavlink)); } + + if (this.adapterConfig != null && !this.adapterConfig.isEmpty()) { + props.put(STATE_ADAPTERS_CONFIG_KEY, new ConfigurationProperties(new LinkedHashMap<>(this.adapterConfig))); + } + ConfigurationProperties n2kProps = new ConfigurationProperties(); n2kProps.put("publishMavlinkDrones", this.n2KTwinConfig.isPublishMavlinkDrones()); n2kProps.put("ais", n2KTwinConfig.getAis()); @@ -232,9 +227,30 @@ public boolean update(BaseConfigDTO config) { hasChanged = true; } + if (newConfig.getAdapterConfig() != null && !this.adapterConfig.equals(newConfig.getAdapterConfig())) { + this.adapterConfig = new LinkedHashMap<>(newConfig.getAdapterConfig()); + hasChanged = true; + } + return hasChanged; } + private void parseAdapterConfigs(Object value) { + if (value instanceof ConfigurationProperties properties) { + for (var entry : properties.entrySet()) { + if (entry.getValue() instanceof ConfigurationProperties adapterProperties) { + this.adapterConfig.put(entry.getKey(), adapterProperties); + } + } + } else if(value instanceof java.util.Map entries) { + for (var entry : entries.entrySet()) { + if (entry.getKey() instanceof String adapterName && entry.getValue() instanceof ConfigurationProperties adapterProperties) { + this.adapterConfig.put(adapterName, adapterProperties); + } + } + } + } + private List parseMavlinkConfigs(Object value) { List configs = new ArrayList<>(); @@ -509,4 +525,4 @@ private List toKnownSourceConfigurationProperties(List< return values; } -} \ No newline at end of file +} diff --git a/src/main/java/io/mapsmessaging/state/config/TwinManagerConfigDTO.java b/src/main/java/io/mapsmessaging/state/config/TwinManagerConfigDTO.java index ea4961d65..d0c9e943e 100644 --- a/src/main/java/io/mapsmessaging/state/config/TwinManagerConfigDTO.java +++ b/src/main/java/io/mapsmessaging/state/config/TwinManagerConfigDTO.java @@ -21,13 +21,16 @@ import io.mapsmessaging.dto.rest.config.BaseManagerConfigDTO; import io.mapsmessaging.dto.rest.config.protocol.impl.TakProtocolDTO; +import io.mapsmessaging.configuration.ConfigurationProperties; import io.mapsmessaging.state.config.n2k.N2KTwinConfig; import lombok.Data; import io.swagger.v3.oas.annotations.media.Schema; import lombok.EqualsAndHashCode; import java.util.ArrayList; +import java.util.LinkedHashMap; import java.util.List; +import java.util.Map; @Data @EqualsAndHashCode(callSuper = true) @@ -101,9 +104,9 @@ public class TwinManagerConfigDTO extends BaseManagerConfigDTO { @Schema( - description = "Stanag configuration" + description = "Protocol-specific state message adapter configuration keyed by adapter name" ) - protected StanagConfig stanagConfig; + protected Map adapterConfig = new LinkedHashMap<>(); @Schema( description = "Configuration on each drone" @@ -122,4 +125,4 @@ public TwinManagerConfigDTO() { public String getSimpleName() { return "State Cache"; } -} \ No newline at end of file +} diff --git a/src/main/java/io/mapsmessaging/state/drone/core/TwinManager.java b/src/main/java/io/mapsmessaging/state/drone/core/TwinManager.java index a5705154d..25197bf2a 100644 --- a/src/main/java/io/mapsmessaging/state/drone/core/TwinManager.java +++ b/src/main/java/io/mapsmessaging/state/drone/core/TwinManager.java @@ -5,7 +5,7 @@ import io.mapsmessaging.logging.Logger; import io.mapsmessaging.logging.LoggerFactory; -import io.mapsmessaging.state.stanag.audit.Auditor; +import io.mapsmessaging.state.auditor.StateAuditContext; import lombok.Getter; import java.time.Instant; @@ -27,7 +27,7 @@ public class TwinManager { private final CopyOnWriteArrayList observers = new CopyOnWriteArrayList<>(); private final Logger logger = LoggerFactory.getLogger(TwinManager.class); @Getter - private final Auditor auditor; + private final StateAuditContext auditContext; private final boolean removeExpiredTwins; private final long staleTimeoutMillis; private final long heartbeatTimeoutMillis; @@ -37,8 +37,8 @@ public TwinManager() { this(true, 10000L, 5000L, 120000L, null); } - public TwinManager(boolean removeExpiredTwins, long staleTimeoutMillis, long heartbeatTimeoutMillis, long retentionTimeoutMillis, Auditor auditor) { - this.auditor = auditor; + public TwinManager(boolean removeExpiredTwins, long staleTimeoutMillis, long heartbeatTimeoutMillis, long retentionTimeoutMillis, StateAuditContext auditContext) { + this.auditContext = auditContext; this.removeExpiredTwins = removeExpiredTwins; this.staleTimeoutMillis = staleTimeoutMillis; this.heartbeatTimeoutMillis = heartbeatTimeoutMillis; @@ -388,4 +388,4 @@ private void notifyRelationshipRemoved(String twinId, } } } -} \ No newline at end of file +} diff --git a/src/main/java/io/mapsmessaging/state/drone/publisher/TwinJsonPublisher.java b/src/main/java/io/mapsmessaging/state/drone/publisher/TwinJsonPublisher.java index 449c2ec31..d893b08b5 100644 --- a/src/main/java/io/mapsmessaging/state/drone/publisher/TwinJsonPublisher.java +++ b/src/main/java/io/mapsmessaging/state/drone/publisher/TwinJsonPublisher.java @@ -32,7 +32,7 @@ import io.mapsmessaging.api.features.QualityOfService; import io.mapsmessaging.engine.schema.SchemaManager; import io.mapsmessaging.engine.session.ClientConnection; -import io.mapsmessaging.state.GsonStanagHelper; +import io.mapsmessaging.state.StateJsonHelper; import io.mapsmessaging.state.drone.core.EntityTwin; import io.mapsmessaging.state.drone.core.TwinManager; import io.mapsmessaging.state.drone.core.TwinObserver; @@ -64,7 +64,7 @@ public class TwinJsonPublisher implements TwinObserver, ClientConnection, Messag public TwinJsonPublisher(TwinManager twinManager, String topicTemplate) throws ExecutionException, InterruptedException, TimeoutException { this.topicTemplate = topicTemplate; this.twinManager = twinManager; - this.gson = GsonStanagHelper.createGson(); + this.gson = StateJsonHelper.createGson(); this.destinationCache = new ConcurrentHashMap<>(); this.session = createSession(); twinManager.addObserver(this); @@ -224,4 +224,4 @@ public String getRemoteIp() { public void sendMessage(@NotNull @NonNull MessageEvent messageEvent) { // no-op } -} \ No newline at end of file +} diff --git a/src/main/java/io/mapsmessaging/state/stanag/ChatListener.java b/src/main/java/io/mapsmessaging/state/stanag/ChatListener.java deleted file mode 100644 index 2fe740f68..000000000 --- a/src/main/java/io/mapsmessaging/state/stanag/ChatListener.java +++ /dev/null @@ -1,39 +0,0 @@ -/* - * - * Copyright [ 2020 - 2024 ] Matthew Buckton - * Copyright [ 2024 - 2026 ] MapsMessaging B.V. - * - * Licensed under the Apache License, Version 2.0 with the Commons Clause - * (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 - * https://commonsclause.com/ - * - * 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 io.mapsmessaging.state.stanag; - -import com.google.gson.JsonObject; -import io.mapsmessaging.state.StateLoopProtocol; - -import java.util.function.Consumer; - -public class ChatListener implements Consumer { - - private final StateLoopProtocol protocol; - - public ChatListener(StateLoopProtocol protocol) { - this.protocol = protocol; - } - - @Override - public void accept(JsonObject jsonObject) { - System.out.println(jsonObject); - } -} diff --git a/src/main/java/io/mapsmessaging/state/stanag/NodeUpdate.java b/src/main/java/io/mapsmessaging/state/stanag/NodeUpdate.java deleted file mode 100644 index 89190d469..000000000 --- a/src/main/java/io/mapsmessaging/state/stanag/NodeUpdate.java +++ /dev/null @@ -1,199 +0,0 @@ -/* - * - * Copyright [ 2020 - 2024 ] Matthew Buckton - * Copyright [ 2024 - 2026 ] MapsMessaging B.V. - * - * Licensed under the Apache License, Version 2.0 with the Commons Clause - * (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 - * https://commonsclause.com/ - * - * 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 io.mapsmessaging.state.stanag; - -import com.google.gson.Gson; -import io.mapsmessaging.MessageDaemon; -import io.mapsmessaging.api.Destination; -import io.mapsmessaging.api.MessageBuilder; -import io.mapsmessaging.api.Session; -import io.mapsmessaging.api.features.DestinationType; -import io.mapsmessaging.api.features.QualityOfService; -import io.mapsmessaging.engine.schema.SchemaManager; -import io.mapsmessaging.security.uuid.NamedVersions; -import io.mapsmessaging.state.GsonStanagHelper; -import io.mapsmessaging.state.config.StanagConfig; -import io.mapsmessaging.state.drone.core.EntityTwin; -import io.mapsmessaging.state.drone.core.TwinObserver; -import io.mapsmessaging.state.drone.core.TwinUpdateContext; -import io.mapsmessaging.state.drone.core.UuidFactory; -import io.mapsmessaging.state.drone.drone.DroneTwin; -import io.mapsmessaging.state.drone.model.Contact; -import io.mapsmessaging.state.drone.util.UuidGenerator; -import io.mapsmessaging.state.stanag.messages.core.MessageHeaderBuilder; -import io.mapsmessaging.state.stanag.messages.node.common.NodeMessageSupport; -import io.mapsmessaging.state.stanag.messages.node.description.NodeDescriptionBuilder; -import io.mapsmessaging.state.stanag.messages.node.dynamic.DynamicUpdate; -import io.mapsmessaging.state.stanag.messages.node.dynamic.DynamicUpdateBuilder; -import io.mapsmessaging.state.stanag.messages.node.status.NodeStatusBuilder; -import lombok.Getter; -import lombok.Setter; - -import java.io.IOException; -import java.nio.charset.StandardCharsets; -import java.time.Clock; -import java.util.*; -import java.util.concurrent.ConcurrentHashMap; -import java.util.concurrent.ExecutionException; -import java.util.concurrent.TimeUnit; -import java.util.concurrent.TimeoutException; - -public class NodeUpdate implements TwinObserver { - - private final Session session; - private final Map statusMap; - private final long descriptionInterval; - private final long statusInterval; - - private final String topicTemplate; - private final NodeDescriptionBuilder nodeDescriptionBuilder; - private final NodeStatusBuilder nodeStatusBuilder; - private final DynamicUpdateBuilder dynamicUpdateBuilder; - private final Gson gson; - - public NodeUpdate(Session session, StanagConfig stanagConfig) { - this.session = session; - statusMap = new ConcurrentHashMap<>(); - this.descriptionInterval = stanagConfig.getDescriptionIntervalSec() * 1000L; - this.statusInterval = stanagConfig.getStatusIntervalSec() * 1000L; - this.topicTemplate = stanagConfig.getTaskTopicTemplate(); - MessageHeaderBuilder messageHeaderBuilder = new MessageHeaderBuilder(Clock.systemUTC()); - NodeMessageSupport nodeMessageSupport = new NodeMessageSupport(); - nodeStatusBuilder = new NodeStatusBuilder(messageHeaderBuilder, nodeMessageSupport); - nodeDescriptionBuilder = new NodeDescriptionBuilder(messageHeaderBuilder, nodeMessageSupport); - dynamicUpdateBuilder = new DynamicUpdateBuilder(messageHeaderBuilder, nodeMessageSupport); - gson = GsonStanagHelper.createGson(); - } - - @Override - public void onTwinAdded(EntityTwin twin, TwinUpdateContext context) { - TwinStatus status = statusMap.get(twin.getTwinId()); - if(status == null) { - statusMap.put(twin.getTwinId(), new TwinStatus()); - } - else{ - status.setLastStatusUpdate(System.currentTimeMillis()); - status.setLastDescriptionUpdate(0); - } - } - - @Override - public void onTwinUpdated(String twinId, EntityTwin current, TwinUpdateContext context) { - TwinStatus status = statusMap.get(twinId); - long currentTime = System.currentTimeMillis(); - if(status != null) { - if(currentTime - status.getLastDescriptionUpdate() > descriptionInterval) { - if(current.getGeoPosition() != null && current.getGeoPosition().getLatitude() != null && current.getGeoPosition().getLongitude() != null) { - sendDescription(status, current); - status.setLastDescriptionUpdate(currentTime); - status.setSentDescription(true); - } - } - else if(currentTime - status.getLastStatusUpdate() > statusInterval && status.isSentDescription()) { - sendStatus(status, current); - status.setLastStatusUpdate(currentTime); - List contactList = ((DroneTwin)current).getContactList(); - if(contactList != null && !contactList.isEmpty()) { - sendContacts((DroneTwin)current, status, contactList); - } - } - } - else{ - onTwinAdded(current, context); - } - } - - private void sendContacts(DroneTwin droneTwin, TwinStatus status, List contactList) { - for (Contact contact : contactList) { - Optional dynamicUpdate = dynamicUpdateBuilder.build(droneTwin, contact); - if (dynamicUpdate.isPresent()) { - String json = gson.toJson(dynamicUpdate.get()); - String topicName = computeTopic(droneTwin.getUuid().toString(), "MessageTypeEnum_DYNAMIC_UPDATE"); - publish(status, topicName, json); - } - } - } - - private void sendStatus(TwinStatus status, EntityTwin current) { - String jsonStatus = gson.toJsonTree(nodeStatusBuilder.build((DroneTwin) current)).getAsJsonObject().toString(); - String topic = computeTopic(current.getUuid().toString(), "MessageTypeEnum_NODE_STATUS"); - publish(status, topic, jsonStatus); - } - - private void sendDescription(TwinStatus status, EntityTwin current) { - String jsonStatus = gson.toJsonTree(nodeDescriptionBuilder.build((DroneTwin) current)).getAsJsonObject().toString(); - String topic = computeTopic(current.getUuid().toString(), "MessageTypeEnum_NODE_DESCRIPTION"); - publish(status, topic, jsonStatus); - } - - private void publish(TwinStatus status, String topic, String json) { - try { - Destination destination = status.destinationMap.get(topic); - if(destination == null){ - destination = findDestination(topic); - status.destinationMap.put(topic, destination); - } - MessageBuilder messageBuilder = new MessageBuilder(); - messageBuilder.setOpaqueData(json.getBytes(StandardCharsets.UTF_8)) - .setQoS(QualityOfService.AT_MOST_ONCE) - .setSchemaId(SchemaManager.DEFAULT_JSON_SCHEMA.toString()); - destination.storeMessage(messageBuilder.build()); - } catch (IOException e) { - // log this - } - } - - - private Destination findDestination(String topicName) throws IOException{ - try { - return session.findDestination(topicName, DestinationType.TOPIC).get(30, TimeUnit.SECONDS); - } catch (InterruptedException | ExecutionException | TimeoutException e) { - throw new IOException(e); - } - } - - @Override - public void onTwinRemoved(EntityTwin removed, TwinUpdateContext context) { - TwinStatus status = statusMap.remove(removed.getTwinId()); - status.close(); - } - - - private String computeTopic(String twinId, String messageEnum) { - String topic = topicTemplate.replace("{twinId}", twinId); - topic = topic.replace("{messageEnumName}", messageEnum); - return topic; - } - - @Getter - @Setter - private static class TwinStatus { - private long lastStatusUpdate = System.currentTimeMillis(); - private long lastDescriptionUpdate = 0; - private String topicPath = "topic"; - private boolean sentDescription = false; - private Map destinationMap = new ConcurrentHashMap<>(); - - public void close(){ - destinationMap.clear(); - } - - } -} diff --git a/src/main/java/io/mapsmessaging/state/stanag/StanagSession.java b/src/main/java/io/mapsmessaging/state/stanag/StanagSession.java deleted file mode 100644 index 8a3eb2fcc..000000000 --- a/src/main/java/io/mapsmessaging/state/stanag/StanagSession.java +++ /dev/null @@ -1,236 +0,0 @@ -/* - * - * Copyright [ 2020 - 2024 ] Matthew Buckton - * Copyright [ 2024 - 2026 ] MapsMessaging B.V. - * - * Licensed under the Apache License, Version 2.0 with the Commons Clause - * (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 - * https://commonsclause.com/ - * - * 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 io.mapsmessaging.state.stanag; - -import com.google.gson.JsonObject; -import com.google.gson.JsonParser; -import io.mapsmessaging.api.Destination; -import io.mapsmessaging.api.MessageEvent; -import io.mapsmessaging.api.features.DestinationType; -import io.mapsmessaging.api.features.QualityOfService; -import io.mapsmessaging.api.message.Message; -import io.mapsmessaging.network.io.impl.noop.NoOpEndPoint; -import io.mapsmessaging.state.MessageHandler; -import io.mapsmessaging.state.StateLoopProtocol; -import io.mapsmessaging.state.config.StanagConfig; -import io.mapsmessaging.state.drone.core.TwinManager; -import io.mapsmessaging.state.stanag.audit.Auditor; -import io.mapsmessaging.utilities.Lifecycle; -import lombok.Getter; -import lombok.NonNull; -import org.jetbrains.annotations.NotNull; - -import java.io.IOException; -import java.nio.charset.StandardCharsets; -import java.util.ArrayList; -import java.util.Map; -import java.util.UUID; -import java.util.concurrent.ConcurrentHashMap; -import java.util.concurrent.ExecutionException; -import java.util.concurrent.TimeUnit; -import java.util.concurrent.TimeoutException; -import java.util.function.Consumer; - -public class StanagSession implements MessageHandler, TaskMessageSender, Lifecycle { - - private final TwinManager twinManager; - - private final StateLoopProtocol protocol; - private final String taskTopic; - private final String chatTopic; - private final String taskTopicTemplate; - private final TaskListener taskListener; - private final Consumer chatListener; - private final StanagConfig stanagConfig; - private final Map destinationCache = new ConcurrentHashMap<>(); - - private NodeUpdate nodeUpdateListener; - - @Getter - private final Auditor auditor; - - public StanagSession(@NonNull @NotNull TwinManager twinManager, @NonNull @NotNull StanagConfig stanagConfig) { - this.twinManager = twinManager; - this.stanagConfig = stanagConfig; - this.protocol = new StateLoopProtocol(new NoOpEndPoint(1, null, new ArrayList<>()), this); - this.auditor = twinManager.getAuditor(); - this.taskTopic = stanagConfig.getTaskTopic(); - this.chatTopic = stanagConfig.getChatTopic(); - this.taskTopicTemplate = stanagConfig.getTaskTopicTemplate(); - this.taskListener = new TaskListener(twinManager, this, stanagConfig); - this.chatListener = new ChatListener(protocol); - } - - public void start() { - try { - if (!isConfigured(taskTopic) && !isConfigured(chatTopic)) { - return; - } - protocol.connect(UUID.randomUUID().toString(), "anonymous", "anonymous"); - this.nodeUpdateListener = new NodeUpdate(protocol.getSession(), stanagConfig); - if (isConfigured(taskTopic)) { - subscribe(taskTopic); - } - if (isConfigured(chatTopic) && !chatTopic.equals(taskTopic)) { - subscribe(chatTopic); - } - taskListener.start(); - } catch (IOException e) { - // Log this - } - twinManager.addObserver(nodeUpdateListener); - } - - public void stop() { - taskListener.stop(); - if (isConfigured(taskTopic)) { - protocol.unsubscribeLocal(taskTopic); - } - - if (isConfigured(chatTopic) && !chatTopic.equals(taskTopic)) { - protocol.unsubscribeLocal(chatTopic); - } - - try { - protocol.close(); - } catch (IOException e) { - // log this - } - } - - @Override - public void handle(@NonNull @NotNull MessageEvent messageEvent) { - try { - JsonObject jsonObject = parseJson(messageEvent.getMessage()); - if (jsonObject == null) { - return; - } - String destinationName = messageEvent.getDestinationName(); - if (matchesTopic(taskTopic, destinationName)) { - taskListener.accept(jsonObject); - } - - if (matchesTopic(chatTopic, destinationName)) { - chatListener.accept(jsonObject); - } - } finally { - messageEvent.getCompletionTask().run(); - } - } - - @Override - public void sendTaskMessage(String taskTopic, Message message) { - respond(taskTopic, message); - } - - public void respond(String topicName, Message message) { - Destination destination = resolveDestination(topicName); - - if (destination == null) { - return; - } - - try { - destination.storeMessage(message); - } catch (IOException exception) { - // log this - } - } - - private Destination resolveDestination(String topicName) { - destinationCache.computeIfPresent(topicName, (cachedTopicName, destination) -> destination.isClosed() ? null : destination); - return destinationCache.computeIfAbsent(topicName, this::findDestination); - } - - private Destination findDestination(String topicName) { - try { - return protocol.getSession().findDestination(topicName, DestinationType.TOPIC).get(1, TimeUnit.SECONDS); - } catch (InterruptedException exception) { - Thread.currentThread().interrupt(); - return null; - } catch (ExecutionException | TimeoutException exception) { - // log this - return null; - } - } - - private void subscribe(String topicName) throws IOException { - protocol.subscribeLocal(topicName, topicName, QualityOfService.AT_MOST_ONCE, null, null, null, null, null); - } - - private JsonObject parseJson(Message message) { - byte[] opaqueData = message.getOpaqueData(); - - if (opaqueData == null || opaqueData.length == 0) { - return null; - } - - try { - return JsonParser.parseString(new String(opaqueData, StandardCharsets.UTF_8)).getAsJsonObject(); - } catch (RuntimeException runtimeException) { - return null; - } - } - - private boolean matchesTopic(String configuredTopic, String destinationName) { - if (!isConfigured(configuredTopic) || destinationName == null) { - return false; - } - - return configuredTopic.equals(destinationName) || matchesWildcardTopic(configuredTopic, destinationName); - } - - private boolean matchesWildcardTopic(String configuredTopic, String destinationName) { - String[] configuredParts = configuredTopic.split("/"); - String[] destinationParts = destinationName.split("/"); - - int configuredIndex = 0; - int destinationIndex = 0; - - while (configuredIndex < configuredParts.length) { - String configuredPart = configuredParts[configuredIndex]; - - if ("#".equals(configuredPart)) { - return configuredIndex == configuredParts.length - 1; - } - - if (destinationIndex >= destinationParts.length) { - return false; - } - - if (!"+".equals(configuredPart) && !configuredPart.equals(destinationParts[destinationIndex])) { - return false; - } - - configuredIndex++; - destinationIndex++; - } - - return destinationIndex == destinationParts.length; - } - - private String buildTopicName(String topicTemplate, String taskTopic) { - return (topicTemplate + "/" + taskTopic).replaceAll("/+", "/"); - } - - private boolean isConfigured(String topicName) { - return topicName != null && !topicName.isBlank(); - } -} \ No newline at end of file diff --git a/src/main/java/io/mapsmessaging/state/stanag/StanagUuidGenerator.java b/src/main/java/io/mapsmessaging/state/stanag/StanagUuidGenerator.java deleted file mode 100644 index c6e077b48..000000000 --- a/src/main/java/io/mapsmessaging/state/stanag/StanagUuidGenerator.java +++ /dev/null @@ -1,46 +0,0 @@ -/* - * - * Copyright [ 2020 - 2024 ] Matthew Buckton - * Copyright [ 2024 - 2026 ] MapsMessaging B.V. - * - * Licensed under the Apache License, Version 2.0 with the Commons Clause - * (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 - * https://commonsclause.com/ - * - * 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 io.mapsmessaging.state.stanag; - - -import io.mapsmessaging.security.uuid.NamedVersions; -import io.mapsmessaging.state.drone.util.UuidGenerator; - -import java.security.NoSuchAlgorithmException; -import java.util.Locale; -import java.util.UUID; - -public class StanagUuidGenerator extends UuidGenerator { - - @Override - public UUID generateUuid(String namespace) { - try { - return resolvePlatformUuid("EE", UUID.fromString("6ba7b810-9dad-11d1-80b4-00c04fd430c8"), "stickleback", namespace); - } catch (NoSuchAlgorithmException e) { - return super.generateUuid(namespace); - } - } - - - private UUID resolvePlatformUuid(String countryCode2Char, UUID suppliedUuid, String platformName, String platformId) throws NoSuchAlgorithmException { - String uuidSource = ("urn:nato:stanag:4817:"+countryCode2Char+":" + platformName + ":" + platformId).toUpperCase(Locale.ROOT); - return io.mapsmessaging.security.uuid.UuidGenerator.getInstance().generate(NamedVersions.SHA1, suppliedUuid, uuidSource ); - } -} diff --git a/src/main/java/io/mapsmessaging/state/stanag/TaskAdminCommand.java b/src/main/java/io/mapsmessaging/state/stanag/TaskAdminCommand.java deleted file mode 100644 index 362f1bd58..000000000 --- a/src/main/java/io/mapsmessaging/state/stanag/TaskAdminCommand.java +++ /dev/null @@ -1,150 +0,0 @@ -/* - * - * Copyright [ 2020 - 2024 ] Matthew Buckton - * Copyright [ 2024 - 2026 ] MapsMessaging B.V. - * - * Licensed under the Apache License, Version 2.0 with the Commons Clause - * (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 - * https://commonsclause.com/ - * - * 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 io.mapsmessaging.state.stanag; - -import com.google.gson.JsonObject; -import io.mapsmessaging.state.drone.model.GeoPosition; -import lombok.Getter; -import lombok.ToString; - -import java.util.UUID; - -@Getter -@ToString -public class TaskAdminCommand { - - private static final String TASK_ADMIN_ACTION_PREFIX = "TaskAdminActionEnum_"; - private static final String TASK_TYPE_PREFIX = "TaskTypeEnum_"; - - private final String action; - private final String taskType; - private final String identifier; - private final UUID taskId; - private final UUID nodeIdentifier; - private final UUID authorityGuid; - private final String positionIdentifier; - private final GeoPosition position; - - public TaskAdminCommand( - String action, - String taskType, - String identifier, - String nodeIdentifier, - String authorityGuid, - String positionIdentifier, - GeoPosition position) { - this.action = action; - this.taskType = taskType; - this.identifier = identifier; - this.taskId = UUID.fromString(identifier); - this.nodeIdentifier = UUID.fromString(nodeIdentifier); - this.authorityGuid = UUID.fromString(authorityGuid); - this.positionIdentifier = positionIdentifier; - this.position = position; - } - - public static TaskAdminCommand fromJson(JsonObject jsonObject) throws TaskAdminCommandException { - try { - JsonObject bodyObject = getRequiredObject(jsonObject, "body"); - - String action = stripPrefix(getRequiredString(bodyObject, "action"), TASK_ADMIN_ACTION_PREFIX); - - String identifier = getRequiredString(bodyObject, "identifier"); - String nodeIdentifier = getRequiredString(bodyObject, "node"); - - JsonObject authorityObject = getRequiredObject(bodyObject, "authority"); - validateDiscriminator(authorityObject, "AuthorityTypeEnum_GUID"); - String authorityGuid = getRequiredString(authorityObject, "guid"); - - JsonObject descriptionObject = getRequiredObject(bodyObject, "description"); - - String taskType = stripPrefix( - getRequiredString(descriptionObject, "$discriminator"), - TASK_TYPE_PREFIX - ); - - if (!"REPOSITION".equals(taskType)) { - throw new TaskAdminCommandException("Unsupported task type: " + taskType); - } - - JsonObject repositionObject = getRequiredObject(descriptionObject, "reposition"); - JsonObject locationObject = getRequiredObject(repositionObject, "location"); - - String positionIdentifier = getRequiredString(locationObject, "identifier"); - - JsonObject geometryObject = getRequiredObject(locationObject, "location"); - validateDiscriminator(geometryObject, "GeometryTypeEnum_POINT"); - - JsonObject pointObject = getRequiredObject(geometryObject, "point"); - - GeoPosition position = new GeoPosition(); - position.setLatitude(getRequiredDouble(pointObject, "latitude")); - position.setLongitude(getRequiredDouble(pointObject, "longitude")); - position.setAltitudeMslMeters(getRequiredDouble(pointObject, "altitude")); - - return new TaskAdminCommand(action, taskType, identifier, nodeIdentifier, authorityGuid, positionIdentifier, position); - } catch (IllegalStateException | ClassCastException exception) { - throw new TaskAdminCommandException("Invalid TASK_ADMIN command structure", exception); - } - } - - private static String stripPrefix(String value, String prefix) { - if (value != null && value.startsWith(prefix)) { - return value.substring(prefix.length()); - } - return value; - } - - private static void validateDiscriminator(JsonObject jsonObject, String expectedValue) - throws TaskAdminCommandException { - String actualValue = getRequiredString(jsonObject, "$discriminator"); - if (!expectedValue.equals(actualValue)) { - throw new TaskAdminCommandException( - "Unsupported discriminator: " + actualValue + ", expected: " + expectedValue); - } - } - - private static JsonObject getRequiredObject(JsonObject jsonObject, String name) - throws TaskAdminCommandException { - if (jsonObject == null || !jsonObject.has(name) || jsonObject.get(name).isJsonNull()) { - throw new TaskAdminCommandException("Missing required object: " + name); - } - if (!jsonObject.get(name).isJsonObject()) { - throw new TaskAdminCommandException("Required field is not an object: " + name); - } - return jsonObject.getAsJsonObject(name); - } - - private static String getRequiredString(JsonObject jsonObject, String name) - throws TaskAdminCommandException { - if (jsonObject == null || !jsonObject.has(name) || jsonObject.get(name).isJsonNull()) { - throw new TaskAdminCommandException("Missing required field: " + name); - } - return jsonObject.get(name).getAsString(); - } - - private static Double getRequiredDouble(JsonObject jsonObject, String name) - throws TaskAdminCommandException { - if (jsonObject == null || !jsonObject.has(name) || jsonObject.get(name).isJsonNull()) { - throw new TaskAdminCommandException("Missing required field: " + name); - } - return jsonObject.get(name).getAsDouble(); - } -} \ No newline at end of file diff --git a/src/main/java/io/mapsmessaging/state/stanag/TaskAdminCommandException.java b/src/main/java/io/mapsmessaging/state/stanag/TaskAdminCommandException.java deleted file mode 100644 index 2d016ffe9..000000000 --- a/src/main/java/io/mapsmessaging/state/stanag/TaskAdminCommandException.java +++ /dev/null @@ -1,31 +0,0 @@ -/* - * - * Copyright [ 2020 - 2024 ] Matthew Buckton - * Copyright [ 2024 - 2026 ] MapsMessaging B.V. - * - * Licensed under the Apache License, Version 2.0 with the Commons Clause - * (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 - * https://commonsclause.com/ - * - * 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 io.mapsmessaging.state.stanag; - -public class TaskAdminCommandException extends Exception { - - public TaskAdminCommandException(String message) { - super(message); - } - - public TaskAdminCommandException(String message, Throwable cause) { - super(message, cause); - } -} \ No newline at end of file diff --git a/src/main/java/io/mapsmessaging/state/stanag/TaskListener.java b/src/main/java/io/mapsmessaging/state/stanag/TaskListener.java deleted file mode 100644 index b7ae35e9a..000000000 --- a/src/main/java/io/mapsmessaging/state/stanag/TaskListener.java +++ /dev/null @@ -1,277 +0,0 @@ -/* - * - * Copyright [ 2020 - 2024 ] Matthew Buckton - * Copyright [ 2024 - 2026 ] MapsMessaging B.V. - * - * Licensed under the Apache License, Version 2.0 with the Commons Clause - * (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 - * https://commonsclause.com/ - * - * 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 io.mapsmessaging.state.stanag; - -import com.google.gson.JsonObject; -import io.mapsmessaging.state.config.StanagConfig; -import io.mapsmessaging.state.config.capability.Authorities; -import io.mapsmessaging.state.drone.core.EntityTwin; -import io.mapsmessaging.state.drone.core.TwinManager; -import io.mapsmessaging.state.drone.drone.DroneTwin; -import io.mapsmessaging.state.stanag.audit.AuditEvent; -import io.mapsmessaging.state.stanag.audit.Auditor; -import io.mapsmessaging.state.stanag.messages.*; -import io.mapsmessaging.state.stanag.messages.core.MessageHeaderBuilder; -import io.mapsmessaging.state.stanag.messages.task.admin.TaskAdminActionEnum; -import io.mapsmessaging.state.stanag.messages.task.admin.TaskAdminMessage; -import io.mapsmessaging.state.stanag.messages.task.feedback.TaskFeedbackMessage; -import io.mapsmessaging.state.stanag.messages.task.feedback.TaskFeedbackMessageBuilder; -import io.mapsmessaging.state.stanag.messages.task.result.ResultReason; -import io.mapsmessaging.state.stanag.messages.task.result.ResultReasonBuilder; -import io.mapsmessaging.state.stanag.messages.task.result.TaskResultMessage; -import io.mapsmessaging.state.stanag.messages.task.result.TaskResultMessageBuilder; -import io.mapsmessaging.state.stanag.tasks.TaskDispatchResult; -import io.mapsmessaging.state.stanag.tasks.TaskDispatcher; -import io.mapsmessaging.state.stanag.tasks.monitor.TaskMonitor; -import io.mapsmessaging.state.stanag.tasks.monitor.TaskMonitorManager; -import io.mapsmessaging.state.stanag.tasks.monitor.TaskStatusPublisher; - -import java.io.IOException; -import java.time.Clock; -import java.time.Duration; -import java.util.Optional; -import java.util.UUID; -import java.util.concurrent.atomic.AtomicInteger; -import java.util.function.Consumer; - -public class TaskListener implements Consumer, TaskStatusPublisher { - - private final TwinManager twinManager; - private final Auditor auditor; - private final TaskDispatcher taskDispatcher; - private final TaskResultMessageBuilder taskResultMessageBuilder; - private final TaskEventPublisher taskEventPublisher; - private final TaskMonitorManager taskMonitorManager; - private final TaskFeedbackMessageBuilder taskFeedbackMessageBuilder; - - public TaskListener(TwinManager twinManager, StanagSession protocol, StanagConfig stanagConfig) { - this.twinManager = twinManager; - this.auditor = twinManager.getAuditor(); - this.taskDispatcher = new TaskDispatcher(protocol, stanagConfig, new AtomicInteger(0)); - this.taskResultMessageBuilder = new TaskResultMessageBuilder(new MessageHeaderBuilder(Clock.systemUTC()), new ResultReasonBuilder()); - this.taskFeedbackMessageBuilder = new TaskFeedbackMessageBuilder(new MessageHeaderBuilder(Clock.systemUTC())); - this.taskEventPublisher = new TaskEventPublisher(protocol, new TaskSchemaValidator(), stanagConfig.getTaskTopicTemplate()); - this.taskMonitorManager = new TaskMonitorManager(Duration.ofSeconds(5), this); - twinManager.addObserver(taskMonitorManager); - } - - public void start() { - taskMonitorManager.start(); - } - - public void stop() { - taskMonitorManager.close(); - } - - @Override - public void publishFeedback(TaskMonitor taskMonitor, TaskFeedbackMessage taskFeedbackMessage) { - taskEventPublisher.publishFeedback(taskMonitor.getDroneUUID(), taskFeedbackMessage); - } - - @Override - public void publishResult(TaskMonitor taskMonitor, TaskResultMessage taskResultMessage) { - taskEventPublisher.publishResult(taskMonitor.getDroneUUID(), taskResultMessage); - auditTaskResult(taskMonitor, taskResultMessage); - } - - public void publishAdmin(TaskMonitor taskMonitor, TaskAdminMessage taskAdminMessage) { - taskEventPublisher.publishAdmin(taskMonitor.getDroneUUID(), taskAdminMessage); - auditTaskAdmin(taskMonitor, taskAdminMessage); - } - - @Override - public void accept(JsonObject jsonObject) { - TaskAdminCommand command = createTaskCommand(jsonObject); - if (command != null) { - if(command.getAction().equals(TaskAdminActionEnum.PUSH.name())) { - DroneTwin droneTwin = findTwin(command); - if (droneTwin == null) { - return; - } - handleTaskForTwin(droneTwin, command); - } - } else { - // log this - } - } - - private void handleTaskForTwin(DroneTwin droneTwin, TaskAdminCommand command) { - TaskDispatchResult dispatchResult = taskDispatcher.dispatch(droneTwin, command); - if (!dispatchResult.isAccepted()) { - rejectTask(dispatchResult.getDroneTwin(), command, dispatchResult.getRejectReason(), dispatchResult.getRejectText()); - } - else{ - TaskMonitor taskMonitor = dispatchResult.getTaskMonitor(); - if (taskMonitor != null) { - acceptTask(taskMonitor, command); - taskMonitorManager.add(taskMonitor); - } - } - } - - private void acceptTask(TaskMonitor taskStat, TaskAdminCommand command) { - Authorities authority = new Authorities(command.getAuthorityGuid()); - TaskStatusContext context = new TaskStatusContext(command.getTaskId(), command.getNodeIdentifier(), authority); - taskEventPublisher.publishFeedback(taskStat.getDroneUUID(), taskFeedbackMessageBuilder.build(context, TaskState.ACTIVE)); - } - - private void rejectTask(DroneTwin droneTwin, TaskAdminCommand command, ResultReason resultReason, String reasonText) { - TaskResultMessage taskResultMessage = buildRejectedTaskResult(command, resultReason, reasonText); - taskEventPublisher.publishResult(droneTwin.getUuid(), taskResultMessage); - auditRejected(droneTwin, command, resultReason, reasonText); - } - - private void rejectWithoutTwin(TaskAdminCommand command, ResultReason resultReason, String reasonText) { - TaskResultMessage taskResultMessage = buildRejectedTaskResult(command, resultReason, reasonText); - auditRejected(null, command, resultReason, reasonText); - } - - private TaskResultMessage buildRejectedTaskResult(TaskAdminCommand command, ResultReason resultReason, String reasonText) { - Authorities authority = new Authorities(command.getAuthorityGuid()); - TaskStatusContext context = new TaskStatusContext(command.getTaskId(), command.getNodeIdentifier(), authority); - return taskResultMessageBuilder.buildRejected(context, resultReason, reasonText); - } - - private void auditRejected(DroneTwin droneTwin, TaskAdminCommand command, ResultReason resultReason, String reasonText) { - if (auditor != null) { - try { - auditor.auditStanagCommandRejected(buildRejectedAuditEvent(droneTwin, command), resultReason.name() + ": " + reasonText); - } catch (IOException exception) { - exception.printStackTrace(); - } - } - } - - private void auditTaskAdmin(TaskMonitor taskMonitor, TaskAdminMessage taskAdminMessage) { - if (auditor != null) { - AuditEvent auditEvent = taskMonitor.getAuditEvent(); - if (auditEvent == null) { - throw new IllegalStateException("Task monitor has no audit event for task " + taskMonitor.getTaskId()); - } - - try { - if (isSuccessfulTaskAdmin(taskAdminMessage)) { - auditor.auditStanagTaskResultPublished(auditEvent); - } else { - auditor.auditStanagTaskResultFailed(auditEvent, taskAdminMessage.getBody().getAction().name()); - } - } catch (IOException exception) { - exception.printStackTrace(); - } - } - } - - private void auditTaskResult(TaskMonitor taskMonitor, TaskResultMessage taskResultMessage) { - if (auditor != null) { - AuditEvent auditEvent = taskMonitor.getAuditEvent(); - if (auditEvent == null) { - throw new IllegalStateException("Task monitor has no audit event for task " + taskMonitor.getTaskId()); - } - try { - if (isSuccessfulTaskResult(taskResultMessage)) { - auditor.auditStanagTaskResultPublished(auditEvent); - } else { - auditor.auditStanagTaskResultFailed(auditEvent, taskResultMessage.getBody().getState().name()); - } - } catch (IOException exception) { - exception.printStackTrace(); - } - } - } - - private AuditEvent buildRejectedAuditEvent(DroneTwin droneTwin, TaskAdminCommand command) { - UUID nodeIdentifier = command.getNodeIdentifier(); - String droneId = nodeIdentifier.toString(); - String destination = droneTwin == null ? droneId : droneTwin.getTwinId(); - - return AuditEvent.builder() - .auditId(command.getIdentifier()) - .correlationId(command.getIdentifier()) - .parentCorrelationId("") - .actor("stanag") - .actorType("system") - .source("stanag") - .destination(destination) - .subject(droneId) - .taskId(command.getIdentifier()) - .commandId(command.getIdentifier()) - .droneId(droneId) - .stanagTaskType(command.getTaskType()) - .protocol("stanag-4817") - .build(); - } - - private boolean isSuccessfulTaskResult(TaskResultMessage taskResultMessage) { - return taskResultMessage.getBody().getState() == TaskState.SUCCEEDED; - } - - private boolean isSuccessfulTaskAdmin(TaskAdminMessage taskAdminMessage) { - return taskAdminMessage.getBody().getAction() == TaskAdminActionEnum.ASSIGN; - } - - private void publishResult(TaskResultMessage taskResultMessage) { - // Existing reject publishing behaviour preserved while you sort out the reject path. - } - - - private TaskAdminCommand createTaskCommand(JsonObject jsonObject) { - try { - return TaskAdminCommand.fromJson(jsonObject); - } catch (TaskAdminCommandException e) { - // Log this and respond - } - return null; - } - - private DroneTwin findTwin(TaskAdminCommand command) { - UUID nodeUuid; - - try { - nodeUuid = command.getNodeIdentifier(); - } catch (IllegalArgumentException exception) { - rejectWithoutTwin(command, ResultReason.LOGIC, "Invalid node identifier"); - return null; - } - - if (nodeUuid == null) { - rejectWithoutTwin(command, ResultReason.LOGIC, "Invalid node identifier"); - return null; - } - - Optional matchingTwin = - twinManager.listTwins().stream() - .filter(entityTwin -> nodeUuid.equals(entityTwin.getUuid())) - .findFirst(); - - if (matchingTwin.isEmpty()) { - rejectWithoutTwin(command, ResultReason.CAPABILITY, "No matching twin found for node"); - return null; - } - - EntityTwin entityTwin = matchingTwin.get(); - if (!(entityTwin instanceof DroneTwin droneTwin)) { - rejectWithoutTwin(command, ResultReason.CAPABILITY, "Matching twin is not a drone"); - return null; - } - - return droneTwin; - } - -} \ No newline at end of file diff --git a/src/main/java/io/mapsmessaging/state/stanag/audit/AuditEvent.java b/src/main/java/io/mapsmessaging/state/stanag/audit/AuditEvent.java deleted file mode 100644 index 97e3a4cdb..000000000 --- a/src/main/java/io/mapsmessaging/state/stanag/audit/AuditEvent.java +++ /dev/null @@ -1,69 +0,0 @@ -/* - * - * Copyright [ 2020 - 2024 ] Matthew Buckton - * Copyright [ 2024 - 2026 ] MapsMessaging B.V. - * - * Licensed under the Apache License, Version 2.0 with the Commons Clause - * (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 - * https://commonsclause.com/ - * - * 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 io.mapsmessaging.state.stanag.audit; - -import lombok.*; - -import java.util.LinkedHashMap; -import java.util.Map; - -@Getter -@Setter -@Builder -@NoArgsConstructor -@AllArgsConstructor -public class AuditEvent { - - private String auditId; - private String correlationId; - private String parentCorrelationId; - - private String actor; - private String actorType; - private String source; - private String destination; - private String subject; - - private String missionId; - private String taskId; - private String commandId; - private String droneId; - - private String stanagTaskType; - private String droneCommandType; - private String protocol; - - private String targetSystem; - private String targetComponent; - - private Map attributes; - - public Map getAttributes() { - if (attributes == null) { - attributes = new LinkedHashMap<>(); - } - - return attributes; - } - - public void addAttribute(String name, String value) { - getAttributes().put(name, value); - } -} \ No newline at end of file diff --git a/src/main/java/io/mapsmessaging/state/stanag/audit/AuditMessages.java b/src/main/java/io/mapsmessaging/state/stanag/audit/AuditMessages.java deleted file mode 100644 index 515d9741f..000000000 --- a/src/main/java/io/mapsmessaging/state/stanag/audit/AuditMessages.java +++ /dev/null @@ -1,81 +0,0 @@ -/* - * - * Copyright [ 2020 - 2024 ] Matthew Buckton - * Copyright [ 2024 - 2026 ] MapsMessaging B.V. - * - * Licensed under the Apache License, Version 2.0 with the Commons Clause - * (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 - * https://commonsclause.com/ - * - * 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 io.mapsmessaging.state.stanag.audit; - -import io.mapsmessaging.logging.Category; -import io.mapsmessaging.logging.LEVEL; -import io.mapsmessaging.logging.LogMessage; -import lombok.Getter; - -public enum AuditMessages implements LogMessage { - - STANAG_COMMAND_RECEIVED(LEVEL.AUDIT, CATEGORY.C2, "Received STANAG task {} for drone {} command {}"), - STANAG_COMMAND_REJECTED(LEVEL.AUDIT, CATEGORY.C2, "Rejected STANAG task {} for drone {} reason {}"), - STANAG_COMMAND_TRANSLATED(LEVEL.AUDIT, CATEGORY.C2, "Translated STANAG task {} to drone command {} for drone {}"), - DRONE_COMMAND_DISPATCHED(LEVEL.AUDIT, CATEGORY.C2, "Dispatched drone command {} to drone {} command {}"), - DRONE_COMMAND_ACKNOWLEDGED(LEVEL.AUDIT, CATEGORY.C2, "Acknowledged drone command {} for drone {} command {}"), - DRONE_COMMAND_FAILED(LEVEL.AUDIT, CATEGORY.C2, "Failed drone command {} for drone {} reason {}"), - STANAG_TASK_RESULT_PUBLISHED(LEVEL.AUDIT, CATEGORY.C2, "Published STANAG task result {} for drone {} task {}"), - STANAG_TASK_RESULT_FAILED(LEVEL.AUDIT, CATEGORY.C2, "Failed STANAG task result {} for drone {} reason {}"); - - private final @Getter LEVEL level; - private final @Getter Category category; - private final @Getter String message; - private final @Getter int parameterCount; - - AuditMessages( - LEVEL level, - Category category, - String message - ) { - this.level = level; - this.category = category; - this.message = message; - this.parameterCount = countParameters(message); - } - - private int countParameters(String message) { - int count = 0; - int location = message.indexOf("{}"); - - while (location != -1) { - count++; - location = message.indexOf("{}", location + 2); - } - - return count; - } - - private enum CATEGORY implements Category { - - C2("Command and Control"); - - private final @Getter String description; - - CATEGORY(String description) { - this.description = description; - } - - @Override - public String getDivision() { - return "Audit"; - } - } -} \ No newline at end of file diff --git a/src/main/java/io/mapsmessaging/state/stanag/audit/AuditPayload.java b/src/main/java/io/mapsmessaging/state/stanag/audit/AuditPayload.java deleted file mode 100644 index 91e85f9b9..000000000 --- a/src/main/java/io/mapsmessaging/state/stanag/audit/AuditPayload.java +++ /dev/null @@ -1,36 +0,0 @@ -/* - * - * Copyright [ 2020 - 2024 ] Matthew Buckton - * Copyright [ 2024 - 2026 ] MapsMessaging B.V. - * - * Licensed under the Apache License, Version 2.0 with the Commons Clause - * (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 - * https://commonsclause.com/ - * - * 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 io.mapsmessaging.state.stanag.audit; - - -import lombok.*; - -@Getter -@Setter -@Builder -@NoArgsConstructor -@AllArgsConstructor -public class AuditPayload { - - private String name; - private String fileName; - private String contentType; - private byte[] payload; -} \ No newline at end of file diff --git a/src/main/java/io/mapsmessaging/state/stanag/audit/Auditor.java b/src/main/java/io/mapsmessaging/state/stanag/audit/Auditor.java deleted file mode 100644 index 19775120b..000000000 --- a/src/main/java/io/mapsmessaging/state/stanag/audit/Auditor.java +++ /dev/null @@ -1,217 +0,0 @@ -/* - * - * Copyright [ 2020 - 2024 ] Matthew Buckton - * Copyright [ 2024 - 2026 ] MapsMessaging B.V. - * - * Licensed under the Apache License, Version 2.0 with the Commons Clause - * (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 - * https://commonsclause.com/ - * - * 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 io.mapsmessaging.state.stanag.audit; - -import io.mapsmessaging.audit.*; - -import java.io.IOException; -import java.time.Instant; -import java.util.ArrayList; -import java.util.List; -import java.util.Map; - -public class Auditor { - - private final AuditLogger auditLogger; - private final AuditPayloadStore auditPayloadStore; - - public Auditor(AuditLogger auditLogger, AuditPayloadStore auditPayloadStore) { - this.auditLogger = auditLogger; - this.auditPayloadStore = auditPayloadStore; - } - - public AuditRecord auditStanagCommandReceived(AuditEvent auditEvent, AuditPayload... payloads) - throws IOException { - return auditLogger.audit( - AuditMessages.STANAG_COMMAND_RECEIVED, - buildAuditContext(auditEvent, "stanag-command-received", AuditOutcome.SUCCESS), - writePayloads(auditEvent, payloads), - safe(auditEvent.getStanagTaskType()), - safe(auditEvent.getDroneId()), - safe(auditEvent.getCommandId())); - } - - public AuditRecord auditStanagCommandRejected( - AuditEvent auditEvent, String reason, AuditPayload... payloads) throws IOException { - return auditLogger.audit( - AuditMessages.STANAG_COMMAND_REJECTED, - buildAuditContext(auditEvent, "stanag-command-rejected", AuditOutcome.REJECTED), - writePayloads(auditEvent, payloads), - safe(auditEvent.getStanagTaskType()), - safe(auditEvent.getDroneId()), - safe(reason)); - } - - public AuditRecord auditStanagCommandTranslated(AuditEvent auditEvent, AuditPayload... payloads) - throws IOException { - return auditLogger.audit( - AuditMessages.STANAG_COMMAND_TRANSLATED, - buildAuditContext(auditEvent, "stanag-command-translated", AuditOutcome.SUCCESS), - writePayloads(auditEvent, payloads), - safe(auditEvent.getStanagTaskType()), - safe(auditEvent.getDroneCommandType()), - safe(auditEvent.getDroneId())); - } - - public AuditRecord auditDroneCommandDispatched(AuditEvent auditEvent, AuditPayload... payloads) - throws IOException { - return auditLogger.audit( - AuditMessages.DRONE_COMMAND_DISPATCHED, - buildAuditContext(auditEvent, "drone-command-dispatched", AuditOutcome.SUCCESS), - writePayloads(auditEvent, payloads), - safe(auditEvent.getDroneCommandType()), - safe(auditEvent.getDroneId()), - safe(auditEvent.getCommandId())); - } - - public AuditRecord auditDroneCommandAcknowledged(AuditEvent auditEvent, AuditPayload... payloads) - throws IOException { - return auditLogger.audit( - AuditMessages.DRONE_COMMAND_ACKNOWLEDGED, - buildAuditContext(auditEvent, "drone-command-acknowledged", AuditOutcome.SUCCESS), - writePayloads(auditEvent, payloads), - safe(auditEvent.getDroneCommandType()), - safe(auditEvent.getDroneId()), - safe(auditEvent.getCommandId())); - } - - public AuditRecord auditDroneCommandFailed( - AuditEvent auditEvent, String reason, AuditPayload... payloads) throws IOException { - return auditLogger.audit( - AuditMessages.DRONE_COMMAND_FAILED, - buildAuditContext(auditEvent, "drone-command-failed", AuditOutcome.FAILURE), - writePayloads(auditEvent, payloads), - safe(auditEvent.getDroneCommandType()), - safe(auditEvent.getDroneId()), - safe(reason)); - } - - public AuditRecord auditStanagTaskResultPublished(AuditEvent auditEvent, AuditPayload... payloads) - throws IOException { - return auditLogger.audit( - AuditMessages.STANAG_TASK_RESULT_PUBLISHED, - buildAuditContext(auditEvent, "stanag-task-result-published", AuditOutcome.SUCCESS), - writePayloads(auditEvent, payloads), - safe(auditEvent.getStanagTaskType()), - safe(auditEvent.getDroneId()), - safe(auditEvent.getTaskId())); - } - - public AuditRecord auditStanagTaskResultFailed( - AuditEvent auditEvent, String reason, AuditPayload... payloads) throws IOException { - return auditLogger.audit( - AuditMessages.STANAG_TASK_RESULT_FAILED, - buildAuditContext(auditEvent, "stanag-task-result-failed", AuditOutcome.FAILURE), - writePayloads(auditEvent, payloads), - safe(auditEvent.getStanagTaskType()), - safe(auditEvent.getDroneId()), - safe(reason)); - } - - private AuditContext buildAuditContext(AuditEvent auditEvent, String action, AuditOutcome outcome) { - AuditContext auditContext = AuditContext.builder() - .auditId(auditEvent.getAuditId()) - .correlationId(auditEvent.getCorrelationId()) - .parentCorrelationId(auditEvent.getParentCorrelationId()) - .actor(auditEvent.getActor()) - .actorType(auditEvent.getActorType()) - .source(auditEvent.getSource()) - .destination(auditEvent.getDestination()) - .subject(auditEvent.getSubject()) - .action(action) - .outcome(outcome) - .timestamp(Instant.now()) - .build(); - - addIfPresent(auditContext, "missionId", auditEvent.getMissionId()); - addIfPresent(auditContext, "taskId", auditEvent.getTaskId()); - addIfPresent(auditContext, "commandId", auditEvent.getCommandId()); - addIfPresent(auditContext, "droneId", auditEvent.getDroneId()); - addIfPresent(auditContext, "stanagTaskType", auditEvent.getStanagTaskType()); - addIfPresent(auditContext, "droneCommandType", auditEvent.getDroneCommandType()); - addIfPresent(auditContext, "protocol", auditEvent.getProtocol()); - addIfPresent(auditContext, "targetSystem", auditEvent.getTargetSystem()); - addIfPresent(auditContext, "targetComponent", auditEvent.getTargetComponent()); - - for (Map.Entry entry : auditEvent.getAttributes().entrySet()) { - addIfPresent(auditContext, entry.getKey(), entry.getValue()); - } - - return auditContext; - } - - private List writePayloads(AuditEvent auditEvent, AuditPayload... payloads) - throws IOException { - List payloadReferences = new ArrayList<>(); - - if (payloads == null) { - return payloadReferences; - } - - for (AuditPayload payload : payloads) { - if (payload == null || payload.getPayload() == null) { - continue; - } - - AuditPayloadReference payloadReference = auditPayloadStore.writePayload( - safeIdentifier(auditEvent.getCorrelationId()), - safe(payload.getName()), - safeFileName(payload.getFileName()), - payload.getPayload()); - - payloadReference.setContentType(safe(payload.getContentType())); - payloadReferences.add(payloadReference); - } - - return payloadReferences; - } - - private void addIfPresent(AuditContext auditContext, String name, String value) { - if (value == null || value.isBlank()) { - return; - } - - auditContext.addAttribute(name, value); - } - - private String safe(String value) { - if (value == null) { - return ""; - } - - return value; - } - - private String safeIdentifier(String value) { - if (value == null || value.isBlank()) { - return "unknown-correlation"; - } - - return value; - } - - private String safeFileName(String value) { - if (value == null || value.isBlank()) { - return "payload.bin"; - } - - return value.replaceAll("[^a-zA-Z0-9._-]", "_"); - } -} \ No newline at end of file diff --git a/src/main/java/io/mapsmessaging/state/stanag/messages/TaskEventPublisher.java b/src/main/java/io/mapsmessaging/state/stanag/messages/TaskEventPublisher.java deleted file mode 100644 index 51aa0c1b1..000000000 --- a/src/main/java/io/mapsmessaging/state/stanag/messages/TaskEventPublisher.java +++ /dev/null @@ -1,86 +0,0 @@ -/* - * - * Copyright [ 2020 - 2024 ] Matthew Buckton - * Copyright [ 2024 - 2026 ] MapsMessaging B.V. - * - * Licensed under the Apache License, Version 2.0 with the Commons Clause - * (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 - * https://commonsclause.com/ - * - * 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 io.mapsmessaging.state.stanag.messages; - -import com.google.gson.Gson; -import com.google.gson.JsonObject; -import io.mapsmessaging.api.MessageBuilder; -import io.mapsmessaging.api.features.QualityOfService; -import io.mapsmessaging.state.GsonStanagHelper; -import io.mapsmessaging.state.stanag.StanagSession; -import io.mapsmessaging.state.stanag.messages.task.admin.TaskAdminMessage; -import io.mapsmessaging.state.stanag.messages.task.feedback.TaskFeedbackMessage; -import io.mapsmessaging.state.stanag.messages.task.result.TaskResultMessage; -import io.mapsmessaging.state.stanag.tasks.monitor.TaskMonitor; - -import java.nio.charset.StandardCharsets; -import java.util.UUID; - -public class TaskEventPublisher { - - private static final String TASK_ADMIN_SCHEMA_NAME = "catl.hibw.messages.task.TaskAdmin"; - private static final String TASK_FEEDBACK_SCHEMA_NAME = "catl.hibw.messages.task.TaskFeedback"; - private static final String TASK_RESULT_SCHEMA_NAME = "catl.hibw.messages.task.TaskResult"; - - private final StanagSession protocol; - private final Gson gson; - private final TaskSchemaValidator schemaValidator; - private final String topicTemplate; - - public TaskEventPublisher(StanagSession protocol, TaskSchemaValidator schemaValidator, String topicTemplate) { - this.protocol = protocol; - this.gson = GsonStanagHelper.createGson(); - this.schemaValidator = schemaValidator; - this.topicTemplate = topicTemplate; - } - - public void publishFeedback(UUID source, TaskFeedbackMessage taskFeedbackMessage) { - String topicPath = topicTemplate.replace("{messageEnumName}","MessageTypeEnum_TASK_FEEDBACK"); - topicPath = topicPath.replace("{twinId}",source.toString() ); - JsonObject jsonObject = gson.toJsonTree(taskFeedbackMessage).getAsJsonObject(); - schemaValidator.validate(TASK_FEEDBACK_SCHEMA_NAME, jsonObject); - publishTaskEvent(topicPath, jsonObject); - } - - public void publishResult(UUID source, TaskResultMessage taskResultMessage) { - String topicPath = topicTemplate.replace("{messageEnumName}","MessageTypeEnum_TASK_RESULT"); - topicPath = topicPath.replace("{twinId}", source.toString() ); - JsonObject jsonObject = gson.toJsonTree(taskResultMessage).getAsJsonObject(); - schemaValidator.validate(TASK_RESULT_SCHEMA_NAME, jsonObject); - publishTaskEvent(topicPath, jsonObject); - } - - public void publishAdmin(UUID source, TaskAdminMessage taskAdminMessage) { - String topicPath = topicTemplate.replace("{messageEnumName}","MessageTypeEnum_TASK_ADMIN"); - topicPath = topicPath.replace("{twinId}", source.toString() ); - JsonObject jsonObject = gson.toJsonTree(taskAdminMessage).getAsJsonObject(); - schemaValidator.validate(TASK_ADMIN_SCHEMA_NAME, jsonObject); - publishTaskEvent(topicPath, jsonObject); - } - - private void publishTaskEvent(String topicPath, JsonObject jsonObject) { - MessageBuilder messageBuilder = new MessageBuilder(); - messageBuilder - .setQoS(QualityOfService.AT_MOST_ONCE) - .setOpaqueData(jsonObject.toString().getBytes(StandardCharsets.UTF_8)); - - protocol.sendTaskMessage(topicPath, messageBuilder.build()); - } -} \ No newline at end of file diff --git a/src/main/java/io/mapsmessaging/state/stanag/messages/TaskSchemaValidator.java b/src/main/java/io/mapsmessaging/state/stanag/messages/TaskSchemaValidator.java deleted file mode 100644 index c2bd71530..000000000 --- a/src/main/java/io/mapsmessaging/state/stanag/messages/TaskSchemaValidator.java +++ /dev/null @@ -1,52 +0,0 @@ -/* - * - * Copyright [ 2020 - 2024 ] Matthew Buckton - * Copyright [ 2024 - 2026 ] MapsMessaging B.V. - * - * Licensed under the Apache License, Version 2.0 with the Commons Clause - * (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 - * https://commonsclause.com/ - * - * 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 io.mapsmessaging.state.stanag.messages; - -import com.google.gson.JsonObject; -import io.mapsmessaging.engine.schema.SchemaManager; -import io.mapsmessaging.schemas.config.SchemaConfig; -import io.mapsmessaging.schemas.formatters.MessageFormatter; -import io.mapsmessaging.schemas.formatters.MessageFormatterFactory; -import io.mapsmessaging.schemas.formatters.ParseMode; -import io.mapsmessaging.schemas.formatters.ParsedObject; - -import java.io.IOException; -import java.nio.charset.StandardCharsets; - -public class TaskSchemaValidator { - - public void validate(String schemaName, JsonObject jsonObject) { - SchemaConfig schemaConfig = SchemaManager.getInstance().getSchemaByName(schemaName); - - if (schemaConfig == null) { - return; - } - - try { - MessageFormatter messageFormatter = MessageFormatterFactory.getInstance().getFormatter(schemaConfig, SchemaManager.getInstance()); - messageFormatter.parse(jsonObject.toString().getBytes(StandardCharsets.UTF_8), ParseMode.STRICT); - } catch (IOException exception) { - System.err.println("---------------------------------------------"); - System.err.println("Original:" + jsonObject); - System.err.println("---------------------------------------------"); - exception.printStackTrace(); - } - } -} \ No newline at end of file diff --git a/src/main/java/io/mapsmessaging/state/stanag/messages/TaskState.java b/src/main/java/io/mapsmessaging/state/stanag/messages/TaskState.java deleted file mode 100644 index 033bbe89d..000000000 --- a/src/main/java/io/mapsmessaging/state/stanag/messages/TaskState.java +++ /dev/null @@ -1,53 +0,0 @@ -/* - * - * Copyright [ 2020 - 2024 ] Matthew Buckton - * Copyright [ 2024 - 2026 ] MapsMessaging B.V. - * - * Licensed under the Apache License, Version 2.0 with the Commons Clause - * (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 - * https://commonsclause.com/ - * - * 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 io.mapsmessaging.state.stanag.messages; - -import lombok.Getter; -import lombok.RequiredArgsConstructor; - -@Getter -@RequiredArgsConstructor -public enum TaskState { - - PENDING(false), - ACTIVE(false), - RECALLING(false), - PREEMPTING(false), - PAUSING(false), - PAUSED(false), - RESUMING(false), - ON_HOLD(false), - ACTIONABLE(false), - PREPARED(false), - PLANNING(false), - WAITING_FOR_PUSH_ACK(false), - WAITING_FOR_CANCEL_ACK(false), - WAITING_FOR_PAUSE_ACK(false), - WAITING_FOR_RESUME_ACK(false), - - REJECTED(true), - RECALLED(true), - PREEMPTED(true), - ABORTED(true), - SUCCEEDED(true), - LOST(true); - - private final boolean terminal; -} \ No newline at end of file diff --git a/src/main/java/io/mapsmessaging/state/stanag/messages/TaskStatusContext.java b/src/main/java/io/mapsmessaging/state/stanag/messages/TaskStatusContext.java deleted file mode 100644 index f726b63ce..000000000 --- a/src/main/java/io/mapsmessaging/state/stanag/messages/TaskStatusContext.java +++ /dev/null @@ -1,27 +0,0 @@ -/* - * - * Copyright [ 2020 - 2024 ] Matthew Buckton - * Copyright [ 2024 - 2026 ] MapsMessaging B.V. - * - * Licensed under the Apache License, Version 2.0 with the Commons Clause - * (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 - * https://commonsclause.com/ - * - * 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 io.mapsmessaging.state.stanag.messages; - -import io.mapsmessaging.state.config.capability.Authorities; - -import java.util.UUID; - -public record TaskStatusContext(UUID taskIdentifier, UUID nodeIdentifier, Authorities authority) { -} \ No newline at end of file diff --git a/src/main/java/io/mapsmessaging/state/stanag/messages/core/MessageHeader.java b/src/main/java/io/mapsmessaging/state/stanag/messages/core/MessageHeader.java deleted file mode 100644 index 45f00510f..000000000 --- a/src/main/java/io/mapsmessaging/state/stanag/messages/core/MessageHeader.java +++ /dev/null @@ -1,40 +0,0 @@ -/* - * - * Copyright [ 2020 - 2024 ] Matthew Buckton - * Copyright [ 2024 - 2026 ] MapsMessaging B.V. - * - * Licensed under the Apache License, Version 2.0 with the Commons Clause - * (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 - * https://commonsclause.com/ - * - * 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 io.mapsmessaging.state.stanag.messages.core; - -import com.google.gson.annotations.SerializedName; -import lombok.AllArgsConstructor; -import lombok.Getter; - -import java.time.Instant; - -@AllArgsConstructor -@Getter -public class MessageHeader { - @SerializedName("message_type") - private final MessageType messageType; - - private final String source; - - @SerializedName("time_sent") - private final Instant timeSent; - - private final String version; -} \ No newline at end of file diff --git a/src/main/java/io/mapsmessaging/state/stanag/messages/core/MessageHeaderBuilder.java b/src/main/java/io/mapsmessaging/state/stanag/messages/core/MessageHeaderBuilder.java deleted file mode 100644 index 85d85ceb2..000000000 --- a/src/main/java/io/mapsmessaging/state/stanag/messages/core/MessageHeaderBuilder.java +++ /dev/null @@ -1,52 +0,0 @@ -/* - * - * Copyright [ 2020 - 2024 ] Matthew Buckton - * Copyright [ 2024 - 2026 ] MapsMessaging B.V. - * - * Licensed under the Apache License, Version 2.0 with the Commons Clause - * (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 - * https://commonsclause.com/ - * - * 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 io.mapsmessaging.state.stanag.messages.core; - -import lombok.RequiredArgsConstructor; - -import java.time.Clock; -import java.time.Instant; -import java.time.temporal.ChronoUnit; -import java.util.Objects; -import java.util.UUID; - -@RequiredArgsConstructor -public class MessageHeaderBuilder { - - private static final String VERSION = "0.3.0"; - - private final Clock clock; - - public MessageHeader build(MessageType messageType, UUID sourceIdentifier, Instant timestamp) { - Objects.requireNonNull(messageType, "messageType cannot be null"); - Objects.requireNonNull(sourceIdentifier, "sourceIdentifier cannot be null"); - - Instant resolvedTimestamp = timestamp != null ? timestamp : now(); - return new MessageHeader(messageType, sourceIdentifier.toString(), truncate(resolvedTimestamp), VERSION); - } - - private Instant now() { - return Instant.now(clock); - } - - private Instant truncate(Instant timestamp) { - return timestamp.truncatedTo(ChronoUnit.MILLIS); - } -} \ No newline at end of file diff --git a/src/main/java/io/mapsmessaging/state/stanag/messages/core/MessageType.java b/src/main/java/io/mapsmessaging/state/stanag/messages/core/MessageType.java deleted file mode 100644 index e535d21f6..000000000 --- a/src/main/java/io/mapsmessaging/state/stanag/messages/core/MessageType.java +++ /dev/null @@ -1,33 +0,0 @@ -/* - * - * Copyright [ 2020 - 2024 ] Matthew Buckton - * Copyright [ 2024 - 2026 ] MapsMessaging B.V. - * - * Licensed under the Apache License, Version 2.0 with the Commons Clause - * (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 - * https://commonsclause.com/ - * - * 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 io.mapsmessaging.state.stanag.messages.core; - -import lombok.Getter; - -@Getter -public enum MessageType { - TASK_ADMIN, - NODE_STATUS, - DYNAMIC_UPDATE, - CHAT, - TASK_FEEDBACK, - NODE_DESCRIPTION, - TASK_RESULT -} \ No newline at end of file diff --git a/src/main/java/io/mapsmessaging/state/stanag/messages/node/common/EntityDescription.java b/src/main/java/io/mapsmessaging/state/stanag/messages/node/common/EntityDescription.java deleted file mode 100644 index 3988c792c..000000000 --- a/src/main/java/io/mapsmessaging/state/stanag/messages/node/common/EntityDescription.java +++ /dev/null @@ -1,70 +0,0 @@ -/* - * - * Copyright [ 2020 - 2024 ] Matthew Buckton - * Copyright [ 2024 - 2026 ] MapsMessaging B.V. - * - * Licensed under the Apache License, Version 2.0 with the Commons Clause - * (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 - * https://commonsclause.com/ - * - * 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 io.mapsmessaging.state.stanag.messages.node.common; - -import com.google.gson.annotations.SerializedName; -import lombok.Builder; -import lombok.Getter; - -@Getter -@Builder -public class EntityDescription { - - private final String version; - - private final String name; - - private final String description; - - @SerializedName("context_type") - private final String contextType; - - @SerializedName("standard_identity") - private final String standardIdentity; - - @SerializedName("symbol_set") - private final String symbolSet; - - private final String status; - - private final String entity; - - @SerializedName("entity_type") - private final String entityType; - - @SerializedName("entity_subtype") - private final String entitySubtype; - - @SerializedName("sector_1") - private final String sector1; - - @SerializedName("sector_2") - private final String sector2; - - @SerializedName("headquarters_task_force_dummy") - private final String headquartersTaskForceDummy; - - @SerializedName("unit_echelon_equipment_mobility") - private final String unitEchelonEquipmentMobility; - - private final String organization; - - private final String nationality; -} \ No newline at end of file diff --git a/src/main/java/io/mapsmessaging/state/stanag/messages/node/common/EulerAngles.java b/src/main/java/io/mapsmessaging/state/stanag/messages/node/common/EulerAngles.java deleted file mode 100644 index 96d8a37e5..000000000 --- a/src/main/java/io/mapsmessaging/state/stanag/messages/node/common/EulerAngles.java +++ /dev/null @@ -1,34 +0,0 @@ -/* - * - * Copyright [ 2020 - 2024 ] Matthew Buckton - * Copyright [ 2024 - 2026 ] MapsMessaging B.V. - * - * Licensed under the Apache License, Version 2.0 with the Commons Clause - * (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 - * https://commonsclause.com/ - * - * 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 io.mapsmessaging.state.stanag.messages.node.common; - -import lombok.Builder; -import lombok.Getter; - -@Getter -@Builder -public class EulerAngles { - - private final Double roll; - - private final Double pitch; - - private final Double yaw; -} \ No newline at end of file diff --git a/src/main/java/io/mapsmessaging/state/stanag/messages/node/common/LatitudeLongitudeAltitude.java b/src/main/java/io/mapsmessaging/state/stanag/messages/node/common/LatitudeLongitudeAltitude.java deleted file mode 100644 index cce839450..000000000 --- a/src/main/java/io/mapsmessaging/state/stanag/messages/node/common/LatitudeLongitudeAltitude.java +++ /dev/null @@ -1,36 +0,0 @@ -/* - * - * Copyright [ 2020 - 2024 ] Matthew Buckton - * Copyright [ 2024 - 2026 ] MapsMessaging B.V. - * - * Licensed under the Apache License, Version 2.0 with the Commons Clause - * (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 - * https://commonsclause.com/ - * - * 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 io.mapsmessaging.state.stanag.messages.node.common; - -import lombok.Builder; -import lombok.Getter; - -import java.util.List; - -@Getter -@Builder -public class LatitudeLongitudeAltitude { - - private final Double latitude; - - private final Double longitude; - - private final List altitude; -} \ No newline at end of file diff --git a/src/main/java/io/mapsmessaging/state/stanag/messages/node/common/NodeMessageSupport.java b/src/main/java/io/mapsmessaging/state/stanag/messages/node/common/NodeMessageSupport.java deleted file mode 100644 index 49a4f794b..000000000 --- a/src/main/java/io/mapsmessaging/state/stanag/messages/node/common/NodeMessageSupport.java +++ /dev/null @@ -1,146 +0,0 @@ -/* - * - * Copyright [ 2020 - 2024 ] Matthew Buckton - * Copyright [ 2024 - 2026 ] MapsMessaging B.V. - * - * Licensed under the Apache License, Version 2.0 with the Commons Clause - * (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 - * https://commonsclause.com/ - * - * 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 io.mapsmessaging.state.stanag.messages.node.common; - -import io.mapsmessaging.state.drone.drone.DroneTwin; -import io.mapsmessaging.state.drone.model.GeoPosition; - -import java.util.HashMap; -import java.util.List; -import java.util.Map; - -public class NodeMessageSupport { - - private static final String POSITION_TYPE_LATITUDE_LONGITUDE_ALTITUDE = "PositionTypeEnum_LATITUDE_LONGITUDE_ALTITUDE"; - private static final String ALTITUDE_TYPE_WGS = "AltitudeTypeEnum_WGS"; - private static final String ORIENTATION_TYPE_EULER_ANGLES = "OrientationTypeEnum_EULER_ANGLES"; - private static final String VELOCITY_TYPE_SPEED_COURSE_CLIMB_RATE = "VelocityTypeEnum_SPEED_COURSE_CLIMB_RATE"; - - public Map buildDescription(DroneTwin droneTwin) { - Map description = new HashMap<>(); - - if (droneTwin.getDescription() != null) { - description.putAll(droneTwin.getDescription()); - } - - if (droneTwin.getCallSign() != null) { - description.put("name", droneTwin.getCallSign()); - } - - return description; - } - - public Pose buildPose(DroneTwin droneTwin) { - return Pose.builder() - .position(buildPosition(droneTwin.getGeoPosition())) - .orientation(buildOrientation(droneTwin.getOrientation())) - .build(); - } - - public Velocity buildVelocity(DroneTwin droneTwin) { - return Velocity.builder() - .discriminator(VELOCITY_TYPE_SPEED_COURSE_CLIMB_RATE) - .speedCourseClimbRate(buildSpeedCourseClimbRate(droneTwin)) - .build(); - } - - private Position buildPosition(GeoPosition geoPosition) { - return Position.builder() - .discriminator(POSITION_TYPE_LATITUDE_LONGITUDE_ALTITUDE) - .latitudeLongitudeAltitude(buildLatitudeLongitudeAltitude(geoPosition)) - .build(); - } - - private LatitudeLongitudeAltitude buildLatitudeLongitudeAltitude(GeoPosition geoPosition) { - if (geoPosition == null) { - return LatitudeLongitudeAltitude.builder().build(); - } - - return LatitudeLongitudeAltitude.builder() - .latitude(geoPosition.getLatitude()) - .longitude(geoPosition.getLongitude()) - .altitude(buildAltitude(geoPosition)) - .build(); - } - - private List buildAltitude(GeoPosition geoPosition) { - if (geoPosition.getAltitudeMslMeters() == null) { - return null; - } - - Altitude altitude = Altitude.builder() - .value(geoPosition.getAltitudeMslMeters()) - .type(ALTITUDE_TYPE_WGS) - .build(); - - return List.of(altitude); - } - - private Orientation buildOrientation(io.mapsmessaging.state.drone.model.Orientation orientation) { - return Orientation.builder() - .discriminator(ORIENTATION_TYPE_EULER_ANGLES) - .eulerAngles(buildEulerAngles(orientation)) - .build(); - } - - private EulerAngles buildEulerAngles(io.mapsmessaging.state.drone.model.Orientation orientation) { - if (orientation == null) { - return EulerAngles.builder().build(); - } - - return EulerAngles.builder() - .roll(orientation.getRollDegrees()) - .pitch(orientation.getPitchDegrees()) - .yaw(orientation.getYawDegrees()) - .build(); - } - - private SpeedCourseClimbRate buildSpeedCourseClimbRate(DroneTwin droneTwin) { - return SpeedCourseClimbRate.builder() - .speed(droneTwin.getGroundSpeedMetersPerSecond()) - .course(droneTwin.getHeadingDegrees()) - .climbRate(normaliseZero(droneTwin.getVerticalSpeedMetersPerSecond())) - .build(); - } - - private Double normaliseZero(Double value) { - if (value == null) { - return null; - } - - if (Double.compare(value, -0.0d) == 0 || Math.abs(value) < 0.0000001d) { - return 0.0d; - } - - return value; - } - - public Pose buildPose(GeoPosition geoPosition) { - Position position = buildPosition(geoPosition); - - if (position == null) { - return null; - } - - return Pose.builder() - .position(position) - .build(); - } -} \ No newline at end of file diff --git a/src/main/java/io/mapsmessaging/state/stanag/messages/node/common/Orientation.java b/src/main/java/io/mapsmessaging/state/stanag/messages/node/common/Orientation.java deleted file mode 100644 index a1f317a0f..000000000 --- a/src/main/java/io/mapsmessaging/state/stanag/messages/node/common/Orientation.java +++ /dev/null @@ -1,35 +0,0 @@ -/* - * - * Copyright [ 2020 - 2024 ] Matthew Buckton - * Copyright [ 2024 - 2026 ] MapsMessaging B.V. - * - * Licensed under the Apache License, Version 2.0 with the Commons Clause - * (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 - * https://commonsclause.com/ - * - * 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 io.mapsmessaging.state.stanag.messages.node.common; - -import com.google.gson.annotations.SerializedName; -import lombok.Builder; -import lombok.Getter; - -@Getter -@Builder -public class Orientation { - - @SerializedName("$discriminator") - private final String discriminator; - - @SerializedName("euler_angles") - private final EulerAngles eulerAngles; -} \ No newline at end of file diff --git a/src/main/java/io/mapsmessaging/state/stanag/messages/node/common/Pose.java b/src/main/java/io/mapsmessaging/state/stanag/messages/node/common/Pose.java deleted file mode 100644 index 747ff2400..000000000 --- a/src/main/java/io/mapsmessaging/state/stanag/messages/node/common/Pose.java +++ /dev/null @@ -1,32 +0,0 @@ -/* - * - * Copyright [ 2020 - 2024 ] Matthew Buckton - * Copyright [ 2024 - 2026 ] MapsMessaging B.V. - * - * Licensed under the Apache License, Version 2.0 with the Commons Clause - * (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 - * https://commonsclause.com/ - * - * 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 io.mapsmessaging.state.stanag.messages.node.common; - -import lombok.Builder; -import lombok.Getter; - -@Getter -@Builder -public class Pose { - - private final Position position; - - private final Orientation orientation; -} \ No newline at end of file diff --git a/src/main/java/io/mapsmessaging/state/stanag/messages/node/common/Position.java b/src/main/java/io/mapsmessaging/state/stanag/messages/node/common/Position.java deleted file mode 100644 index 896c923a0..000000000 --- a/src/main/java/io/mapsmessaging/state/stanag/messages/node/common/Position.java +++ /dev/null @@ -1,35 +0,0 @@ -/* - * - * Copyright [ 2020 - 2024 ] Matthew Buckton - * Copyright [ 2024 - 2026 ] MapsMessaging B.V. - * - * Licensed under the Apache License, Version 2.0 with the Commons Clause - * (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 - * https://commonsclause.com/ - * - * 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 io.mapsmessaging.state.stanag.messages.node.common; - -import com.google.gson.annotations.SerializedName; -import lombok.Builder; -import lombok.Getter; - -@Getter -@Builder -public class Position { - - @SerializedName("$discriminator") - private final String discriminator; - - @SerializedName("latitude_longitude_altitude") - private final LatitudeLongitudeAltitude latitudeLongitudeAltitude; -} \ No newline at end of file diff --git a/src/main/java/io/mapsmessaging/state/stanag/messages/node/common/SpeedCourseClimbRate.java b/src/main/java/io/mapsmessaging/state/stanag/messages/node/common/SpeedCourseClimbRate.java deleted file mode 100644 index ae848863a..000000000 --- a/src/main/java/io/mapsmessaging/state/stanag/messages/node/common/SpeedCourseClimbRate.java +++ /dev/null @@ -1,36 +0,0 @@ -/* - * - * Copyright [ 2020 - 2024 ] Matthew Buckton - * Copyright [ 2024 - 2026 ] MapsMessaging B.V. - * - * Licensed under the Apache License, Version 2.0 with the Commons Clause - * (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 - * https://commonsclause.com/ - * - * 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 io.mapsmessaging.state.stanag.messages.node.common; - -import com.google.gson.annotations.SerializedName; -import lombok.Builder; -import lombok.Getter; - -@Getter -@Builder -public class SpeedCourseClimbRate { - - private final Double speed; - - private final Double course; - - @SerializedName("climb_rate") - private final Double climbRate; -} \ No newline at end of file diff --git a/src/main/java/io/mapsmessaging/state/stanag/messages/node/common/Velocity.java b/src/main/java/io/mapsmessaging/state/stanag/messages/node/common/Velocity.java deleted file mode 100644 index 25c099e30..000000000 --- a/src/main/java/io/mapsmessaging/state/stanag/messages/node/common/Velocity.java +++ /dev/null @@ -1,35 +0,0 @@ -/* - * - * Copyright [ 2020 - 2024 ] Matthew Buckton - * Copyright [ 2024 - 2026 ] MapsMessaging B.V. - * - * Licensed under the Apache License, Version 2.0 with the Commons Clause - * (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 - * https://commonsclause.com/ - * - * 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 io.mapsmessaging.state.stanag.messages.node.common; - -import com.google.gson.annotations.SerializedName; -import lombok.Builder; -import lombok.Getter; - -@Getter -@Builder -public class Velocity { - - @SerializedName("$discriminator") - private final String discriminator; - - @SerializedName("speed_course_climb_rate") - private final SpeedCourseClimbRate speedCourseClimbRate; -} \ No newline at end of file diff --git a/src/main/java/io/mapsmessaging/state/stanag/messages/node/description/NodeDescription.java b/src/main/java/io/mapsmessaging/state/stanag/messages/node/description/NodeDescription.java deleted file mode 100644 index 4f97b8ea1..000000000 --- a/src/main/java/io/mapsmessaging/state/stanag/messages/node/description/NodeDescription.java +++ /dev/null @@ -1,34 +0,0 @@ -/* - * - * Copyright [ 2020 - 2024 ] Matthew Buckton - * Copyright [ 2024 - 2026 ] MapsMessaging B.V. - * - * Licensed under the Apache License, Version 2.0 with the Commons Clause - * (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 - * https://commonsclause.com/ - * - * 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 io.mapsmessaging.state.stanag.messages.node.description; - - -import io.mapsmessaging.state.stanag.messages.core.MessageHeader; -import lombok.AllArgsConstructor; -import lombok.Getter; - -@Getter -@AllArgsConstructor -public class NodeDescription { - - private final MessageHeader header; - - private final NodeDescriptionBody body; -} \ No newline at end of file diff --git a/src/main/java/io/mapsmessaging/state/stanag/messages/node/description/NodeDescriptionBody.java b/src/main/java/io/mapsmessaging/state/stanag/messages/node/description/NodeDescriptionBody.java deleted file mode 100644 index 5029ba346..000000000 --- a/src/main/java/io/mapsmessaging/state/stanag/messages/node/description/NodeDescriptionBody.java +++ /dev/null @@ -1,38 +0,0 @@ -package io.mapsmessaging.state.stanag.messages.node.description; - -import com.google.gson.annotations.SerializedName; -import io.mapsmessaging.state.config.capability.TaskCapabilities; -import io.mapsmessaging.state.stanag.messages.node.common.Pose; -import io.mapsmessaging.state.stanag.messages.node.common.Velocity; -import lombok.Builder; -import lombok.Getter; - -import java.time.Instant; -import java.util.Map; -import java.util.UUID; - -@Getter -@Builder -public class NodeDescriptionBody { - - private final UUID identifier; - - private final Map description; - - private final TaskCapabilities capabilities; - - private final Instant timestamp; - - @SerializedName("time_of_validity") - private final Instant timeOfValidity; - - private final Pose pose; - - private final Velocity velocity; - - @SerializedName("time_of_initiation") - private final Instant timeOfInitiation; - - @SerializedName("source_of_information") - private final UUID sourceOfInformation; -} \ No newline at end of file diff --git a/src/main/java/io/mapsmessaging/state/stanag/messages/node/description/NodeDescriptionBuilder.java b/src/main/java/io/mapsmessaging/state/stanag/messages/node/description/NodeDescriptionBuilder.java deleted file mode 100644 index 30b118c97..000000000 --- a/src/main/java/io/mapsmessaging/state/stanag/messages/node/description/NodeDescriptionBuilder.java +++ /dev/null @@ -1,42 +0,0 @@ -package io.mapsmessaging.state.stanag.messages.node.description; - -import io.mapsmessaging.MessageDaemon; -import io.mapsmessaging.state.drone.drone.DroneTwin; -import io.mapsmessaging.state.stanag.messages.core.MessageHeader; -import io.mapsmessaging.state.stanag.messages.core.MessageHeaderBuilder; -import io.mapsmessaging.state.stanag.messages.core.MessageType; -import io.mapsmessaging.state.stanag.messages.node.common.NodeMessageSupport; -import lombok.RequiredArgsConstructor; - -import java.util.Objects; - -@RequiredArgsConstructor -public class NodeDescriptionBuilder { - - private final MessageHeaderBuilder messageHeaderBuilder; - - private final NodeMessageSupport nodeMessageSupport; - - public NodeDescription build(DroneTwin droneTwin) { - Objects.requireNonNull(droneTwin, "droneTwin cannot be null"); - Objects.requireNonNull(droneTwin.getUuid(), "droneTwin uuid cannot be null"); - - MessageHeader header = messageHeaderBuilder.build( - MessageType.NODE_DESCRIPTION, - droneTwin.getUuid(), - droneTwin.getLastSeenAt()); - - NodeDescriptionBody body = NodeDescriptionBody.builder() - .identifier(droneTwin.getUuid()) - .description(nodeMessageSupport.buildDescription(droneTwin)) - .capabilities(droneTwin.getCapabilities()) - .timestamp(droneTwin.getOperationalUpdatedAt()) - .timeOfValidity(droneTwin.getValidTill()) - .pose(nodeMessageSupport.buildPose(droneTwin)) - .velocity(nodeMessageSupport.buildVelocity(droneTwin)) - .timeOfInitiation(droneTwin.getCreatedAt()) - .build(); - - return new NodeDescription(header, body); - } -} \ No newline at end of file diff --git a/src/main/java/io/mapsmessaging/state/stanag/messages/node/dynamic/DynamicUpdate.java b/src/main/java/io/mapsmessaging/state/stanag/messages/node/dynamic/DynamicUpdate.java deleted file mode 100644 index 03920a5ab..000000000 --- a/src/main/java/io/mapsmessaging/state/stanag/messages/node/dynamic/DynamicUpdate.java +++ /dev/null @@ -1,14 +0,0 @@ -package io.mapsmessaging.state.stanag.messages.node.dynamic; - -import io.mapsmessaging.state.stanag.messages.core.MessageHeader; -import lombok.AllArgsConstructor; -import lombok.Getter; - -@Getter -@AllArgsConstructor -public class DynamicUpdate { - - private final MessageHeader header; - - private final DynamicUpdateBody body; -} \ No newline at end of file diff --git a/src/main/java/io/mapsmessaging/state/stanag/messages/node/dynamic/DynamicUpdateBody.java b/src/main/java/io/mapsmessaging/state/stanag/messages/node/dynamic/DynamicUpdateBody.java deleted file mode 100644 index d490c2aff..000000000 --- a/src/main/java/io/mapsmessaging/state/stanag/messages/node/dynamic/DynamicUpdateBody.java +++ /dev/null @@ -1,12 +0,0 @@ -package io.mapsmessaging.state.stanag.messages.node.dynamic; - -import lombok.Builder; -import lombok.Getter; - -@Getter -@Builder -public class DynamicUpdateBody { - - private final DynamicUpdateOperation operation; - -} \ No newline at end of file diff --git a/src/main/java/io/mapsmessaging/state/stanag/messages/node/dynamic/DynamicUpdateBuilder.java b/src/main/java/io/mapsmessaging/state/stanag/messages/node/dynamic/DynamicUpdateBuilder.java deleted file mode 100644 index a40aa05e4..000000000 --- a/src/main/java/io/mapsmessaging/state/stanag/messages/node/dynamic/DynamicUpdateBuilder.java +++ /dev/null @@ -1,146 +0,0 @@ -package io.mapsmessaging.state.stanag.messages.node.dynamic; - -import io.mapsmessaging.MessageDaemon; -import io.mapsmessaging.state.drone.drone.DroneTwin; -import io.mapsmessaging.state.drone.model.Contact; -import io.mapsmessaging.state.stanag.messages.core.MessageHeader; -import io.mapsmessaging.state.stanag.messages.core.MessageHeaderBuilder; -import io.mapsmessaging.state.stanag.messages.core.MessageType; -import io.mapsmessaging.state.stanag.messages.node.common.EntityDescription; -import io.mapsmessaging.state.stanag.messages.node.common.NodeMessageSupport; -import io.mapsmessaging.state.stanag.messages.node.common.Pose; -import lombok.RequiredArgsConstructor; - -import java.time.Instant; -import java.util.Objects; -import java.util.Optional; -import java.util.UUID; - -@RequiredArgsConstructor -public class DynamicUpdateBuilder { - - private static final String OPERATION_TYPE_PUT_VALUE = "DynamicUpdateOperationTypeEnum_PUT_VALUE"; - - private static final String VALUE_TYPE_TRACK = "ValueTypeEnum_TRACK"; - - private static final String CONTEXT_TYPE_SIMULATION = "ContextEnum_SIMULATION"; - - private static final String STANDARD_IDENTITY_FRIEND = "StandardIdentityEnum_FRIEND"; - - private static final String SYMBOL_SET_SEA_SURFACE = "SymbolSetEnum_SEA_SURFACE"; - - private static final String ENTITY_STATUS_PRESENT = "EntityStatusEnum_PRESENT"; - - private static final String ORGANIZATION_MAPS = "MAPS"; - - private static final String NATIONALITY_EST = "EST"; - - private static final String ENTITY_VERSION = "10"; - - private static final String DEFAULT_ENTITY = "11"; - - private static final String DEFAULT_ENTITY_TYPE = "11"; - - private static final String DEFAULT_ENTITY_SUBTYPE = "00"; - - private static final String DEFAULT_SECTOR_1 = "00"; - - private static final String DEFAULT_SECTOR_2 = "00"; - - private static final String HEADQUARTERS_TASK_FORCE_DUMMY_NOT_APPLICABLE = "HeadquartersTaskForceDummyEnum_NOT_APPLICABLE"; - - private static final String UNIT_ECHELON_EQUIPMENT_MOBILITY_UNKNOWN = "UnitEchelonEquipmentMobilityEnum_UNKNOWN"; - - private static final String TRACK_PHASE_TRACKED = "TrackPhase_TRACKED"; - - private final MessageHeaderBuilder messageHeaderBuilder; - - private final NodeMessageSupport nodeMessageSupport; - - public Optional build(DroneTwin droneTwin, Contact contact) { - Objects.requireNonNull(droneTwin, "droneTwin cannot be null"); - Objects.requireNonNull(contact, "contact cannot be null"); - Objects.requireNonNull(droneTwin.getUuid(), "droneTwin uuid cannot be null"); - - if (contact.getId() == null || contact.getUpdatedTimeMs() <= 0) { - return Optional.empty(); - } - - Pose pose = nodeMessageSupport.buildPose(contact.getPosition()); - - if (pose == null) { - return Optional.empty(); - } - - MessageHeader header = messageHeaderBuilder.build( - MessageType.DYNAMIC_UPDATE, - droneTwin.getUuid(), - droneTwin.getLastSeenAt()); - - Track track = Track.builder() - .identifier(contact.getId()) - .description(buildEntityDescription(contact)) - .timestamp(Instant.ofEpochMilli(contact.getUpdatedTimeMs())) - .pose(pose) - .timeOfInitiation(buildCreatedTimestamp(contact)) - .timeOfValidity(buildValidityTimestamp(contact)) - .trackPhase(TRACK_PHASE_TRACKED) - .build(); - - PutValue putValue = PutValue.builder() - .discriminator(VALUE_TYPE_TRACK) - .track(track) - .build(); - - DynamicUpdateOperation operation = DynamicUpdateOperation.builder() - .discriminator(OPERATION_TYPE_PUT_VALUE) - .putValue(putValue) - .build(); - - DynamicUpdateBody body = DynamicUpdateBody.builder() - .operation(operation) - .build(); - - return Optional.of(new DynamicUpdate(header, body)); - } - - private EntityDescription buildEntityDescription(Contact contact) { - if (contact.getDescription() == null || contact.getDescription().isBlank()) { - return null; - } - - return EntityDescription.builder() - .version(ENTITY_VERSION) - .name(contact.getDescription()) - .contextType(CONTEXT_TYPE_SIMULATION) - .standardIdentity(STANDARD_IDENTITY_FRIEND) - .symbolSet(SYMBOL_SET_SEA_SURFACE) - .status(ENTITY_STATUS_PRESENT) - .entity(DEFAULT_ENTITY) - .entityType(DEFAULT_ENTITY_TYPE) - .entitySubtype(DEFAULT_ENTITY_SUBTYPE) - .sector1(DEFAULT_SECTOR_1) - .sector2(DEFAULT_SECTOR_2) - .headquartersTaskForceDummy(HEADQUARTERS_TASK_FORCE_DUMMY_NOT_APPLICABLE) - .unitEchelonEquipmentMobility(UNIT_ECHELON_EQUIPMENT_MOBILITY_UNKNOWN) - .organization(ORGANIZATION_MAPS) - .nationality(NATIONALITY_EST) - .build(); - } - - private Instant buildCreatedTimestamp(Contact contact) { - if (contact.getCreatedTimeMs() <= 0) { - return null; - } - - return Instant.ofEpochMilli(contact.getCreatedTimeMs()); - } - - private Instant buildValidityTimestamp(Contact contact) { - if (contact.getUpdatedTimeMs() <= 0 || contact.getTtlMillis() <= 0) { - return null; - } - - return Instant.ofEpochMilli(contact.getUpdatedTimeMs() + contact.getTtlMillis()); - } -} \ No newline at end of file diff --git a/src/main/java/io/mapsmessaging/state/stanag/messages/node/dynamic/DynamicUpdateOperation.java b/src/main/java/io/mapsmessaging/state/stanag/messages/node/dynamic/DynamicUpdateOperation.java deleted file mode 100644 index d2044cc6e..000000000 --- a/src/main/java/io/mapsmessaging/state/stanag/messages/node/dynamic/DynamicUpdateOperation.java +++ /dev/null @@ -1,16 +0,0 @@ -package io.mapsmessaging.state.stanag.messages.node.dynamic; - -import com.google.gson.annotations.SerializedName; -import lombok.Builder; -import lombok.Getter; - -@Getter -@Builder -public class DynamicUpdateOperation { - - @SerializedName("$discriminator") - private final String discriminator; - - @SerializedName("put_value") - private final PutValue putValue; -} \ No newline at end of file diff --git a/src/main/java/io/mapsmessaging/state/stanag/messages/node/dynamic/PutValue.java b/src/main/java/io/mapsmessaging/state/stanag/messages/node/dynamic/PutValue.java deleted file mode 100644 index f0615b61e..000000000 --- a/src/main/java/io/mapsmessaging/state/stanag/messages/node/dynamic/PutValue.java +++ /dev/null @@ -1,15 +0,0 @@ -package io.mapsmessaging.state.stanag.messages.node.dynamic; - -import com.google.gson.annotations.SerializedName; -import lombok.Builder; -import lombok.Getter; - -@Getter -@Builder -public class PutValue { - - @SerializedName("$discriminator") - private final String discriminator; - - private final Track track; -} \ No newline at end of file diff --git a/src/main/java/io/mapsmessaging/state/stanag/messages/node/dynamic/Track.java b/src/main/java/io/mapsmessaging/state/stanag/messages/node/dynamic/Track.java deleted file mode 100644 index 2f7257558..000000000 --- a/src/main/java/io/mapsmessaging/state/stanag/messages/node/dynamic/Track.java +++ /dev/null @@ -1,38 +0,0 @@ -package io.mapsmessaging.state.stanag.messages.node.dynamic; - -import com.google.gson.annotations.SerializedName; -import io.mapsmessaging.state.stanag.messages.node.common.EntityDescription; -import io.mapsmessaging.state.stanag.messages.node.common.Pose; -import io.mapsmessaging.state.stanag.messages.node.common.Velocity; -import lombok.Builder; -import lombok.Getter; - -import java.time.Instant; -import java.util.UUID; - -@Getter -@Builder -public class Track { - - private final UUID identifier; - - private final EntityDescription description; - - private final Instant timestamp; - - private final Pose pose; - - @SerializedName("track_phase") - private final String trackPhase; - - @SerializedName("source_of_information") - private final UUID sourceOfInformation; - - @SerializedName("time_of_initiation") - private final Instant timeOfInitiation; - - @SerializedName("time_of_validity") - private final Instant timeOfValidity; - - private final Velocity velocity; -} \ No newline at end of file diff --git a/src/main/java/io/mapsmessaging/state/stanag/messages/node/status/NodeStatus.java b/src/main/java/io/mapsmessaging/state/stanag/messages/node/status/NodeStatus.java deleted file mode 100644 index 1b5f9f577..000000000 --- a/src/main/java/io/mapsmessaging/state/stanag/messages/node/status/NodeStatus.java +++ /dev/null @@ -1,33 +0,0 @@ -/* - * - * Copyright [ 2020 - 2024 ] Matthew Buckton - * Copyright [ 2024 - 2026 ] MapsMessaging B.V. - * - * Licensed under the Apache License, Version 2.0 with the Commons Clause - * (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 - * https://commonsclause.com/ - * - * 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 io.mapsmessaging.state.stanag.messages.node.status; - -import io.mapsmessaging.state.stanag.messages.core.MessageHeader; -import lombok.AllArgsConstructor; -import lombok.Getter; - -@Getter -@AllArgsConstructor -public class NodeStatus { - - private final MessageHeader header; - - private final NodeStatusBody body; -} \ No newline at end of file diff --git a/src/main/java/io/mapsmessaging/state/stanag/messages/node/status/NodeStatusBody.java b/src/main/java/io/mapsmessaging/state/stanag/messages/node/status/NodeStatusBody.java deleted file mode 100644 index 0eea50aff..000000000 --- a/src/main/java/io/mapsmessaging/state/stanag/messages/node/status/NodeStatusBody.java +++ /dev/null @@ -1,35 +0,0 @@ -package io.mapsmessaging.state.stanag.messages.node.status; - -import com.google.gson.annotations.SerializedName; -import io.mapsmessaging.state.stanag.messages.node.common.Pose; -import io.mapsmessaging.state.stanag.messages.node.common.Velocity; -import lombok.Builder; -import lombok.Getter; - -import java.time.Instant; -import java.util.Map; -import java.util.UUID; - -@Getter -@Builder -public class NodeStatusBody { - - private final UUID identifier; - - private final Map description; - - private final Instant timestamp; - - @SerializedName("time_of_validity") - private final Instant timeOfValidity; - - private final Pose pose; - - private final Velocity velocity; - - @SerializedName("time_of_initiation") - private final Instant timeOfInitiation; - - @SerializedName("source_of_information") - private final UUID sourceOfInformation; -} \ No newline at end of file diff --git a/src/main/java/io/mapsmessaging/state/stanag/messages/node/status/NodeStatusBuilder.java b/src/main/java/io/mapsmessaging/state/stanag/messages/node/status/NodeStatusBuilder.java deleted file mode 100644 index 6205bc8b3..000000000 --- a/src/main/java/io/mapsmessaging/state/stanag/messages/node/status/NodeStatusBuilder.java +++ /dev/null @@ -1,41 +0,0 @@ -package io.mapsmessaging.state.stanag.messages.node.status; - -import io.mapsmessaging.MessageDaemon; -import io.mapsmessaging.state.drone.drone.DroneTwin; -import io.mapsmessaging.state.stanag.messages.core.MessageHeader; -import io.mapsmessaging.state.stanag.messages.core.MessageHeaderBuilder; -import io.mapsmessaging.state.stanag.messages.core.MessageType; -import io.mapsmessaging.state.stanag.messages.node.common.NodeMessageSupport; -import lombok.RequiredArgsConstructor; - -import java.util.Objects; - -@RequiredArgsConstructor -public class NodeStatusBuilder { - - private final MessageHeaderBuilder messageHeaderBuilder; - - private final NodeMessageSupport nodeMessageSupport; - - public NodeStatus build(DroneTwin droneTwin) { - Objects.requireNonNull(droneTwin, "droneTwin cannot be null"); - Objects.requireNonNull(droneTwin.getUuid(), "droneTwin uuid cannot be null"); - - MessageHeader header = messageHeaderBuilder.build( - MessageType.NODE_STATUS, - droneTwin.getUuid(), - droneTwin.getLastSeenAt()); - - NodeStatusBody body = NodeStatusBody.builder() - .identifier(droneTwin.getUuid()) - .description(nodeMessageSupport.buildDescription(droneTwin)) - .timestamp(droneTwin.getOperationalUpdatedAt()) - .timeOfValidity(droneTwin.getValidTill()) - .pose(nodeMessageSupport.buildPose(droneTwin)) - .velocity(nodeMessageSupport.buildVelocity(droneTwin)) - .timeOfInitiation(droneTwin.getCreatedAt()) - .build(); - - return new NodeStatus(header, body); - } -} \ No newline at end of file diff --git a/src/main/java/io/mapsmessaging/state/stanag/messages/task/admin/TaskAdminActionEnum.java b/src/main/java/io/mapsmessaging/state/stanag/messages/task/admin/TaskAdminActionEnum.java deleted file mode 100644 index c95accb65..000000000 --- a/src/main/java/io/mapsmessaging/state/stanag/messages/task/admin/TaskAdminActionEnum.java +++ /dev/null @@ -1,11 +0,0 @@ -package io.mapsmessaging.state.stanag.messages.task.admin; - -public enum TaskAdminActionEnum { - UPDATE, - PUSH, - PULL, - CANCEL, - PAUSE, - RESUME, - ASSIGN -} \ No newline at end of file diff --git a/src/main/java/io/mapsmessaging/state/stanag/messages/task/admin/TaskAdminBody.java b/src/main/java/io/mapsmessaging/state/stanag/messages/task/admin/TaskAdminBody.java deleted file mode 100644 index fb5934ab5..000000000 --- a/src/main/java/io/mapsmessaging/state/stanag/messages/task/admin/TaskAdminBody.java +++ /dev/null @@ -1,20 +0,0 @@ -package io.mapsmessaging.state.stanag.messages.task.admin; - -import io.mapsmessaging.state.config.capability.Authorities; -import lombok.Builder; -import lombok.Getter; - -import java.util.UUID; - -@Getter -@Builder -public class TaskAdminBody { - - private final UUID identifier; - - private final UUID node; - - private final TaskAdminActionEnum action; - - private final Authorities authority; -} \ No newline at end of file diff --git a/src/main/java/io/mapsmessaging/state/stanag/messages/task/admin/TaskAdminMessage.java b/src/main/java/io/mapsmessaging/state/stanag/messages/task/admin/TaskAdminMessage.java deleted file mode 100644 index 5cd0d5508..000000000 --- a/src/main/java/io/mapsmessaging/state/stanag/messages/task/admin/TaskAdminMessage.java +++ /dev/null @@ -1,14 +0,0 @@ -package io.mapsmessaging.state.stanag.messages.task.admin; - -import io.mapsmessaging.state.stanag.messages.core.MessageHeader; -import lombok.AllArgsConstructor; -import lombok.Getter; - -@AllArgsConstructor -@Getter -public class TaskAdminMessage { - - private final MessageHeader header; - - private final TaskAdminBody body; -} \ No newline at end of file diff --git a/src/main/java/io/mapsmessaging/state/stanag/messages/task/admin/TaskAdminMessageBuilder.java b/src/main/java/io/mapsmessaging/state/stanag/messages/task/admin/TaskAdminMessageBuilder.java deleted file mode 100644 index 27c86f9cd..000000000 --- a/src/main/java/io/mapsmessaging/state/stanag/messages/task/admin/TaskAdminMessageBuilder.java +++ /dev/null @@ -1,34 +0,0 @@ -package io.mapsmessaging.state.stanag.messages.task.admin; - -import io.mapsmessaging.MessageDaemon; -import io.mapsmessaging.state.config.capability.Authorities; -import io.mapsmessaging.state.stanag.messages.TaskStatusContext; -import io.mapsmessaging.state.stanag.messages.core.MessageHeader; -import io.mapsmessaging.state.stanag.messages.core.MessageHeaderBuilder; -import io.mapsmessaging.state.stanag.messages.core.MessageType; -import lombok.RequiredArgsConstructor; - -import java.time.Instant; -import java.util.Objects; - -@RequiredArgsConstructor -public class TaskAdminMessageBuilder { - - private final MessageHeaderBuilder headerBuilder; - - public TaskAdminMessage build(TaskStatusContext context, TaskAdminActionEnum action, Authorities authority) { - Objects.requireNonNull(context, "context cannot be null"); - Objects.requireNonNull(action, "action cannot be null"); - - MessageHeader header = headerBuilder.build(MessageType.TASK_ADMIN, context.nodeIdentifier() , Instant.now()); - - TaskAdminBody body = TaskAdminBody.builder() - .identifier(context.taskIdentifier()) - .node(context.nodeIdentifier()) - .action(action) - .authority(authority) - .build(); - - return new TaskAdminMessage(header, body); - } -} \ No newline at end of file diff --git a/src/main/java/io/mapsmessaging/state/stanag/messages/task/feedback/TaskFeedbackBody.java b/src/main/java/io/mapsmessaging/state/stanag/messages/task/feedback/TaskFeedbackBody.java deleted file mode 100644 index 93e5ab05d..000000000 --- a/src/main/java/io/mapsmessaging/state/stanag/messages/task/feedback/TaskFeedbackBody.java +++ /dev/null @@ -1,31 +0,0 @@ -package io.mapsmessaging.state.stanag.messages.task.feedback; - -import com.google.gson.annotations.SerializedName; -import io.mapsmessaging.state.stanag.messages.TaskState; -import lombok.Builder; -import lombok.Getter; -import lombok.Setter; - -import java.util.List; -import java.util.UUID; - -@Getter -@Setter -@Builder -public class TaskFeedbackBody { - - private final UUID identifier; - - private final UUID node; - - private final TaskState state; - - @SerializedName("percent_complete") - private Double percentComplete; - - @SerializedName("time_remaining") - private final String timeRemaining; - - @SerializedName("waypoints_remaining") - private final List waypointsRemaining; -} \ No newline at end of file diff --git a/src/main/java/io/mapsmessaging/state/stanag/messages/task/feedback/TaskFeedbackMessage.java b/src/main/java/io/mapsmessaging/state/stanag/messages/task/feedback/TaskFeedbackMessage.java deleted file mode 100644 index bd14b692e..000000000 --- a/src/main/java/io/mapsmessaging/state/stanag/messages/task/feedback/TaskFeedbackMessage.java +++ /dev/null @@ -1,14 +0,0 @@ -package io.mapsmessaging.state.stanag.messages.task.feedback; - -import io.mapsmessaging.state.stanag.messages.core.MessageHeader; -import lombok.AllArgsConstructor; -import lombok.Getter; - -@Getter -@AllArgsConstructor -public class TaskFeedbackMessage { - - private final MessageHeader header; - - private final TaskFeedbackBody body; -} \ No newline at end of file diff --git a/src/main/java/io/mapsmessaging/state/stanag/messages/task/feedback/TaskFeedbackMessageBuilder.java b/src/main/java/io/mapsmessaging/state/stanag/messages/task/feedback/TaskFeedbackMessageBuilder.java deleted file mode 100644 index 946a585a2..000000000 --- a/src/main/java/io/mapsmessaging/state/stanag/messages/task/feedback/TaskFeedbackMessageBuilder.java +++ /dev/null @@ -1,89 +0,0 @@ -package io.mapsmessaging.state.stanag.messages.task.feedback; - -import io.mapsmessaging.MessageDaemon; -import io.mapsmessaging.state.stanag.messages.TaskState; -import io.mapsmessaging.state.stanag.messages.TaskStatusContext; -import io.mapsmessaging.state.stanag.messages.core.MessageHeader; -import io.mapsmessaging.state.stanag.messages.core.MessageHeaderBuilder; -import io.mapsmessaging.state.stanag.messages.core.MessageType; -import lombok.RequiredArgsConstructor; - -import java.time.Instant; -import java.util.List; -import java.util.Objects; - -@RequiredArgsConstructor -public class TaskFeedbackMessageBuilder { - - private final MessageHeaderBuilder headerBuilder; - - public TaskFeedbackMessage buildActive(TaskStatusContext context) { - return build(context, TaskState.ACTIVE); - } - - public TaskFeedbackMessage buildProgress(TaskStatusContext context, double percentComplete) { - return build(context, TaskState.ACTIVE, percentComplete, null, null); - } - - public TaskFeedbackMessage buildWaypointsRemaining( - TaskStatusContext context, - List waypointsRemaining) { - - return build(context, TaskState.ACTIVE, null, null, waypointsRemaining); - } - - public TaskFeedbackMessage build(TaskStatusContext context, TaskState taskState) { - return build(context, taskState, null, null, null); - } - - public TaskFeedbackMessage build( - TaskStatusContext context, - TaskState taskState, - Double percentComplete, - String timeRemaining, - List waypointsRemaining) { - - Objects.requireNonNull(context, "context cannot be null"); - Objects.requireNonNull(taskState, "taskState cannot be null"); - - validateFeedbackState(taskState); - validatePercentComplete(percentComplete); - - MessageHeader header = headerBuilder.build(MessageType.TASK_FEEDBACK, context.nodeIdentifier(), Instant.now()); - - TaskFeedbackBody.TaskFeedbackBodyBuilder bodyBuilder = TaskFeedbackBody.builder() - .identifier(context.taskIdentifier()) - .node(context.nodeIdentifier()) - .state(taskState); - - if (percentComplete != null) { - bodyBuilder.percentComplete(percentComplete); - } - - if (timeRemaining != null) { - bodyBuilder.timeRemaining(timeRemaining); - } - - if (waypointsRemaining != null && !waypointsRemaining.isEmpty()) { - bodyBuilder.waypointsRemaining(waypointsRemaining); - } - - return new TaskFeedbackMessage(header, bodyBuilder.build()); - } - - private void validatePercentComplete(Double percentComplete) { - if (percentComplete == null) { - return; - } - - if (percentComplete < 0 || percentComplete > 100) { - throw new IllegalArgumentException("percent_complete must be between 0 and 100"); - } - } - - private void validateFeedbackState(TaskState taskState) { - if (taskState.isTerminal()) { - throw new IllegalArgumentException("Terminal task states must use TASK_RESULT: " + taskState); - } - } -} \ No newline at end of file diff --git a/src/main/java/io/mapsmessaging/state/stanag/messages/task/result/ResultReason.java b/src/main/java/io/mapsmessaging/state/stanag/messages/task/result/ResultReason.java deleted file mode 100644 index 53e625ef5..000000000 --- a/src/main/java/io/mapsmessaging/state/stanag/messages/task/result/ResultReason.java +++ /dev/null @@ -1,15 +0,0 @@ -package io.mapsmessaging.state.stanag.messages.task.result; - -public enum ResultReason { - SAFETY, - AUTHORITY, - ENVIRONMENT, - ENDURANCE, - LOGIC, - DYNAMICS, - HARDWARE, - COMMUNICATION, - TEMPORAL, - GEOSPATIAL, - CAPABILITY -} \ No newline at end of file diff --git a/src/main/java/io/mapsmessaging/state/stanag/messages/task/result/ResultReasonBuilder.java b/src/main/java/io/mapsmessaging/state/stanag/messages/task/result/ResultReasonBuilder.java deleted file mode 100644 index 210b10a53..000000000 --- a/src/main/java/io/mapsmessaging/state/stanag/messages/task/result/ResultReasonBuilder.java +++ /dev/null @@ -1,17 +0,0 @@ -package io.mapsmessaging.state.stanag.messages.task.result; - -import io.mapsmessaging.state.stanag.messages.FlexibleEnumeration; - -public class ResultReasonBuilder { - - public FlexibleEnumeration build(ResultReason resultReason, String text) { - if (resultReason == null) { - return null; - } - - if (text != null) { - return new FlexibleEnumeration(resultReason.name()+" "+text); - } - return new FlexibleEnumeration(resultReason.name()); - } -} \ No newline at end of file diff --git a/src/main/java/io/mapsmessaging/state/stanag/messages/task/result/TaskResultBody.java b/src/main/java/io/mapsmessaging/state/stanag/messages/task/result/TaskResultBody.java deleted file mode 100644 index c51ca0381..000000000 --- a/src/main/java/io/mapsmessaging/state/stanag/messages/task/result/TaskResultBody.java +++ /dev/null @@ -1,26 +0,0 @@ -package io.mapsmessaging.state.stanag.messages.task.result; - -import com.google.gson.annotations.SerializedName; -import io.mapsmessaging.state.stanag.messages.FlexibleEnumeration; -import io.mapsmessaging.state.stanag.messages.TaskState; -import lombok.Builder; -import lombok.Getter; - -import java.time.Instant; -import java.util.UUID; - -@Getter -@Builder -public class TaskResultBody { - - private final UUID identifier; - - private final UUID node; - - private final TaskState state; - - private final Instant timestamp; - - @SerializedName("result_reason") - private final FlexibleEnumeration resultReason; -} \ No newline at end of file diff --git a/src/main/java/io/mapsmessaging/state/stanag/messages/task/result/TaskResultMessage.java b/src/main/java/io/mapsmessaging/state/stanag/messages/task/result/TaskResultMessage.java deleted file mode 100644 index 444c86428..000000000 --- a/src/main/java/io/mapsmessaging/state/stanag/messages/task/result/TaskResultMessage.java +++ /dev/null @@ -1,14 +0,0 @@ -package io.mapsmessaging.state.stanag.messages.task.result; - -import io.mapsmessaging.state.stanag.messages.core.MessageHeader; -import lombok.AllArgsConstructor; -import lombok.Getter; - -@Getter -@AllArgsConstructor -public class TaskResultMessage { - - private final MessageHeader header; - - private final TaskResultBody body; -} \ No newline at end of file diff --git a/src/main/java/io/mapsmessaging/state/stanag/messages/task/result/TaskResultMessageBuilder.java b/src/main/java/io/mapsmessaging/state/stanag/messages/task/result/TaskResultMessageBuilder.java deleted file mode 100644 index 4af6a9986..000000000 --- a/src/main/java/io/mapsmessaging/state/stanag/messages/task/result/TaskResultMessageBuilder.java +++ /dev/null @@ -1,68 +0,0 @@ -package io.mapsmessaging.state.stanag.messages.task.result; - -import io.mapsmessaging.MessageDaemon; -import io.mapsmessaging.state.stanag.messages.TaskState; -import io.mapsmessaging.state.stanag.messages.TaskStatusContext; -import io.mapsmessaging.state.stanag.messages.core.MessageHeader; -import io.mapsmessaging.state.stanag.messages.core.MessageHeaderBuilder; -import io.mapsmessaging.state.stanag.messages.core.MessageType; -import lombok.RequiredArgsConstructor; - -import java.time.Instant; -import java.util.Objects; - -@RequiredArgsConstructor -public class TaskResultMessageBuilder { - - private final MessageHeaderBuilder headerBuilder; - - private final ResultReasonBuilder resultReasonBuilder; - - public TaskResultMessage buildSucceeded(TaskStatusContext context) { - return build(context, TaskState.SUCCEEDED, null, null); - } - - public TaskResultMessage buildRejected(TaskStatusContext context, ResultReason resultReason, String reasonText) { - return build(context, TaskState.REJECTED, resultReason, reasonText); - } - - public TaskResultMessage buildAborted(TaskStatusContext context, ResultReason resultReason,String reasonText) { - return build(context, TaskState.ABORTED, resultReason, reasonText); - } - - public TaskResultMessage buildLost(TaskStatusContext context, ResultReason resultReason,String reasonText) { - return build(context, TaskState.LOST, resultReason, reasonText); - } - - public TaskResultMessage buildRecalled(TaskStatusContext context, ResultReason resultReason,String reasonText) { - return build(context, TaskState.RECALLED, resultReason, reasonText); - } - - public TaskResultMessage buildPreempted(TaskStatusContext context, ResultReason resultReason,String reasonText) { - return build(context, TaskState.PREEMPTED, resultReason, reasonText); - } - - public TaskResultMessage build(TaskStatusContext context, TaskState taskState, ResultReason resultReason, String reasonText) { - Objects.requireNonNull(context, "context cannot be null"); - Objects.requireNonNull(taskState, "taskState cannot be null"); - - validateResultState(taskState); - - MessageHeader header = headerBuilder.build(MessageType.TASK_RESULT, context.nodeIdentifier(), Instant.now()); - TaskResultBody body = TaskResultBody.builder() - .identifier(context.taskIdentifier()) - .node(context.nodeIdentifier()) - .state(taskState) - .timestamp(Instant.now()) - .resultReason(resultReasonBuilder.build(resultReason, reasonText)) - .build(); - - return new TaskResultMessage(header, body); - } - - private void validateResultState(TaskState taskState) { - if (!taskState.isTerminal()) { - throw new IllegalArgumentException("Non-terminal task states must use TASK_FEEDBACK: " + taskState); - } - } -} \ No newline at end of file diff --git a/src/main/java/io/mapsmessaging/state/stanag/tasks/RepositionTaskHandler.java b/src/main/java/io/mapsmessaging/state/stanag/tasks/RepositionTaskHandler.java deleted file mode 100644 index eec832cb8..000000000 --- a/src/main/java/io/mapsmessaging/state/stanag/tasks/RepositionTaskHandler.java +++ /dev/null @@ -1,104 +0,0 @@ -/* - * - * Copyright [ 2020 - 2024 ] Matthew Buckton - * Copyright [ 2024 - 2026 ] MapsMessaging B.V. - * - * Licensed under the Apache License, Version 2.0 with the Commons Clause - * (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 - * https://commonsclause.com/ - * - * 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 io.mapsmessaging.state.stanag.tasks; - -import io.mapsmessaging.state.drone.drone.DroneTwin; -import io.mapsmessaging.state.drone.model.GeoPosition; -import io.mapsmessaging.state.mavlink.messages.MavlinkCommandInt; -import io.mapsmessaging.state.stanag.StanagSession; -import io.mapsmessaging.state.stanag.TaskAdminCommand; -import io.mapsmessaging.state.stanag.audit.AuditEvent; -import io.mapsmessaging.state.stanag.tasks.monitor.RepositionTaskMonitor; -import io.mapsmessaging.state.stanag.tasks.monitor.TaskMonitor; -import io.mapsmessaging.state.util.GeoUtils; - -import java.time.Duration; -import java.util.UUID; - -public class RepositionTaskHandler extends TaskHandler { - - @Override - public TaskMonitor handle(DroneTwin droneTwin, TaskAdminCommand command, StanagSession protocol,String template, int taskSequence) { - GeoPosition newPosition = command.getPosition(); - float yaw = GeoUtils.bearingDegrees(droneTwin.getGeoPosition(), newPosition); - MavlinkCommandInt mavlinkRequest = MavlinkCommandInt.reposition(droneTwin.getSystemId(), droneTwin.getComponentId(), newPosition, yaw, taskSequence); - AuditEvent auditEvent = buildAuditEvent(droneTwin, command, mavlinkRequest, taskSequence, newPosition, yaw); - auditAndDispatch(droneTwin, protocol, mavlinkRequest, auditEvent); - return new RepositionTaskMonitor(command.getTaskId(), droneTwin, template, taskSequence, Duration.ofMinutes(60), Duration.ofSeconds(15), command.getPosition(), auditEvent); - } - - @Override - public String getTaskType() { - return "REPOSITION"; - } - - private AuditEvent buildAuditEvent( - DroneTwin droneTwin, - TaskAdminCommand command, - MavlinkCommandInt mavlinkRequest, - int taskSequence, - GeoPosition newPosition, - float yaw - ) { - String commandIdentifier = command.getIdentifier(); - String droneIdentifier = droneTwin.getUuid().toString(); - - AuditEvent auditEvent = AuditEvent.builder() - .auditId(UUID.randomUUID().toString()) - .correlationId(commandIdentifier) - .actor("stanag") - .actorType("system") - .source("stanag") - .destination("drone") - .subject(droneIdentifier) - .taskId(commandIdentifier) - .commandId(commandIdentifier) - .droneId(droneIdentifier) - .stanagTaskType(getTaskType()) - .droneCommandType(mavlinkRequest.getMessageType()) - .protocol("MAVLink") - .targetSystem(String.valueOf(droneTwin.getSystemId())) - .targetComponent(String.valueOf(droneTwin.getComponentId())) - .build(); - - auditEvent.addAttribute("taskSequence", String.valueOf(taskSequence)); - auditEvent.addAttribute("mavlinkCommand", String.valueOf(mavlinkRequest.getCommand())); - - if (newPosition.getLatitude() != null) { - auditEvent.addAttribute("latitude", String.valueOf(newPosition.getLatitude())); - } - - if (newPosition.getLongitude() != null) { - auditEvent.addAttribute("longitude", String.valueOf(newPosition.getLongitude())); - } - - if (newPosition.getAltitudeMslMeters() != null) { - auditEvent.addAttribute("altitudeMslMeters", String.valueOf(newPosition.getAltitudeMslMeters())); - } - - if (newPosition.getAltitudeAglMeters() != null) { - auditEvent.addAttribute("altitudeAglMeters", String.valueOf(newPosition.getAltitudeAglMeters())); - } - - auditEvent.addAttribute("yaw", String.valueOf(yaw)); - - return auditEvent; - } -} \ No newline at end of file diff --git a/src/main/java/io/mapsmessaging/state/stanag/tasks/TaskDispatchResult.java b/src/main/java/io/mapsmessaging/state/stanag/tasks/TaskDispatchResult.java deleted file mode 100644 index ab807b708..000000000 --- a/src/main/java/io/mapsmessaging/state/stanag/tasks/TaskDispatchResult.java +++ /dev/null @@ -1,53 +0,0 @@ -/* - * - * Copyright [ 2020 - 2024 ] Matthew Buckton - * Copyright [ 2024 - 2026 ] MapsMessaging B.V. - * - * Licensed under the Apache License, Version 2.0 with the Commons Clause - * (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 - * https://commonsclause.com/ - * - * 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 io.mapsmessaging.state.stanag.tasks; - -import io.mapsmessaging.state.drone.drone.DroneTwin; -import io.mapsmessaging.state.stanag.messages.task.result.ResultReason; -import io.mapsmessaging.state.stanag.tasks.monitor.TaskMonitor; -import lombok.Getter; - -@Getter -public class TaskDispatchResult { - - private final DroneTwin droneTwin; - private final TaskMonitor taskMonitor; - private final ResultReason rejectReason; - private final String rejectText; - - private TaskDispatchResult(DroneTwin droneTwin, TaskMonitor taskMonitor, ResultReason rejectReason, String rejectText) { - this.droneTwin = droneTwin; - this.taskMonitor = taskMonitor; - this.rejectReason = rejectReason; - this.rejectText = rejectText; - } - - public static TaskDispatchResult accepted(DroneTwin droneTwin, TaskMonitor taskMonitor) { - return new TaskDispatchResult(droneTwin, taskMonitor, null, null); - } - - public static TaskDispatchResult rejected(DroneTwin droneTwin, ResultReason rejectReason, String rejectText) { - return new TaskDispatchResult(droneTwin, null, rejectReason, rejectText); - } - - public boolean isAccepted() { - return rejectReason == null; - } -} \ No newline at end of file diff --git a/src/main/java/io/mapsmessaging/state/stanag/tasks/TaskDispatcher.java b/src/main/java/io/mapsmessaging/state/stanag/tasks/TaskDispatcher.java deleted file mode 100644 index 193712443..000000000 --- a/src/main/java/io/mapsmessaging/state/stanag/tasks/TaskDispatcher.java +++ /dev/null @@ -1,150 +0,0 @@ -/* - * - * Copyright [ 2020 - 2024 ] Matthew Buckton - * Copyright [ 2024 - 2026 ] MapsMessaging B.V. - * - * Licensed under the Apache License, Version 2.0 with the Commons Clause - * (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 - * https://commonsclause.com/ - * - * 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 io.mapsmessaging.state.stanag.tasks; - -import io.mapsmessaging.state.config.StanagConfig; -import io.mapsmessaging.state.config.capability.Authorities; -import io.mapsmessaging.state.config.capability.PlanTaskType; -import io.mapsmessaging.state.config.capability.TaskCapabilities; -import io.mapsmessaging.state.config.capability.TaskCapability; -import io.mapsmessaging.state.drone.core.EntityTwin; -import io.mapsmessaging.state.drone.drone.DroneTwin; -import io.mapsmessaging.state.stanag.StanagSession; -import io.mapsmessaging.state.stanag.TaskAdminCommand; -import io.mapsmessaging.state.stanag.messages.task.result.ResultReason; -import io.mapsmessaging.state.stanag.tasks.monitor.TaskMonitor; - -import java.util.Locale; -import java.util.Map; -import java.util.ServiceLoader; -import java.util.concurrent.ConcurrentHashMap; -import java.util.concurrent.atomic.AtomicInteger; - -public class TaskDispatcher { - - private final StanagSession protocol; - private final AtomicInteger taskSequence; - private final Map taskHandlers = new ConcurrentHashMap<>(); - private final StanagConfig stanagConfig; - - public TaskDispatcher(StanagSession protocol, StanagConfig stanagConfig, AtomicInteger taskSequence) { - this.protocol = protocol; - this.taskSequence = taskSequence; - this.stanagConfig = stanagConfig; - ServiceLoader.load(TaskHandler.class).forEach(this::registerTaskHandler); - } - - public TaskDispatchResult dispatch(EntityTwin twin, TaskAdminCommand command) { - if (!(twin instanceof DroneTwin droneTwin)) { - return TaskDispatchResult.rejected(null, ResultReason.CAPABILITY, "Matching twin is not a drone"); - } - - ResultReason authorisationRejectReason = getAuthorisationRejectReason(droneTwin, command); - if (authorisationRejectReason != null) { - return TaskDispatchResult.rejected(droneTwin, authorisationRejectReason, "Task is not authorised"); - } - - ResultReason validationRejectReason = getValidationRejectReason(droneTwin); - if (validationRejectReason != null) { - return TaskDispatchResult.rejected(droneTwin, validationRejectReason, "Drone is not armed"); - } - - TaskHandler taskHandler = taskHandlers.get(normaliseTaskType(command.getTaskType())); - if (taskHandler == null) { - return TaskDispatchResult.rejected(droneTwin, ResultReason.CAPABILITY, "No task handler for task type"); - } - - TaskMonitor taskMonitor = taskHandler.handle(droneTwin, command, protocol, stanagConfig.getTaskTopicTemplate(), taskSequence.getAndIncrement()); - Authorities authority = new Authorities(); - authority.setGuid(command.getAuthorityGuid()); - taskMonitor.setAuthority(authority); - taskMonitor.setAccepted(); - return TaskDispatchResult.accepted(droneTwin, taskMonitor); - } - - private void registerTaskHandler(TaskHandler taskHandler) { - String taskType = normaliseTaskType(taskHandler.getTaskType()); - TaskHandler previousTaskHandler = taskHandlers.putIfAbsent(taskType, taskHandler); - - if (previousTaskHandler != null) { - throw new IllegalStateException( - "Duplicate task handler for task type " - + taskType - + ": " - + previousTaskHandler.getClass().getName() - + " and " - + taskHandler.getClass().getName()); - } - } - - private ResultReason getAuthorisationRejectReason(DroneTwin droneTwin, TaskAdminCommand command) { - TaskCapabilities capabilities = droneTwin.getCapabilities(); - - if (capabilities == null || capabilities.getTasks() == null) { - return ResultReason.CAPABILITY; - } - - TaskCapability capability = - capabilities.getTasks().stream() - .filter(taskCapability -> matchesTaskType(taskCapability.getTaskType(), command.getTaskType())) - .findFirst() - .orElse(null); - - if (capability == null) { - return ResultReason.CAPABILITY; - } - - if (capability.getAuthorities() == null || capability.getAuthorities().length == 0) { - return null; - } - - for (Authorities authority : capability.getAuthorities()) { - if (authority.getGuid().equals(command.getAuthorityGuid())) { - return null; - } - } - - return ResultReason.AUTHORITY; - } - - private ResultReason getValidationRejectReason(DroneTwin droneTwin) { - if (!Boolean.TRUE.equals(droneTwin.getArmed())) { - return ResultReason.SAFETY; - } - - return null; - } - - private boolean matchesTaskType(PlanTaskType taskType, String taskTypeName) { - if (taskType == null) { - return false; - } - - return taskType.name().equals(taskTypeName) || taskType.toString().equals(taskTypeName); - } - - private String normaliseTaskType(String taskType) { - if (taskType == null) { - return ""; - } - - return taskType.toUpperCase(Locale.ROOT); - } -} \ No newline at end of file diff --git a/src/main/java/io/mapsmessaging/state/stanag/tasks/TaskHandler.java b/src/main/java/io/mapsmessaging/state/stanag/tasks/TaskHandler.java deleted file mode 100644 index 847442f08..000000000 --- a/src/main/java/io/mapsmessaging/state/stanag/tasks/TaskHandler.java +++ /dev/null @@ -1,78 +0,0 @@ -/* - * - * Copyright [ 2020 - 2024 ] Matthew Buckton - * Copyright [ 2024 - 2026 ] MapsMessaging B.V. - * - * Licensed under the Apache License, Version 2.0 with the Commons Clause - * (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 - * https://commonsclause.com/ - * - * 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 io.mapsmessaging.state.stanag.tasks; - -import io.mapsmessaging.api.MessageBuilder; -import io.mapsmessaging.api.features.QualityOfService; -import io.mapsmessaging.state.drone.drone.DroneTwin; -import io.mapsmessaging.state.mavlink.messages.MavlinkCommandInt; -import io.mapsmessaging.state.stanag.StanagSession; -import io.mapsmessaging.state.stanag.TaskAdminCommand; -import io.mapsmessaging.state.stanag.audit.AuditEvent; -import io.mapsmessaging.state.stanag.audit.Auditor; -import io.mapsmessaging.state.stanag.tasks.monitor.TaskMonitor; - -import java.io.IOException; -import java.nio.charset.StandardCharsets; - -public abstract class TaskHandler { - - public abstract TaskMonitor handle(DroneTwin droneTwin, TaskAdminCommand command, StanagSession protocol, String template, int taskSequence); - - public abstract String getTaskType(); - - protected void auditAndDispatch(DroneTwin droneTwin, StanagSession protocol, MavlinkCommandInt mavlinkRequest, AuditEvent auditEvent) { - Auditor auditor = protocol.getAuditor(); - if (auditor != null) { - auditTranslated(auditor, auditEvent); - } - sendMavlinkRequest(droneTwin, mavlinkRequest, protocol); - if (auditor != null) { - auditDispatched(auditor, auditEvent); - } - } - - protected void sendMavlinkRequest(DroneTwin droneTwin, MavlinkCommandInt mavlinkRequest, StanagSession protocol) { - MessageBuilder messageBuilder = new MessageBuilder(); - - messageBuilder - .setOpaqueData(mavlinkRequest.toMavlinkJsonObject(255, 0).toString().getBytes(StandardCharsets.UTF_8)) - .setQoS(QualityOfService.AT_MOST_ONCE) - .setCorrelationData(droneTwin.getUniqueOutboundIdentifier()); - - protocol.respond(droneTwin.getResponseTopicName(), messageBuilder.build()); - } - - protected void auditTranslated(Auditor auditor, AuditEvent auditEvent) { - try { - auditor.auditStanagCommandTranslated(auditEvent); - } catch (IOException exception) { - throw new IllegalStateException("Unable to audit STANAG command translation", exception); - } - } - - protected void auditDispatched(Auditor auditor, AuditEvent auditEvent) { - try { - auditor.auditDroneCommandDispatched(auditEvent); - } catch (IOException exception) { - throw new IllegalStateException("Unable to audit MAVLink command dispatch", exception); - } - } -} \ No newline at end of file diff --git a/src/main/java/io/mapsmessaging/state/stanag/tasks/monitor/CompletedTaskMonitor.java b/src/main/java/io/mapsmessaging/state/stanag/tasks/monitor/CompletedTaskMonitor.java deleted file mode 100644 index 9a2e317ef..000000000 --- a/src/main/java/io/mapsmessaging/state/stanag/tasks/monitor/CompletedTaskMonitor.java +++ /dev/null @@ -1,47 +0,0 @@ -/* - * - * Copyright [ 2020 - 2024 ] Matthew Buckton - * Copyright [ 2024 - 2026 ] MapsMessaging B.V. - * - * Licensed under the Apache License, Version 2.0 with the Commons Clause - * (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 - * https://commonsclause.com/ - * - * 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 io.mapsmessaging.state.stanag.tasks.monitor; - -import io.mapsmessaging.state.drone.drone.DroneTwin; -import io.mapsmessaging.state.stanag.audit.AuditEvent; - -import java.time.Duration; -import java.util.UUID; - -public class CompletedTaskMonitor extends TaskMonitor { - - private final String taskType; - - public CompletedTaskMonitor(UUID taskId, DroneTwin droneTwin, String topicPath, int taskSequence, String taskType, AuditEvent auditEvent) { - super(taskId, droneTwin, topicPath, taskSequence, Duration.ofSeconds(1), Duration.ofSeconds(1), auditEvent); - this.taskType = taskType; - setComplete(); - } - - @Override - public String getTaskType() { - return taskType; - } - - @Override - protected void updateTask(DroneTwin droneTwin) { - setComplete(); - } -} \ No newline at end of file diff --git a/src/main/java/io/mapsmessaging/state/stanag/tasks/monitor/RepositionTaskMonitor.java b/src/main/java/io/mapsmessaging/state/stanag/tasks/monitor/RepositionTaskMonitor.java deleted file mode 100644 index ab129b988..000000000 --- a/src/main/java/io/mapsmessaging/state/stanag/tasks/monitor/RepositionTaskMonitor.java +++ /dev/null @@ -1,194 +0,0 @@ -/* - * - * Copyright [ 2020 - 2024 ] Matthew Buckton - * Copyright [ 2024 - 2026 ] MapsMessaging B.V. - * - * Licensed under the Apache License, Version 2.0 with the Commons Clause - * (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 - * https://commonsclause.com/ - * - * 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 io.mapsmessaging.state.stanag.tasks.monitor; - -import io.mapsmessaging.state.drone.drone.DroneTwin; -import io.mapsmessaging.state.drone.model.GeoPosition; -import io.mapsmessaging.state.stanag.audit.AuditEvent; -import io.mapsmessaging.state.stanag.messages.task.feedback.TaskFeedbackMessage; -import io.mapsmessaging.state.stanag.messages.task.feedback.TaskFeedbackMessageBuilder; -import io.mapsmessaging.state.util.GeoUtils; -import lombok.Getter; - -import java.time.Duration; -import java.time.Instant; -import java.util.UUID; - -@Getter -public class RepositionTaskMonitor extends TaskMonitor { - - public static final String TASK_TYPE = "REPOSITION"; - - private static final double COMPLETE_DISTANCE_METERS = 10.0d; - private static final double MINIMUM_CLOSING_SPEED_METERS_PER_SECOND = 0.1d; - private static final double MINIMUM_PROGRESS_PERCENTAGE_FOR_ETA = 1.0d; - private static final long MINIMUM_ELAPSED_SECONDS_FOR_ETA = 5L; - - private final GeoPosition targetPosition; - - private GeoPosition startPosition; - private Instant startTimestamp; - - private double originalDistanceMeters; - private double currentDistanceMeters; - private double averageClosingSpeedMetersPerSecond; - private double progressPercentage; - private long estimatedSecondsToTarget; - - public RepositionTaskMonitor( - UUID taskId, - DroneTwin droneTwin, - String topicPath, - int taskSequence, - Duration timeout, - Duration feedbackInterval, - GeoPosition targetPosition, - AuditEvent auditEvent) { - super(taskId, droneTwin, topicPath, taskSequence, timeout, feedbackInterval, auditEvent); - this.targetPosition = targetPosition; - this.originalDistanceMeters = Double.MAX_VALUE; - this.currentDistanceMeters = Double.MAX_VALUE; - this.averageClosingSpeedMetersPerSecond = 0.0d; - this.progressPercentage = 0.0d; - this.estimatedSecondsToTarget = -1L; - } - - @Override - public String getTaskType() { - return TASK_TYPE; - } - - @Override - protected void updateTask(DroneTwin droneTwin) { - if (droneTwin == null || !getDroneUUID().equals(droneTwin.getUuid())) { - return; - } - - GeoPosition currentPosition = droneTwin.getGeoPosition(); - if (!hasUsablePosition(currentPosition) || !hasUsablePosition(targetPosition)) { - return; - } - - Instant currentTimestamp = Instant.now(); - - if (startPosition == null) { - initialisePosition(currentPosition, currentTimestamp); - setInProgress(); - return; - } - - updatePosition(currentPosition, currentTimestamp); - - if (currentDistanceMeters <= COMPLETE_DISTANCE_METERS) { - progressPercentage = 100.0d; - estimatedSecondsToTarget = 0L; - setComplete(); - return; - } - - setInProgress(); - } - - public TaskFeedbackMessage buildFeedback(TaskFeedbackMessageBuilder feedbackMessageBuilder) { - TaskFeedbackMessage feedbackMessage =super.buildFeedback(feedbackMessageBuilder); - feedbackMessage.getBody().setPercentComplete(getProgressPercentage()); - return feedbackMessage; - } - - private void initialisePosition(GeoPosition currentPosition, Instant currentTimestamp) { - startPosition = currentPosition; - startTimestamp = currentTimestamp; - - originalDistanceMeters = GeoUtils.distanceMeters(currentPosition, targetPosition); - currentDistanceMeters = originalDistanceMeters; - averageClosingSpeedMetersPerSecond = 0.0d; - progressPercentage = calculateProgressPercentage(); - estimatedSecondsToTarget = -1L; - } - - private void updatePosition(GeoPosition currentPosition, Instant currentTimestamp) { - currentDistanceMeters = GeoUtils.distanceMeters(currentPosition, targetPosition); - updateProgress(); - updateAverageClosingSpeed(currentTimestamp); - updateEstimatedSecondsToTarget(currentTimestamp); - } - - private void updateProgress() { - progressPercentage = calculateProgressPercentage(); - } - - private void updateAverageClosingSpeed(Instant currentTimestamp) { - long elapsedSeconds = Duration.between(startTimestamp, currentTimestamp).toSeconds(); - - if (elapsedSeconds <= 0L) { - averageClosingSpeedMetersPerSecond = 0.0d; - return; - } - - double closedDistanceMeters = originalDistanceMeters - currentDistanceMeters; - - if (closedDistanceMeters <= 0.0d) { - averageClosingSpeedMetersPerSecond = 0.0d; - return; - } - - averageClosingSpeedMetersPerSecond = closedDistanceMeters / elapsedSeconds; - } - - private double calculateProgressPercentage() { - if (originalDistanceMeters <= 0.0d) { - return 100.0d; - } - double progress = ((originalDistanceMeters - currentDistanceMeters) / originalDistanceMeters) * 100.0d; - return roundToOneDecimalPlace(Math.clamp(progress, 0.0d, 100.0d)); - } - - private void updateEstimatedSecondsToTarget(Instant currentTimestamp) { - long elapsedSeconds = Duration.between(startTimestamp, currentTimestamp).toSeconds(); - if (elapsedSeconds < MINIMUM_ELAPSED_SECONDS_FOR_ETA) { - estimatedSecondsToTarget = -1L; - return; - } - if (progressPercentage < MINIMUM_PROGRESS_PERCENTAGE_FOR_ETA) { - estimatedSecondsToTarget = -1L; - return; - } - if (averageClosingSpeedMetersPerSecond <= MINIMUM_CLOSING_SPEED_METERS_PER_SECOND) { - estimatedSecondsToTarget = -1L; - return; - } - estimatedSecondsToTarget = Math.round(currentDistanceMeters / averageClosingSpeedMetersPerSecond); - } - - private String buildTimeRemaining() { - long secondsToTarget = Math.max(0L, Math.round(estimatedSecondsToTarget)); - return Duration.ofSeconds(secondsToTarget).toString(); - } - - private boolean hasUsablePosition(GeoPosition position) { - return position != null - && position.getLatitude() != null - && position.getLongitude() != null; - } - - private double roundToOneDecimalPlace(double value) { - return Math.round(value * 10.0d) / 10.0d; - } -} \ No newline at end of file diff --git a/src/main/java/io/mapsmessaging/state/stanag/tasks/monitor/TaskMonitor.java b/src/main/java/io/mapsmessaging/state/stanag/tasks/monitor/TaskMonitor.java deleted file mode 100644 index 294fd6d7a..000000000 --- a/src/main/java/io/mapsmessaging/state/stanag/tasks/monitor/TaskMonitor.java +++ /dev/null @@ -1,203 +0,0 @@ -/* - * - * Copyright [ 2020 - 2024 ] Matthew Buckton - * Copyright [ 2024 - 2026 ] MapsMessaging B.V. - * - * Licensed under the Apache License, Version 2.0 with the Commons Clause - * (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 - * https://commonsclause.com/ - * - * 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 io.mapsmessaging.state.stanag.tasks.monitor; - -import io.mapsmessaging.state.config.capability.Authorities; -import io.mapsmessaging.state.drone.drone.DroneTwin; -import io.mapsmessaging.state.stanag.audit.AuditEvent; -import io.mapsmessaging.state.stanag.messages.*; -import io.mapsmessaging.state.stanag.messages.task.feedback.TaskFeedbackMessage; -import io.mapsmessaging.state.stanag.messages.task.feedback.TaskFeedbackMessageBuilder; -import io.mapsmessaging.state.stanag.messages.task.result.ResultReason; -import io.mapsmessaging.state.stanag.messages.task.result.TaskResultMessage; -import io.mapsmessaging.state.stanag.messages.task.result.TaskResultMessageBuilder; -import lombok.Getter; -import lombok.Setter; - -import java.time.Duration; -import java.time.Instant; -import java.util.UUID; - -import static io.mapsmessaging.state.stanag.messages.TaskState.LOST; - -@Getter -public abstract class TaskMonitor { - - private final UUID taskId; - private final UUID droneUUID; - private final int taskSequence; - private final Duration timeout; - private final Duration feedbackInterval; - private final Instant createdTimestamp; - private final AuditEvent auditEvent; - private final String topicPath; - - private TaskMonitorState state; - private Instant lastUpdatedTimestamp; - private Instant lastFeedbackTimestamp; - private boolean resultEmitted; - - - @Setter - private Authorities authority; - - protected TaskMonitor( - UUID taskId, - DroneTwin droneTwin, - String topicPath, - int taskSequence, - Duration timeout, - Duration feedbackInterval, - AuditEvent auditEvent) { - this.taskId = taskId; - this.droneUUID = droneTwin.getUuid(); - this.taskSequence = taskSequence; - this.timeout = timeout; - this.feedbackInterval = feedbackInterval; - this.auditEvent = auditEvent; - this.createdTimestamp = Instant.now(); - this.lastUpdatedTimestamp = createdTimestamp; - this.state = TaskMonitorState.ACCEPTED; - this.topicPath = topicPath; - } - - public abstract String getTaskType(); - - public final void update(DroneTwin droneTwin) { - if (isFinished()) { - return; - } - - Instant now = Instant.now(); - if (timeout != null && Duration.between(createdTimestamp, now).compareTo(timeout) > 0) { - setTimedOut(); - return; - } - - updateTask(droneTwin); - lastUpdatedTimestamp = now; - } - - protected abstract void updateTask(DroneTwin droneTwin); - - public boolean hasFeedback() { - if (isFinished()) { - return false; - } - - if (feedbackInterval == null) { - return false; - } - - if (lastFeedbackTimestamp == null) { - return true; - } - - return Duration.between(lastFeedbackTimestamp, Instant.now()).compareTo(feedbackInterval) >= 0; - } - - public TaskFeedbackMessage buildFeedback(TaskFeedbackMessageBuilder feedbackMessageBuilder) { - TaskFeedbackMessage feedbackMessage = feedbackMessageBuilder.build(buildStatusContext(), mapFeedbackState()); - markFeedbackEmitted(); - return feedbackMessage; - } - - public boolean hasResult() { - return isFinished() && !resultEmitted; - } - - public TaskResultMessage buildResult(TaskResultMessageBuilder resultMessageBuilder) { - TaskResultMessage resultMessage = resultMessageBuilder.build(buildStatusContext(), mapResultState(), buildResultReason(), null); - markResultEmitted(); - return resultMessage; - } - - public boolean isFinished() { - return state == TaskMonitorState.COMPLETE - || state == TaskMonitorState.FAILED - || state == TaskMonitorState.TIMEOUT - || state == TaskMonitorState.LOST; - } - - public void setAccepted() { - if (!isFinished()) { - state = TaskMonitorState.ACCEPTED; - } - } - - public void setInProgress() { - if (!isFinished()) { - state = TaskMonitorState.IN_PROGRESS; - } - } - - public void setComplete() { - if (!isFinished()) { - state = TaskMonitorState.COMPLETE; - } - } - - public void setFailed() { - if (!isFinished()) { - state = TaskMonitorState.FAILED; - } - } - - public void setTimedOut() { - if (!isFinished()) { - state = TaskMonitorState.TIMEOUT; - } - } - - public void setLost() { - if (!isFinished()) { - state = TaskMonitorState.LOST; - } - } - - protected ResultReason buildResultReason() { - return null; - } - - private TaskStatusContext buildStatusContext() { - return new TaskStatusContext(taskId, droneUUID, authority); - } - - private TaskState mapFeedbackState() { - return TaskState.ACTIVE; - } - - private TaskState mapResultState() { - return switch (state) { - case COMPLETE -> TaskState.SUCCEEDED; - case FAILED -> TaskState.ABORTED; - case TIMEOUT, LOST -> LOST; - case ACCEPTED, IN_PROGRESS -> TaskState.ACTIVE; - }; - } - - private void markFeedbackEmitted() { - lastFeedbackTimestamp = Instant.now(); - } - - private void markResultEmitted() { - resultEmitted = true; - } -} \ No newline at end of file diff --git a/src/main/java/io/mapsmessaging/state/stanag/tasks/monitor/TaskMonitorManager.java b/src/main/java/io/mapsmessaging/state/stanag/tasks/monitor/TaskMonitorManager.java deleted file mode 100644 index faaeec530..000000000 --- a/src/main/java/io/mapsmessaging/state/stanag/tasks/monitor/TaskMonitorManager.java +++ /dev/null @@ -1,168 +0,0 @@ -/* - * - * Copyright [ 2020 - 2024 ] Matthew Buckton - * Copyright [ 2024 - 2026 ] MapsMessaging B.V. - * - * Licensed under the Apache License, Version 2.0 with the Commons Clause - * (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 - * https://commonsclause.com/ - * - * 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 io.mapsmessaging.state.stanag.tasks.monitor; - -import io.mapsmessaging.state.drone.core.EntityTwin; -import io.mapsmessaging.state.drone.core.TwinObserver; -import io.mapsmessaging.state.drone.core.TwinUpdateContext; -import io.mapsmessaging.state.drone.drone.DroneTwin; -import io.mapsmessaging.state.stanag.messages.core.MessageHeaderBuilder; -import io.mapsmessaging.state.stanag.messages.task.feedback.TaskFeedbackMessage; -import io.mapsmessaging.state.stanag.messages.task.feedback.TaskFeedbackMessageBuilder; -import io.mapsmessaging.state.stanag.messages.task.result.ResultReasonBuilder; -import io.mapsmessaging.state.stanag.messages.task.result.TaskResultMessage; -import io.mapsmessaging.state.stanag.messages.task.result.TaskResultMessageBuilder; -import io.mapsmessaging.utilities.threads.SimpleTaskScheduler; -import lombok.Getter; - -import java.time.Clock; -import java.time.Duration; -import java.util.List; -import java.util.UUID; -import java.util.concurrent.CopyOnWriteArrayList; -import java.util.concurrent.TimeUnit; -import java.util.concurrent.atomic.AtomicBoolean; - -public class TaskMonitorManager implements AutoCloseable, TwinObserver { - - private final Duration scanInterval; - private final TaskStatusPublisher statusPublisher; - - private final TaskFeedbackMessageBuilder feedbackMessageBuilder; - private final TaskResultMessageBuilder resultMessageBuilder; - - @Getter - private final List taskMonitors = new CopyOnWriteArrayList<>(); - - private final AtomicBoolean running = new AtomicBoolean(false); - - public TaskMonitorManager(Duration scanInterval, TaskStatusPublisher statusPublisher) { - this.scanInterval = scanInterval; - this.statusPublisher = statusPublisher; - - Clock clock = Clock.systemUTC(); - MessageHeaderBuilder messageHeaderBuilder = new MessageHeaderBuilder(clock); - feedbackMessageBuilder = new TaskFeedbackMessageBuilder(messageHeaderBuilder); - ResultReasonBuilder resultReasonBuilder = new ResultReasonBuilder(); - resultMessageBuilder = new TaskResultMessageBuilder(messageHeaderBuilder, resultReasonBuilder); - } - - public void start() { - if (!running.compareAndSet(false, true)) { - return; - } - SimpleTaskScheduler.getInstance().scheduleAtFixedRate(this::scanTaskMonitors, scanInterval.toMillis(), scanInterval.toMillis(), TimeUnit.MILLISECONDS); - } - - public void add(TaskMonitor taskMonitor) { - if (taskMonitor == null) { - return; - } - taskMonitors.add(taskMonitor); - } - - public void update(DroneTwin droneTwin) { - if (droneTwin == null) { - return; - } - - for (TaskMonitor taskMonitor : taskMonitors) { - if (taskMonitor.getDroneUUID().equals(droneTwin.getUuid())) { - taskMonitor.update(droneTwin); - } - } - } - - public void scanTaskMonitors() { - if (!running.get()) { - return; - } - - for (TaskMonitor taskMonitor : taskMonitors) { - scanTaskMonitor(taskMonitor); - } - } - - @Override - public void close() { - running.set(false); - taskMonitors.clear(); - } - - private void scanTaskMonitor(TaskMonitor taskMonitor) { - if (taskMonitor.hasResult()) { - publishResult(taskMonitor); - taskMonitors.remove(taskMonitor); - return; - } - - if (taskMonitor.hasFeedback()) { - publishFeedback(taskMonitor); - } - } - - private void publishFeedback(TaskMonitor taskMonitor) { - TaskFeedbackMessage taskFeedbackMessage = taskMonitor.buildFeedback(feedbackMessageBuilder); - - statusPublisher.publishFeedback(taskMonitor, taskFeedbackMessage); - } - - private void publishResult(TaskMonitor taskMonitor) { - TaskResultMessage taskResultMessage = taskMonitor.buildResult(resultMessageBuilder); - - statusPublisher.publishResult(taskMonitor, taskResultMessage); - } - - - @Override - public void onTwinUpdated(String twinId, EntityTwin current, TwinUpdateContext context) { - if (!(current instanceof DroneTwin droneTwin)) { - return; - } - - update(droneTwin); - } - - @Override - public void onTwinRemoved(EntityTwin removed, TwinUpdateContext context) { - if (!(removed instanceof DroneTwin droneTwin)) { - return; - } - - cancelTaskMonitorsForDrone(droneTwin.getUuid()); - } - - private void cancelTaskMonitorsForDrone(UUID droneId) { - for (TaskMonitor taskMonitor : taskMonitors) { - if (!taskMonitor.getDroneUUID().equals(droneId)) { - continue; - } - - taskMonitor.setLost(); - - if (taskMonitor.hasResult()) { - publishResult(taskMonitor); - } - - taskMonitors.remove(taskMonitor); - } - } -} \ No newline at end of file diff --git a/src/main/java/io/mapsmessaging/state/stanag/tasks/monitor/TaskStatusPublisher.java b/src/main/java/io/mapsmessaging/state/stanag/tasks/monitor/TaskStatusPublisher.java deleted file mode 100644 index 8074b0862..000000000 --- a/src/main/java/io/mapsmessaging/state/stanag/tasks/monitor/TaskStatusPublisher.java +++ /dev/null @@ -1,30 +0,0 @@ -/* - * - * Copyright [ 2020 - 2024 ] Matthew Buckton - * Copyright [ 2024 - 2026 ] MapsMessaging B.V. - * - * Licensed under the Apache License, Version 2.0 with the Commons Clause - * (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 - * https://commonsclause.com/ - * - * 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 io.mapsmessaging.state.stanag.tasks.monitor; - -import io.mapsmessaging.state.stanag.messages.task.feedback.TaskFeedbackMessage; -import io.mapsmessaging.state.stanag.messages.task.result.TaskResultMessage; - -public interface TaskStatusPublisher { - - void publishFeedback(TaskMonitor taskMonitor, TaskFeedbackMessage taskFeedbackMessage); - - void publishResult(TaskMonitor taskMonitor, TaskResultMessage taskResultMessage); -} \ No newline at end of file diff --git a/src/main/resources/META-INF/services/io.mapsmessaging.state.stanag.tasks.TaskHandler b/src/main/resources/META-INF/services/io.mapsmessaging.state.stanag.tasks.TaskHandler deleted file mode 100644 index 47adc6214..000000000 --- a/src/main/resources/META-INF/services/io.mapsmessaging.state.stanag.tasks.TaskHandler +++ /dev/null @@ -1,20 +0,0 @@ -# -# -# Copyright [ 2020 - 2024 ] Matthew Buckton -# Copyright [ 2024 - 2026 ] MapsMessaging B.V. -# -# Licensed under the Apache License, Version 2.0 with the Commons Clause -# (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 -# https://commonsclause.com/ -# -# 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. -# - -io.mapsmessaging.state.stanag.tasks.RepositionTaskHandler \ No newline at end of file diff --git a/src/test/java/io/mapsmessaging/state/auditor/AuditorFactoryTest.java b/src/test/java/io/mapsmessaging/state/auditor/AuditorFactoryTest.java new file mode 100644 index 000000000..bffe66506 --- /dev/null +++ b/src/test/java/io/mapsmessaging/state/auditor/AuditorFactoryTest.java @@ -0,0 +1,49 @@ +/* + * + * Copyright [ 2020 - 2024 ] Matthew Buckton + * Copyright [ 2024 - 2026 ] MapsMessaging B.V. + * + * Licensed under the Apache License, Version 2.0 with the Commons Clause + * (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 + * https://commonsclause.com/ + * + * 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 io.mapsmessaging.state.auditor; + +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertSame; + +import io.mapsmessaging.state.drone.core.TwinManager; +import java.nio.file.Path; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.io.TempDir; + +class AuditorFactoryTest { + + @TempDir + private Path tempDir; + + @Test + void build_returnsGenericAuditContextThatTwinManagerCanExpose() throws Exception { + AuditorFactory factory = new AuditorFactory(); + + try (AuditorFactory.AuditorInstance auditorInstance = factory.build(tempDir)) { + StateAuditContext auditContext = auditorInstance.getAuditContext(); + TwinManager twinManager = new TwinManager(true, 10000L, 5000L, 120000L, auditContext); + + assertNotNull(auditContext); + assertNotNull(auditContext.auditLogger()); + assertNotNull(auditContext.auditPayloadStore()); + assertSame(auditContext, twinManager.getAuditContext()); + } + } +} diff --git a/src/test/java/io/mapsmessaging/state/config/TwinManagerConfigTest.java b/src/test/java/io/mapsmessaging/state/config/TwinManagerConfigTest.java new file mode 100644 index 000000000..e71a82457 --- /dev/null +++ b/src/test/java/io/mapsmessaging/state/config/TwinManagerConfigTest.java @@ -0,0 +1,139 @@ +/* + * + * Copyright [ 2020 - 2024 ] Matthew Buckton + * Copyright [ 2024 - 2026 ] MapsMessaging B.V. + * + * Licensed under the Apache License, Version 2.0 with the Commons Clause + * (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 + * https://commonsclause.com/ + * + * 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 io.mapsmessaging.state.config; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertInstanceOf; +import static org.junit.jupiter.api.Assertions.assertNull; +import static org.junit.jupiter.api.Assertions.assertSame; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import io.mapsmessaging.configuration.ConfigurationProperties; +import io.mapsmessaging.state.config.n2k.N2KTwinConfig; +import java.lang.reflect.Constructor; +import org.junit.jupiter.api.Test; + +class TwinManagerConfigTest { + + @Test + void dto_adapterConfig_defaultsEmptyAndAcceptsMultipleAdapterKeys() { + TwinManagerConfigDTO config = new TwinManagerConfigDTO(); + ConfigurationProperties firstAdapter = adapterProperties("first/topic", 11); + ConfigurationProperties secondAdapter = adapterProperties("second/topic", 22); + + config.getAdapterConfig().put("alphaAdapter", firstAdapter); + config.getAdapterConfig().put("bravoAdapter", secondAdapter); + + assertEquals(2, config.getAdapterConfig().size()); + assertSame(firstAdapter, config.getAdapterConfig().get("alphaAdapter")); + assertSame(secondAdapter, config.getAdapterConfig().get("bravoAdapter")); + } + + @Test + void constructor_preservesArbitraryAdapterBlocks() throws ReflectiveOperationException { + ConfigurationProperties root = new ConfigurationProperties(); + root.put("heartbeatTimeoutMillis", 5000L); + root.put("stateAdapters", stateAdapterConfig( + "alphaAdapter", adapterProperties("alpha/topic", 11), + "bravoAdapter", adapterProperties("bravo/topic", 22) + )); + + TwinManagerConfig config = newTwinManagerConfig(root); + + assertEquals(2, config.getAdapterConfig().size()); + assertEquals("alpha/topic", config.getAdapterConfig().get("alphaAdapter").getProperty("topic")); + assertEquals(11, config.getAdapterConfig().get("alphaAdapter").getIntProperty("interval", 0)); + assertEquals("bravo/topic", config.getAdapterConfig().get("bravoAdapter").getProperty("topic")); + assertEquals(22, config.getAdapterConfig().get("bravoAdapter").getIntProperty("interval", 0)); + } + + @Test + void constructor_ignoresTopLevelAdapterLikeBlocks() throws ReflectiveOperationException { + ConfigurationProperties root = new ConfigurationProperties(); + root.put("alphaAdapter", adapterProperties("alpha/topic", 5)); + + TwinManagerConfig config = newTwinManagerConfig(root); + + assertTrue(config.getAdapterConfig().isEmpty()); + } + + @Test + void toConfigurationProperties_writesAllAdapterBlocksBack() throws ReflectiveOperationException { + ConfigurationProperties root = new ConfigurationProperties(); + ConfigurationProperties firstAdapter = adapterProperties("alpha/topic", 11); + ConfigurationProperties secondAdapter = adapterProperties("bravo/topic", 22); + root.put("stateAdapters", stateAdapterConfig( + "alphaAdapter", firstAdapter, + "bravoAdapter", secondAdapter + )); + + TwinManagerConfig config = newTwinManagerConfig(root); + ConfigurationProperties saved = config.toConfigurationProperties(); + ConfigurationProperties savedAdapters = assertInstanceOf(ConfigurationProperties.class, saved.get("stateAdapters")); + + assertSame(firstAdapter, savedAdapters.get("alphaAdapter")); + assertSame(secondAdapter, savedAdapters.get("bravoAdapter")); + assertNull(saved.get("alphaAdapter")); + assertNull(saved.get("bravoAdapter")); + } + + @Test + void update_copiesAdapterConfigMap() { + TwinManagerConfig config = new TwinManagerConfig(); + config.setN2KTwinConfig(new N2KTwinConfig()); + TwinManagerConfigDTO newConfig = new TwinManagerConfigDTO(); + newConfig.setN2KTwinConfig(new N2KTwinConfig()); + ConfigurationProperties adapter = adapterProperties("alpha/topic", 11); + newConfig.getAdapterConfig().put("alphaAdapter", adapter); + + boolean changed = config.update(newConfig); + + assertTrue(changed); + assertEquals(1, config.getAdapterConfig().size()); + assertSame(adapter, config.getAdapterConfig().get("alphaAdapter")); + } + + private ConfigurationProperties adapterProperties(String topic, int interval) { + ConfigurationProperties properties = new ConfigurationProperties(); + properties.put("enabled", true); + properties.put("topic", topic); + properties.put("interval", interval); + return properties; + } + + private ConfigurationProperties stateAdapterConfig( + String firstName, + ConfigurationProperties firstProperties, + String secondName, + ConfigurationProperties secondProperties + ) { + ConfigurationProperties stateAdapters = new ConfigurationProperties(); + stateAdapters.put(firstName, firstProperties); + stateAdapters.put(secondName, secondProperties); + return stateAdapters; + } + + private TwinManagerConfig newTwinManagerConfig(ConfigurationProperties properties) + throws ReflectiveOperationException { + Constructor constructor = TwinManagerConfig.class.getDeclaredConstructor(ConfigurationProperties.class); + constructor.setAccessible(true); + return constructor.newInstance(properties); + } +} diff --git a/src/test/java/io/mapsmessaging/state/stanag/tasks/messages/TaskStatusMessageBuilderTest.java b/src/test/java/io/mapsmessaging/state/stanag/tasks/messages/TaskStatusMessageBuilderTest.java deleted file mode 100644 index a3ee390af..000000000 --- a/src/test/java/io/mapsmessaging/state/stanag/tasks/messages/TaskStatusMessageBuilderTest.java +++ /dev/null @@ -1,172 +0,0 @@ -/* - * - * Copyright [ 2020 - 2024 ] Matthew Buckton - * Copyright [ 2024 - 2026 ] MapsMessaging B.V. - * - * Licensed under the Apache License, Version 2.0 with the Commons Clause - * (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 - * https://commonsclause.com/ - * - * 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 io.mapsmessaging.state.stanag.tasks.messages; - -import static org.junit.jupiter.api.Assertions.assertEquals; -import static org.junit.jupiter.api.Assertions.assertFalse; -import static org.junit.jupiter.api.Assertions.assertThrows; - -import com.google.gson.Gson; -import com.google.gson.JsonObject; -import java.time.Clock; -import java.time.Instant; -import java.time.ZoneOffset; -import java.util.UUID; - -import io.mapsmessaging.state.GsonStanagHelper; - -import io.mapsmessaging.state.stanag.messages.*; -import io.mapsmessaging.state.stanag.messages.core.MessageHeaderBuilder; -import io.mapsmessaging.state.stanag.messages.task.feedback.TaskFeedbackMessage; -import io.mapsmessaging.state.stanag.messages.task.feedback.TaskFeedbackMessageBuilder; -import io.mapsmessaging.state.stanag.messages.task.result.ResultReason; -import io.mapsmessaging.state.stanag.messages.task.result.ResultReasonBuilder; -import io.mapsmessaging.state.stanag.messages.task.result.TaskResultMessage; -import io.mapsmessaging.state.stanag.messages.task.result.TaskResultMessageBuilder; -import org.junit.jupiter.api.BeforeEach; -import org.junit.jupiter.api.Test; - -class TaskStatusMessageBuilderTest { - - private static final UUID TASK_IDENTIFIER = UUID.fromString("7f1f8d0b-91bc-4f89-ae4e-5cb2e6d9a6a8"); - private static final UUID NODE_IDENTIFIER = UUID.fromString("53a15a3e-b8da-5dbe-bb9d-fdf3a8ff8159"); - private static final Instant FIXED_TIME = Instant.parse("2026-06-05T06:16:05.021Z"); - - private Gson gson; - private TaskStatusContext context; - private TaskFeedbackMessageBuilder feedbackMessageBuilder; - private TaskResultMessageBuilder resultMessageBuilder; - - @BeforeEach - void setUp() { - gson = GsonStanagHelper.createGson(); - - Clock clock = Clock.fixed(FIXED_TIME, ZoneOffset.UTC); - MessageHeaderBuilder headerBuilder = new MessageHeaderBuilder(clock); - - feedbackMessageBuilder = new TaskFeedbackMessageBuilder(headerBuilder); - resultMessageBuilder = new TaskResultMessageBuilder(headerBuilder, new ResultReasonBuilder()); - - context = new TaskStatusContext(TASK_IDENTIFIER, NODE_IDENTIFIER, null); - } - - - @Test - void shouldBuildProgressFeedbackMessage() { - TaskFeedbackMessage message = feedbackMessageBuilder.buildProgress(context, 42.5d); - - JsonObject body = toJsonObject(message).getAsJsonObject("body"); - - assertEquals(TASK_IDENTIFIER, body.get("identifier").getAsString()); - assertEquals(NODE_IDENTIFIER, body.get("node").getAsString()); - assertEquals("TaskStateEnum_ACTIVE", body.get("state").getAsString()); - assertEquals(42.5d, body.get("percent_complete").getAsDouble(), 0.0001d); - - assertFalse(body.has("time_remaining")); - assertFalse(body.has("waypoints_remaining")); - } - - @Test - void shouldRejectProgressLessThanZero() { - assertThrows( - IllegalArgumentException.class, - () -> feedbackMessageBuilder.buildProgress(context, -0.1d)); - } - - @Test - void shouldRejectProgressGreaterThanOneHundred() { - assertThrows( - IllegalArgumentException.class, - () -> feedbackMessageBuilder.buildProgress(context, 100.1d)); - } - - @Test - void shouldNotAllowTerminalStateInFeedbackMessage() { - assertThrows( - IllegalArgumentException.class, - () -> feedbackMessageBuilder.build(context, TaskState.SUCCEEDED)); - } - - @Test - void shouldBuildSucceededResultMessage() { - TaskResultMessage message = resultMessageBuilder.buildSucceeded(context); - - JsonObject json = toJsonObject(message); - JsonObject header = json.getAsJsonObject("header"); - JsonObject body = json.getAsJsonObject("body"); - - assertEquals("MessageTypeEnum_TASK_RESULT", header.get("message_type").getAsString()); - assertEquals(NODE_IDENTIFIER, header.get("source").getAsString()); - assertEquals("2026-06-05T06:16:05.021Z", header.get("time_sent").getAsString()); - assertEquals("0.3.0", header.get("version").getAsString()); - - assertEquals(TASK_IDENTIFIER, body.get("identifier").getAsString()); - assertEquals(NODE_IDENTIFIER, body.get("node").getAsString()); - assertEquals("TaskStateEnum_SUCCEEDED", body.get("state").getAsString()); - - assertFalse(body.has("result_reason")); - } - - @Test - void shouldBuildRejectedResultMessageWithReason() { - TaskResultMessage message = resultMessageBuilder.buildRejected(context, ResultReason.SAFETY, "Safety reason"); - - JsonObject body = toJsonObject(message).getAsJsonObject("body"); - JsonObject resultReason = body.getAsJsonObject("result_reason"); - - assertEquals(TASK_IDENTIFIER, body.get("identifier").getAsString()); - assertEquals(NODE_IDENTIFIER, body.get("node").getAsString()); - assertEquals("TaskStateEnum_REJECTED", body.get("state").getAsString()); - assertEquals("ResultReasonEnum_SAFETY", resultReason.get("name").getAsString()); - } - - @Test - void shouldBuildAbortedResultMessageWithReason() { - TaskResultMessage message = resultMessageBuilder.buildAborted(context, ResultReason.HARDWARE, "Hardware reason"); - - JsonObject body = toJsonObject(message).getAsJsonObject("body"); - JsonObject resultReason = body.getAsJsonObject("result_reason"); - - assertEquals("TaskStateEnum_ABORTED", body.get("state").getAsString()); - assertEquals("ResultReasonEnum_HARDWARE", resultReason.get("name").getAsString()); - } - - @Test - void shouldBuildLostResultMessageWithReason() { - TaskResultMessage message = resultMessageBuilder.buildLost(context, ResultReason.COMMUNICATION, "Communication reason"); - - JsonObject body = toJsonObject(message).getAsJsonObject("body"); - JsonObject resultReason = body.getAsJsonObject("result_reason"); - - assertEquals("TaskStateEnum_LOST", body.get("state").getAsString()); - assertEquals("ResultReasonEnum_COMMUNICATION", resultReason.get("name").getAsString()); - } - - @Test - void shouldNotAllowNonTerminalStateInResultMessage() { - assertThrows( - IllegalArgumentException.class, - () -> resultMessageBuilder.build(context, TaskState.ACTIVE, null, null)); - } - - private JsonObject toJsonObject(Object value) { - return gson.toJsonTree(value).getAsJsonObject(); - } -} \ No newline at end of file