diff --git a/.github/workflows/cdb-base-mqtt.yml b/.github/workflows/cdb-base-mqtt.yml new file mode 100644 index 000000000..f05712045 --- /dev/null +++ b/.github/workflows/cdb-base-mqtt.yml @@ -0,0 +1,45 @@ +name: CDB Base MQTT + +on: + push: + paths: + - "tools/developer_tools/cdb-base-mqtt/**" + - ".github/workflows/cdb-base-mqtt.yml" + pull_request: + paths: + - "tools/developer_tools/cdb-base-mqtt/**" + - ".github/workflows/cdb-base-mqtt.yml" + workflow_dispatch: + +permissions: + contents: read + +jobs: + test: + runs-on: ubuntu-latest + defaults: + run: + working-directory: tools/developer_tools/cdb-base-mqtt + + steps: + - name: Check out repository + uses: actions/checkout@v4 + + - name: Set up Java 8 and Maven cache + uses: actions/setup-java@v4 + with: + distribution: temurin + java-version: "8" + cache: maven + cache-dependency-path: tools/developer_tools/cdb-base-mqtt/pom.xml + + - name: Run tests + run: mvn --batch-mode --no-transfer-progress verify + + - name: Publish test results + if: always() + uses: actions/upload-artifact@v4 + with: + name: cdb-base-mqtt-test-results + path: tools/developer_tools/cdb-base-mqtt/target/surefire-reports/ + if-no-files-found: warn diff --git a/Makefile b/Makefile index f2daa3b76..33d369d64 100644 --- a/Makefile +++ b/Makefile @@ -9,6 +9,7 @@ TOP = . SUBDIRS = src .PHONY: support support-mysql dev-config prepare-release release-python-client +.PHONY: mqtt-configuration configure-mqtt mqtt-configuration-dev configure-mqtt-dev .PHONY: db backup db-dev deploy-web-portal undeploy-web-portal deploy-web-service undeploy-web-service .PHONY: db-dev backup-dev deploy-web-portal-dev undeploy-web-portal-dev deploy-web-service-dev undeploy-web-service-dev @@ -28,6 +29,12 @@ release-python-client: configuration: $(TOP)/sbin/cdb_create_configuration.sh +mqtt-configuration: + $(TOP)/sbin/cdb_create_mqtt_configuration.sh cdb + +configure-mqtt: + $(TOP)/sbin/cdb_configure_mqtt_service.sh cdb + support: $(TOP)/sbin/cdb_install_support.sh @@ -79,6 +86,12 @@ undeploy-web-service: configuration-dev: $(TOP)/sbin/cdb_create_configuration.sh cdb_dev +mqtt-configuration-dev: + $(TOP)/sbin/cdb_create_mqtt_configuration.sh cdb_dev + +configure-mqtt-dev: + $(TOP)/sbin/cdb_configure_mqtt_service.sh cdb_dev + db-dev: $(TOP)/sbin/cdb_create_db.sh cdb_dev diff --git a/sbin/cdb_configure_mqtt_service.sh b/sbin/cdb_configure_mqtt_service.sh new file mode 100755 index 000000000..020298bd0 --- /dev/null +++ b/sbin/cdb_configure_mqtt_service.sh @@ -0,0 +1,170 @@ +#!/bin/bash + +# Copyright (c) UChicago Argonne, LLC. All rights reserved. +# See LICENSE file. + + +# +# Script used for configuring MQTT connector for CDB webapp +# Deploys MQTT Resource Adapter and creates connection pool/resource +# +# Usage: +# +# $0 [CDB_DB_NAME] [mqtt_config_file] +# +# If no config file is specified, defaults to $CDB_INSTALL_DIR/etc/.mqtt.conf +# +# Sample MQTT configuration file contents (mqtt.conf): +# MQTT_HOST=localhost # MQTT broker hostname (default: localhost) +# MQTT_PORT=1883 # MQTT broker port (default: 1883) +# MQTT_USERNAME=admin # MQTT username (optional) +# MQTT_PASSWORD=admin # MQTT password (optional) +# MQTT_CLEAN_SESSION=true # Clean session flag (optional) +# MQTT_QOS=1 # Quality of Service level (optional) +# MQTT_KEEP_ALIVE_INTERVAL=60 # Keep alive interval in seconds (optional) +# MQTT_CONNECTION_TIMEOUT=30 # Connection timeout in seconds (optional) +# MQTT_MAX_INFLIGHT=10 # Maximum number of messages in flight (optional) +# MQTT_AUTOMATIC_RECONNECT=true # Automatic reconnection on disconnect (optional) +# MQTT_FILE_PERSISTANCE=false # Enable file-based message persistence (optional) +# MQTT_PERSISTENCE_DIRECTORY=. # Directory for persistent message storage (optional) +# MQTT_TOPIC_FILTER # MQTT topic filter for subscriptions (optional) + +MY_DIR=`dirname $0` && cd $MY_DIR && MY_DIR=`pwd` +if [ -z "${CDB_ROOT_DIR}" ]; then + CDB_ROOT_DIR=$MY_DIR/.. +fi +CDB_ENV_FILE=${CDB_ROOT_DIR}/setup.sh +if [ ! -f ${CDB_ENV_FILE} ]; then + echo "Environment file ${CDB_ENV_FILE} does not exist." + exit 2 +fi +. ${CDB_ENV_FILE} > /dev/null + +# Constants +MQTT_POOL_NAME="cdb/MQTT/pool" +MQTT_RESOURCE_NAME="cdb/MQTT/resource" +MQTT_RAR_NAME="mqtt-rar-0.8.0" +MQTT_CONNECTOR_RAR_DEPLOYMENT_NAME="cdb-mqtt-rar-deployment" +MQTT_RAR_PATH=$CDB_ROOT_DIR/src/lib/${MQTT_RAR_NAME}.rar + +# Look for the deployment-specific MQTT configuration file +CDB_DB_NAME=${1:-${CDB_DB_NAME:-cdb}} +if [ ! -z "$2" ]; then + mqttConfigFile=$2 +else + mqttConfigFile=$CDB_INSTALL_DIR/etc/${CDB_DB_NAME}.mqtt.conf +fi + +if [ -f $mqttConfigFile ]; then + echo "Using MQTT config file: $mqttConfigFile" + . $mqttConfigFile +else + echo "Error: MQTT config file $mqttConfigFile not found." + echo "You can create one using cdb_create_mqtt_configuration.sh ${CDB_DB_NAME}" + exit 1 +fi + +CDB_HOST_ARCH=$(uname -sm | tr -s '[:upper:][:blank:]' '[:lower:][\-]') +GLASSFISH_DIR=$CDB_SUPPORT_DIR/payara/$CDB_HOST_ARCH + +ASADMIN_CMD=$GLASSFISH_DIR/bin/asadmin + +# MQTT Configuration defaults +MQTT_HOST=${MQTT_HOST:=localhost} +MQTT_PORT=${MQTT_PORT:=1883} + +# Build properties string for connection pool +PROPERTIES="serverURIs=tcp\\://${MQTT_HOST}\\:${MQTT_PORT}" + +if [ ! -z "$MQTT_USERNAME" ]; then + PROPERTIES="${PROPERTIES}:userName=${MQTT_USERNAME}" +fi + +if [ ! -z "$MQTT_PASSWORD" ]; then + PROPERTIES="${PROPERTIES}:password=${MQTT_PASSWORD}" +fi + +if [ ! -z "$MQTT_CLEAN_SESSION" ]; then + PROPERTIES="${PROPERTIES}:cleanSession=${MQTT_CLEAN_SESSION}" +fi + +if [ ! -z "$MQTT_KEEP_ALIVE_INTERVAL" ]; then + PROPERTIES="${PROPERTIES}:keepAliveInterval=${MQTT_KEEP_ALIVE_INTERVAL}" +fi + +if [ ! -z "$MQTT_CONNECTION_TIMEOUT" ]; then + PROPERTIES="${PROPERTIES}:connectionTimeout=${MQTT_CONNECTION_TIMEOUT}" +fi + +if [ ! -z "$MQTT_MAX_INFLIGHT" ]; then + PROPERTIES="${PROPERTIES}:maxInflight=${MQTT_MAX_INFLIGHT}" +fi + +if [ ! -z "$MQTT_AUTOMATIC_RECONNECT" ]; then + PROPERTIES="${PROPERTIES}:automaticReconnect=${MQTT_AUTOMATIC_RECONNECT}" +fi + +if [ ! -z "$MQTT_FILE_PERSISTANCE" ]; then + PROPERTIES="${PROPERTIES}:filePersistance=${MQTT_FILE_PERSISTANCE}" +fi + +if [ ! -z "$MQTT_PERSISTENCE_DIRECTORY" ]; then + PROPERTIES="${PROPERTIES}:persistenceDirectory=${MQTT_PERSISTENCE_DIRECTORY}" +fi + +if [ ! -z "$MQTT_QOS" ]; then + PROPERTIES="${PROPERTIES}:qos=${MQTT_QOS}" +fi + +if [ ! -z "$MQTT_TOPIC_FILTER" ]; then + PROPERTIES="${PROPERTIES}:topicFilter=${MQTT_TOPIC_FILTER}" +fi + +# Deploy MQTT RAR +echo "Deploying MQTT RAR" +if [ -f "$MQTT_RAR_PATH" ]; then + # Check if already deployed and undeploy if needed + $ASADMIN_CMD list-applications | grep -q ${MQTT_CONNECTOR_RAR_DEPLOYMENT_NAME} && { + echo "Undeploying existing MQTT RAR" + # Check if resource exists and delete it + $ASADMIN_CMD list-connector-resources | grep -q ${MQTT_RESOURCE_NAME} && { + echo "Deleting existing MQTT resource" + $ASADMIN_CMD delete-connector-resource ${MQTT_RESOURCE_NAME} || exit 1 + } + + # Check if connection pool exists and delete it + $ASADMIN_CMD list-connector-connection-pools | grep -q ${MQTT_POOL_NAME} && { + echo "Deleting existing MQTT connection pool" + $ASADMIN_CMD delete-connector-connection-pool ${MQTT_POOL_NAME} || exit 1 + } + + echo "Undeploying existing MQTT RAR" + $ASADMIN_CMD undeploy ${MQTT_CONNECTOR_RAR_DEPLOYMENT_NAME} || exit 1 + } + $ASADMIN_CMD deploy --name ${MQTT_CONNECTOR_RAR_DEPLOYMENT_NAME} $MQTT_RAR_PATH || exit 1 +else + echo "Warning: MQTT RAR file not found at $MQTT_RAR_PATH" + exit 1 +fi + +# Create MQTT connection pool +echo "Creating MQTT connection pool ${MQTT_POOL_NAME}" +$ASADMIN_CMD create-connector-connection-pool \ + --raname ${MQTT_CONNECTOR_RAR_DEPLOYMENT_NAME} \ + --connectiondefinition fish.payara.cloud.connectors.mqtt.api.MQTTConnectionFactory \ + --property "${PROPERTIES}" \ + ${MQTT_POOL_NAME} || exit 1 +# Create MQTT resource +echo "Creating MQTT resource ${MQTT_RESOURCE_NAME}" +$ASADMIN_CMD create-connector-resource \ + --poolname ${MQTT_POOL_NAME} \ + ${MQTT_RESOURCE_NAME} || exit 1 + +# The connector logs allocation failures with a full stack trace before returning null. +$ASADMIN_CMD set-log-levels fish.payara.cloud.connectors.mqtt.api.outbound.MQTTConnectionFactoryImpl=OFF || exit 1 + +# Test MQTT connection pool +echo "Testing MQTT connection pool" +$ASADMIN_CMD ping-connection-pool ${MQTT_POOL_NAME} || { echo "Warning: MQTT connection pool ping failed"; exit 1; } + +echo "Restart or redeploy CDB." \ No newline at end of file diff --git a/sbin/cdb_create_mqtt_configuration.sh b/sbin/cdb_create_mqtt_configuration.sh new file mode 100755 index 000000000..000b0f4fb --- /dev/null +++ b/sbin/cdb_create_mqtt_configuration.sh @@ -0,0 +1,135 @@ +#!/bin/bash + +# Script to create MQTT configuration file for CDB service +# Usage: $0 [CDB_DB_NAME] + +MY_DIR=`dirname $0` && cd $MY_DIR && MY_DIR=`pwd` +if [ -z "${CDB_ROOT_DIR}" ]; then + CDB_ROOT_DIR=$MY_DIR/.. +fi +CDB_ENV_FILE=${CDB_ROOT_DIR}/setup.sh +if [ ! -f ${CDB_ENV_FILE} ]; then + echo "Environment file ${CDB_ENV_FILE} does not exist." + exit 2 +fi +. ${CDB_ENV_FILE} > /dev/null + +# Default configuration file location +CDB_DB_NAME=${1:-${CDB_DB_NAME:-cdb}} +MQTT_CONFIG_FILE=${CDB_INSTALL_DIR}/etc/${CDB_DB_NAME}.mqtt.conf + +echo "===================================" +echo "MQTT Configuration Setup for CDB" +echo "===================================" +echo "" +echo "This script will help you create an MQTT configuration file." +echo "Configuration will be saved to: $MQTT_CONFIG_FILE" +echo "" + +# Check if config file already exists +if [ -f "$MQTT_CONFIG_FILE" ]; then + read -p "Configuration file already exists. Overwrite? (y/n): " -n 1 -r + echo + if [[ ! $REPLY =~ ^[Yy]$ ]]; then + echo "Exiting without changes." + exit 0 + fi +fi + +echo "" +echo "Configuration Details:" +echo "- cleanSession: Whether client and server should remember state across reconnects" +echo "- automaticReconnect: Whether client will automatically reconnect if connection is lost" +echo "- filePersistance: Whether to use file persistence for un-acknowledged messages" +echo "- persistenceDirectory: Directory to use for file persistence" +echo "- connectionTimeout: Connection timeout value in seconds" +echo "- maxInflight: Maximum messages that can be sent without acknowledgements" +echo "- keepAliveInterval: Keep alive interval in seconds" +echo "- userName/password: Authentication credentials" +# Disable MDB only variables. +# echo "- topicFilter: Topic Filter (For MDBs only)" +# echo "- qos: Quality of Service for the subscription (For MDBs only)" +echo "" + +# Ensure directory exists +mkdir -p $(dirname "$MQTT_CONFIG_FILE") + +# Prompt for configuration values +read -p "MQTT Host [localhost]: " MQTT_HOST +MQTT_HOST=${MQTT_HOST:-localhost} + +read -p "MQTT Port [1883]: " MQTT_PORT +MQTT_PORT=${MQTT_PORT:-1883} + +read -p "MQTT Username (leave empty for no auth): " MQTT_USERNAME + +if [ ! -z "$MQTT_USERNAME" ]; then + read -s -p "MQTT Password: " MQTT_PASSWORD + echo +fi + +read -p "Clean Session (true/false) [false]: " CLEAN_SESSION +CLEAN_SESSION=${CLEAN_SESSION:-false} + +read -p "Automatic Reconnect (true/false) [true]: " AUTOMATIC_RECONNECT +AUTOMATIC_RECONNECT=${AUTOMATIC_RECONNECT:-true} + +read -p "File Persistance (true/false) [false]: " FILE_PERSISTANCE +FILE_PERSISTANCE=${FILE_PERSISTANCE:-false} + +read -p "Persistence Directory [.]: " PERSISTENCE_DIRECTORY +PERSISTENCE_DIRECTORY=${PERSISTENCE_DIRECTORY:-.} + +read -p "Connection Timeout (seconds) [30]: " CONNECTION_TIMEOUT +CONNECTION_TIMEOUT=${CONNECTION_TIMEOUT:-30} + +read -p "Max Inflight [10]: " MAX_INFLIGHT +MAX_INFLIGHT=${MAX_INFLIGHT:-10} + +read -p "Keep Alive Interval (seconds) [60]: " KEEP_ALIVE_INTERVAL +KEEP_ALIVE_INTERVAL=${KEEP_ALIVE_INTERVAL:-60} + +# MDB only variables +# read -p "Topic Filter (leave empty if not using MDB): " TOPIC_FILTER +# read -p "QoS (0/1/2) [0]: " QOS +# QOS=${QOS:-0} + +# Write configuration file +cat > "$MQTT_CONFIG_FILE" << EOF +# MQTT Configuration for CDB Service +# Generated on $(date) + +MQTT_HOST=$MQTT_HOST +MQTT_PORT=$MQTT_PORT +EOF + +if [ ! -z "$MQTT_USERNAME" ]; then + echo "MQTT_USERNAME=$MQTT_USERNAME" >> "$MQTT_CONFIG_FILE" +fi + +if [ ! -z "$MQTT_PASSWORD" ]; then + echo "MQTT_PASSWORD=$MQTT_PASSWORD" >> "$MQTT_CONFIG_FILE" +fi + +cat >> "$MQTT_CONFIG_FILE" << EOF +MQTT_CLEAN_SESSION=$CLEAN_SESSION +MQTT_AUTOMATIC_RECONNECT=$AUTOMATIC_RECONNECT +MQTT_FILE_PERSISTANCE=$FILE_PERSISTANCE +MQTT_PERSISTENCE_DIRECTORY=$PERSISTENCE_DIRECTORY +MQTT_CONNECTION_TIMEOUT=$CONNECTION_TIMEOUT +MQTT_MAX_INFLIGHT=$MAX_INFLIGHT +MQTT_KEEP_ALIVE_INTERVAL=$KEEP_ALIVE_INTERVAL +EOF + +if [ ! -z "$TOPIC_FILTER" ]; then + echo "MQTT_TOPIC_FILTER=$TOPIC_FILTER" >> "$MQTT_CONFIG_FILE" +fi + +if [ ! -z "$QOS" ]; then + echo "MQTT_QOS=$QOS" >> "$MQTT_CONFIG_FILE" +fi + +echo "" +echo "Configuration file created successfully at: $MQTT_CONFIG_FILE" +echo "" +echo "You can now run cdb_configure_mqtt_service.sh ${CDB_DB_NAME} to apply this configuration." \ No newline at end of file diff --git a/src/java/CdbWebPortal/lib/cdb-base-mqtt-1.0.0.jar b/src/java/CdbWebPortal/lib/cdb-base-mqtt-1.0.0.jar new file mode 100644 index 000000000..04b3e761a Binary files /dev/null and b/src/java/CdbWebPortal/lib/cdb-base-mqtt-1.0.0.jar differ diff --git a/src/java/CdbWebPortal/lib/mqtt-jca-api-1.0.0.jar b/src/java/CdbWebPortal/lib/mqtt-jca-api-1.0.0.jar new file mode 100644 index 000000000..fa423699f Binary files /dev/null and b/src/java/CdbWebPortal/lib/mqtt-jca-api-1.0.0.jar differ diff --git a/src/java/CdbWebPortal/nbproject/build-impl.xml b/src/java/CdbWebPortal/nbproject/build-impl.xml index 2c394668c..5467c0d3e 100644 --- a/src/java/CdbWebPortal/nbproject/build-impl.xml +++ b/src/java/CdbWebPortal/nbproject/build-impl.xml @@ -1041,6 +1041,8 @@ exists or setup the property manually. For example like this: + + @@ -1104,6 +1106,8 @@ exists or setup the property manually. For example like this: + + diff --git a/src/java/CdbWebPortal/nbproject/genfiles.properties b/src/java/CdbWebPortal/nbproject/genfiles.properties index f3a7e4c63..c1e02b10e 100644 --- a/src/java/CdbWebPortal/nbproject/genfiles.properties +++ b/src/java/CdbWebPortal/nbproject/genfiles.properties @@ -1,10 +1,10 @@ -build.xml.data.CRC32=d5de4743 +build.xml.data.CRC32=9c7b4582 build.xml.script.CRC32=67492cbd build.xml.stylesheet.CRC32=1707db4f@1.94.0.1 # This file is used by a NetBeans-based IDE to track changes in generated files such as build-impl.xml. # Do not edit this file. You may delete it but then the IDE will never regenerate such files for you. -nbproject/build-impl.xml.data.CRC32=d5de4743 -nbproject/build-impl.xml.script.CRC32=8f60c160 +nbproject/build-impl.xml.data.CRC32=9c7b4582 +nbproject/build-impl.xml.script.CRC32=e1cf7847 nbproject/build-impl.xml.stylesheet.CRC32=334708a0@1.94.0.1 nbproject/rest-build.xml.data.CRC32=83e13ee9 nbproject/rest-build.xml.script.CRC32=0d1fb1b4 diff --git a/src/java/CdbWebPortal/nbproject/project.properties b/src/java/CdbWebPortal/nbproject/project.properties index 4206c7f64..34eea5b9f 100644 --- a/src/java/CdbWebPortal/nbproject/project.properties +++ b/src/java/CdbWebPortal/nbproject/project.properties @@ -42,6 +42,7 @@ file.reference.bcmail-jdk14-1.38.jar=lib/bcmail-jdk14-1.38.jar file.reference.bcprov-jdk14-1.38.jar=lib/bcprov-jdk14-1.38.jar file.reference.bctsp-jdk14-1.38.jar=lib/bctsp-jdk14-1.38.jar file.reference.classgraph-4.8.31.jar=lib/classgraph-4.8.31.jar +file.reference.cdb-base-mqtt-1.0.0.jar=lib/cdb-base-mqtt-1.0.0.jar file.reference.commons-lang3-3.11.jar=lib/commons-lang3-3.11.jar file.reference.commons-logging-1.2.jar=lib/commons-logging-1.2.jar file.reference.dm-api-3.3.1.jar=lib/dm-api-3.3.1.jar @@ -79,6 +80,7 @@ file.reference.libphonenumber-8.12.3.jar=lib/libphonenumber-8.12.3.jar file.reference.log4j-api-2.26.0.jar=lib/log4j-api-2.26.0.jar file.reference.log4j-core-2.26.0.jar=lib/log4j-core-2.26.0.jar file.reference.metadata-extractor-2.17.0.jar=lib/metadata-extractor-2.17.0.jar +file.reference.mqtt-jca-api-1.0.0.jar=lib/mqtt-jca-api-1.0.0.jar file.reference.omnifaces-3.10.1.jar-1=lib/omnifaces-3.10.1.jar file.reference.pdfbox-2.0.24.jar=lib/pdfbox-2.0.24.jar file.reference.poi-4.0.1.jar=lib/poi-4.0.1.jar @@ -169,7 +171,9 @@ javac.classpath=\ ${file.reference.flexmark-util-html-0.64.8.jar}:\ ${file.reference.flexmark-util-misc-0.64.8.jar}:\ ${file.reference.flexmark-util-sequence-0.64.8.jar}:\ - ${file.reference.flexmark-util-visitor-0.64.8.jar} + ${file.reference.flexmark-util-visitor-0.64.8.jar}:\ + ${file.reference.cdb-base-mqtt-1.0.0.jar}:\ + ${file.reference.mqtt-jca-api-1.0.0.jar} # Space-separated list of extra javac options javac.compilerargs= javac.debug=true diff --git a/src/java/CdbWebPortal/nbproject/project.xml b/src/java/CdbWebPortal/nbproject/project.xml index fe2ceeb99..553e82ac8 100644 --- a/src/java/CdbWebPortal/nbproject/project.xml +++ b/src/java/CdbWebPortal/nbproject/project.xml @@ -242,6 +242,14 @@ ${file.reference.flexmark-util-visitor-0.64.8.jar} WEB-INF/lib + + ${file.reference.cdb-base-mqtt-1.0.0.jar} + WEB-INF/lib + + + ${file.reference.mqtt-jca-api-1.0.0.jar} + WEB-INF/lib + diff --git a/src/java/CdbWebPortal/src/java/gov/anl/aps/cdb/mqtt/CdbEntityEvent.java b/src/java/CdbWebPortal/src/java/gov/anl/aps/cdb/mqtt/CdbEntityEvent.java new file mode 100644 index 000000000..f206c9582 --- /dev/null +++ b/src/java/CdbWebPortal/src/java/gov/anl/aps/cdb/mqtt/CdbEntityEvent.java @@ -0,0 +1,45 @@ +/* + * Copyright (c) UChicago Argonne, LLC. All rights reserved. + * See LICENSE file. + */ +package gov.anl.aps.cdb.mqtt; + +import gov.anl.aps.cdb.portal.model.db.entities.CdbEntity; +import gov.anl.aps.cdb.portal.model.db.entities.UserInfo; + +/** Scalar snapshot of a completed entity operation. */ +public class CdbEntityEvent extends CdbMqttEvent { + + private final CdbMqttOperation operation; + private final String entityId; + private final String entityType; + private final String entityDescription; + + public CdbEntityEvent(CdbMqttTopic topic, CdbEntity entity, UserInfo triggeredByUser) { + super(topic, triggeredByUser); + if (entity == null) { + throw new IllegalArgumentException("entity is required"); + } + this.operation = CdbMqttOperation.valueOf(topic.name()); + Object id = entity.getId(); + this.entityId = id == null ? null : id.toString(); + this.entityType = entity.getClass().getSimpleName(); + this.entityDescription = entity.getSystemLogString(); + } + + public CdbMqttOperation getOperation() { + return operation; + } + + public String getEntityId() { + return entityId; + } + + public String getEntityType() { + return entityType; + } + + public String getEntityDescription() { + return entityDescription; + } +} diff --git a/src/java/CdbWebPortal/src/java/gov/anl/aps/cdb/mqtt/CdbEntityTypeSnapshot.java b/src/java/CdbWebPortal/src/java/gov/anl/aps/cdb/mqtt/CdbEntityTypeSnapshot.java new file mode 100644 index 000000000..ac4d9d523 --- /dev/null +++ b/src/java/CdbWebPortal/src/java/gov/anl/aps/cdb/mqtt/CdbEntityTypeSnapshot.java @@ -0,0 +1,25 @@ +/* + * Copyright (c) UChicago Argonne, LLC. All rights reserved. + * See LICENSE file. + */ +package gov.anl.aps.cdb.mqtt; + +import gov.anl.aps.cdb.portal.model.db.entities.EntityType; + +/** Scalar entity-type reference for event payloads. */ +public final class CdbEntityTypeSnapshot { + + private final Integer id; + private final String name; + + public CdbEntityTypeSnapshot(EntityType entityType) { + if (entityType == null) { + throw new IllegalArgumentException("entity type is required"); + } + id = entityType.getId(); + name = entityType.getName(); + } + + public Integer getId() { return id; } + public String getName() { return name; } +} diff --git a/src/java/CdbWebPortal/src/java/gov/anl/aps/cdb/mqtt/CdbInventoryAssemblyElementEvent.java b/src/java/CdbWebPortal/src/java/gov/anl/aps/cdb/mqtt/CdbInventoryAssemblyElementEvent.java new file mode 100644 index 000000000..0fc86c8a2 --- /dev/null +++ b/src/java/CdbWebPortal/src/java/gov/anl/aps/cdb/mqtt/CdbInventoryAssemblyElementEvent.java @@ -0,0 +1,43 @@ +/* + * Copyright (c) UChicago Argonne, LLC. All rights reserved. + * See LICENSE file. + */ +package gov.anl.aps.cdb.mqtt; + +import gov.anl.aps.cdb.portal.model.db.entities.Item; +import gov.anl.aps.cdb.portal.model.db.entities.ItemDomainInventoryBase; +import gov.anl.aps.cdb.portal.model.db.entities.ItemElement; +import gov.anl.aps.cdb.portal.model.db.entities.UserInfo; + +/** Scalar snapshot of a completed inventory assembly-element assignment. */ +public final class CdbInventoryAssemblyElementEvent extends CdbMqttEvent { + + private final CdbItemSnapshot inventoryAssembly; + private final CdbItemElementSnapshot assemblyElement; + private final CdbItemElementSnapshot catalogElement; + private final CdbItemSnapshot oldAssignedInventoryItem; + private final CdbItemSnapshot newAssignedInventoryItem; + + public CdbInventoryAssemblyElementEvent(ItemDomainInventoryBase inventoryAssembly, + ItemElement assemblyElement, Item oldAssignedInventoryItem, + Item newAssignedInventoryItem, UserInfo user) { + super(CdbMqttTopic.INVENTORY_ASSEMBLY_ELEMENT_UPDATE, user); + if (inventoryAssembly == null || assemblyElement == null) { + throw new IllegalArgumentException("inventory assembly and element are required"); + } + this.inventoryAssembly = new CdbItemSnapshot(inventoryAssembly); + this.assemblyElement = new CdbItemElementSnapshot(assemblyElement); + ItemElement sourceElement = assemblyElement.getDerivedFromItemElement(); + catalogElement = sourceElement == null ? null : new CdbItemElementSnapshot(sourceElement); + this.oldAssignedInventoryItem = oldAssignedInventoryItem == null + ? null : new CdbItemSnapshot(oldAssignedInventoryItem); + this.newAssignedInventoryItem = newAssignedInventoryItem == null + ? null : new CdbItemSnapshot(newAssignedInventoryItem); + } + + public CdbItemSnapshot getInventoryAssembly() { return inventoryAssembly; } + public CdbItemElementSnapshot getAssemblyElement() { return assemblyElement; } + public CdbItemElementSnapshot getCatalogElement() { return catalogElement; } + public CdbItemSnapshot getOldAssignedInventoryItem() { return oldAssignedInventoryItem; } + public CdbItemSnapshot getNewAssignedInventoryItem() { return newAssignedInventoryItem; } +} diff --git a/src/java/CdbWebPortal/src/java/gov/anl/aps/cdb/mqtt/CdbItemElementSnapshot.java b/src/java/CdbWebPortal/src/java/gov/anl/aps/cdb/mqtt/CdbItemElementSnapshot.java new file mode 100644 index 000000000..4562ee78e --- /dev/null +++ b/src/java/CdbWebPortal/src/java/gov/anl/aps/cdb/mqtt/CdbItemElementSnapshot.java @@ -0,0 +1,34 @@ +/* + * Copyright (c) UChicago Argonne, LLC. All rights reserved. + * See LICENSE file. + */ +package gov.anl.aps.cdb.mqtt; + +import gov.anl.aps.cdb.portal.model.db.entities.ItemElement; + +/** Scalar item-element reference for event payloads. */ +public final class CdbItemElementSnapshot { + + private final String id; + private final String name; + private final CdbItemSnapshot parentItem; + private final CdbItemSnapshot containedItem; + + public CdbItemElementSnapshot(ItemElement element) { + if (element == null) { + throw new IllegalArgumentException("element is required"); + } + Object elementId = element.getId(); + id = elementId == null ? null : elementId.toString(); + name = element.getName(); + parentItem = element.getParentItem() == null + ? null : new CdbItemSnapshot(element.getParentItem()); + containedItem = element.getContainedItem() == null + ? null : new CdbItemSnapshot(element.getContainedItem()); + } + + public String getId() { return id; } + public String getName() { return name; } + public CdbItemSnapshot getParentItem() { return parentItem; } + public CdbItemSnapshot getContainedItem() { return containedItem; } +} diff --git a/src/java/CdbWebPortal/src/java/gov/anl/aps/cdb/mqtt/CdbItemLocationEvent.java b/src/java/CdbWebPortal/src/java/gov/anl/aps/cdb/mqtt/CdbItemLocationEvent.java new file mode 100644 index 000000000..d13ff52cd --- /dev/null +++ b/src/java/CdbWebPortal/src/java/gov/anl/aps/cdb/mqtt/CdbItemLocationEvent.java @@ -0,0 +1,39 @@ +/* + * Copyright (c) UChicago Argonne, LLC. All rights reserved. + * See LICENSE file. + */ +package gov.anl.aps.cdb.mqtt; + +import gov.anl.aps.cdb.portal.model.db.entities.Item; +import gov.anl.aps.cdb.portal.model.db.entities.LocatableItem; +import gov.anl.aps.cdb.portal.model.db.entities.UserInfo; + +/** Scalar snapshot of a completed item-location change. */ +public final class CdbItemLocationEvent extends CdbMqttEvent { + + private final CdbItemSnapshot item; + private final CdbItemSnapshot oldLocation; + private final CdbItemSnapshot newLocation; + private final String oldLocationDetails; + private final String newLocationDetails; + + public CdbItemLocationEvent(LocatableItem item, + Item oldLocation, Item newLocation, + String oldLocationDetails, String newLocationDetails, UserInfo user) { + super(CdbMqttTopic.ITEM_LOCATION_UPDATE, user); + if (item == null) { + throw new IllegalArgumentException("item is required"); + } + this.item = new CdbItemSnapshot(item); + this.oldLocation = oldLocation == null ? null : new CdbItemSnapshot(oldLocation); + this.newLocation = newLocation == null ? null : new CdbItemSnapshot(newLocation); + this.oldLocationDetails = oldLocationDetails; + this.newLocationDetails = newLocationDetails; + } + + public CdbItemSnapshot getItem() { return item; } + public CdbItemSnapshot getOldLocation() { return oldLocation; } + public CdbItemSnapshot getNewLocation() { return newLocation; } + public String getOldLocationDetails() { return oldLocationDetails; } + public String getNewLocationDetails() { return newLocationDetails; } +} diff --git a/src/java/CdbWebPortal/src/java/gov/anl/aps/cdb/mqtt/CdbItemPropertyEvent.java b/src/java/CdbWebPortal/src/java/gov/anl/aps/cdb/mqtt/CdbItemPropertyEvent.java new file mode 100644 index 000000000..6f736f8f3 --- /dev/null +++ b/src/java/CdbWebPortal/src/java/gov/anl/aps/cdb/mqtt/CdbItemPropertyEvent.java @@ -0,0 +1,125 @@ +/* + * Copyright (c) UChicago Argonne, LLC. All rights reserved. + * See LICENSE file. + */ +package gov.anl.aps.cdb.mqtt; + +import gov.anl.aps.cdb.portal.model.db.entities.Item; +import gov.anl.aps.cdb.portal.model.db.entities.PropertyValue; +import gov.anl.aps.cdb.portal.model.db.entities.UserInfo; + +/** Scalar snapshot of a completed item-property change. */ +public final class CdbItemPropertyEvent extends CdbMqttEvent { + + public static final int MAX_TEXT_LENGTH = 16 * 1024; + + private final CdbMqttOperation operation; + private final String itemId; + private final String itemType; + private final String itemDescription; + private final CdbPropertySnapshot oldProperty; + private final CdbPropertySnapshot newProperty; + private final String textDiff; + private final boolean diffTruncated; + + private CdbItemPropertyEvent(CdbMqttTopic topic, Item item, + PropertyValue oldProperty, PropertyValue newProperty, UserInfo user) { + super(topic, user); + if (item == null || (oldProperty == null && newProperty == null)) { + throw new IllegalArgumentException("item and a property value are required"); + } + operation = operationFor(topic); + Object id = item.getId(); + itemId = id == null ? null : id.toString(); + itemType = item.getClass().getSimpleName(); + itemDescription = item.getSystemLogString(); + + boolean longText = (oldProperty != null && CdbPropertySnapshot.isLongText(oldProperty)) + || (newProperty != null && CdbPropertySnapshot.isLongText(newProperty)); + String oldText = oldProperty == null ? null : oldProperty.getText(); + String newText = newProperty == null ? null : newProperty.getText(); + if (longText && oldProperty != null && newProperty != null) { + String diff = createLineDiff(oldText, newText); + diffTruncated = diff.length() > MAX_TEXT_LENGTH; + textDiff = truncate(diff); + this.oldProperty = new CdbPropertySnapshot(oldProperty, null, true); + this.newProperty = new CdbPropertySnapshot(newProperty, null, true); + } else { + String boundedOldText = truncate(oldText); + String boundedNewText = truncate(newText); + diffTruncated = length(oldText) > MAX_TEXT_LENGTH || length(newText) > MAX_TEXT_LENGTH; + textDiff = null; + this.oldProperty = oldProperty == null ? null + : new CdbPropertySnapshot(oldProperty, boundedOldText, false); + this.newProperty = newProperty == null ? null + : new CdbPropertySnapshot(newProperty, boundedNewText, false); + } + } + + public static CdbItemPropertyEvent added(Item item, PropertyValue property, UserInfo user) { + return new CdbItemPropertyEvent(CdbMqttTopic.ITEM_PROPERTY_ADD, item, null, property, user); + } + + public static CdbItemPropertyEvent updated(Item item, PropertyValue oldProperty, + PropertyValue newProperty, UserInfo user) { + return new CdbItemPropertyEvent(CdbMqttTopic.ITEM_PROPERTY_UPDATE, + item, oldProperty, newProperty, user); + } + + public static CdbItemPropertyEvent deleted(Item item, PropertyValue property, UserInfo user) { + return new CdbItemPropertyEvent(CdbMqttTopic.ITEM_PROPERTY_DELETE, item, property, null, user); + } + + private static CdbMqttOperation operationFor(CdbMqttTopic topic) { + switch (topic) { + case ITEM_PROPERTY_ADD: return CdbMqttOperation.ADD; + case ITEM_PROPERTY_UPDATE: return CdbMqttOperation.UPDATE; + case ITEM_PROPERTY_DELETE: return CdbMqttOperation.DELETE; + default: throw new IllegalArgumentException("Invalid property event topic"); + } + } + + private static String createLineDiff(String oldText, String newText) { + String[] oldLines = lines(oldText); + String[] newLines = lines(newText); + int prefix = 0; + while (prefix < oldLines.length && prefix < newLines.length + && oldLines[prefix].equals(newLines[prefix])) { + prefix++; + } + int suffix = 0; + while (suffix < oldLines.length - prefix && suffix < newLines.length - prefix + && oldLines[oldLines.length - suffix - 1] + .equals(newLines[newLines.length - suffix - 1])) { + suffix++; + } + + StringBuilder result = new StringBuilder(); + for (int i = prefix; i < oldLines.length - suffix; i++) { + result.append('-').append(oldLines[i]).append('\n'); + } + for (int i = prefix; i < newLines.length - suffix; i++) { + result.append('+').append(newLines[i]).append('\n'); + } + return result.toString(); + } + + private static String[] lines(String value) { + return value == null || value.isEmpty() ? new String[0] : value.split("\\R", -1); + } + + private static int length(String value) { return value == null ? 0 : value.length(); } + private static String truncate(String value) { + return value == null || value.length() <= MAX_TEXT_LENGTH + ? value : value.substring(0, MAX_TEXT_LENGTH); + } + + public CdbMqttOperation getOperation() { return operation; } + public String getItemId() { return itemId; } + public String getItemType() { return itemType; } + public String getItemDescription() { return itemDescription; } + public CdbPropertySnapshot getOldProperty() { return oldProperty; } + public CdbPropertySnapshot getNewProperty() { return newProperty; } + public String getTextDiff() { return textDiff; } + public boolean isDiffTruncated() { return diffTruncated; } +} diff --git a/src/java/CdbWebPortal/src/java/gov/anl/aps/cdb/mqtt/CdbItemSnapshot.java b/src/java/CdbWebPortal/src/java/gov/anl/aps/cdb/mqtt/CdbItemSnapshot.java new file mode 100644 index 000000000..b6f699a9a --- /dev/null +++ b/src/java/CdbWebPortal/src/java/gov/anl/aps/cdb/mqtt/CdbItemSnapshot.java @@ -0,0 +1,51 @@ +/* + * Copyright (c) UChicago Argonne, LLC. All rights reserved. + * See LICENSE file. + */ +package gov.anl.aps.cdb.mqtt; + +import com.fasterxml.jackson.annotation.JsonProperty; +import gov.anl.aps.cdb.portal.model.db.entities.EntityType; +import gov.anl.aps.cdb.portal.model.db.entities.Item; +import java.util.ArrayList; +import java.util.Collections; +import java.util.Comparator; +import java.util.List; + +/** Scalar item reference for event payloads. */ +public final class CdbItemSnapshot { + + private final String id; + private final String domain; + private final Integer domainId; + private final String description; + private final List entityTypes; + + public CdbItemSnapshot(Item item) { + if (item == null) { + throw new IllegalArgumentException("item is required"); + } + Object itemId = item.getId(); + id = itemId == null ? null : itemId.toString(); + domain = item.getDomain() == null ? null : item.getDomain().getName(); + domainId = item.getDomain() == null ? null : item.getDomain().getId(); + description = item.getSystemLogString(); + List snapshots = new ArrayList<>(); + if (item.getEntityTypeList() != null) { + List entityTypes = new ArrayList<>(item.getEntityTypeList()); + entityTypes.sort(Comparator.comparing(EntityType::getId, + Comparator.nullsLast(Comparator.naturalOrder()))); + for (EntityType entityType : entityTypes) { + snapshots.add(new CdbEntityTypeSnapshot(entityType)); + } + } + entityTypes = Collections.unmodifiableList(snapshots); + } + + public String getId() { return id; } + public String getDomain() { return domain; } + @JsonProperty("domain_id") + public Integer getDomainId() { return domainId; } + public String getDescription() { return description; } + public List getEntityTypes() { return entityTypes; } +} diff --git a/src/java/CdbWebPortal/src/java/gov/anl/aps/cdb/mqtt/CdbMachineAssignedItemEvent.java b/src/java/CdbWebPortal/src/java/gov/anl/aps/cdb/mqtt/CdbMachineAssignedItemEvent.java new file mode 100644 index 000000000..ce0cae98c --- /dev/null +++ b/src/java/CdbWebPortal/src/java/gov/anl/aps/cdb/mqtt/CdbMachineAssignedItemEvent.java @@ -0,0 +1,39 @@ +/* + * Copyright (c) UChicago Argonne, LLC. All rights reserved. + * See LICENSE file. + */ +package gov.anl.aps.cdb.mqtt; + +import gov.anl.aps.cdb.portal.model.db.entities.Item; +import gov.anl.aps.cdb.portal.model.db.entities.ItemDomainMachineDesign; +import gov.anl.aps.cdb.portal.model.db.entities.UserInfo; + +/** Scalar snapshot of a completed machine assigned-item change. */ +public final class CdbMachineAssignedItemEvent extends CdbMqttEvent { + + private final CdbItemSnapshot machine; + private final CdbItemSnapshot oldAssignedItem; + private final CdbItemSnapshot newAssignedItem; + private final boolean oldInstalled; + private final boolean newInstalled; + + public CdbMachineAssignedItemEvent(ItemDomainMachineDesign machine, + Item oldAssignedItem, Item newAssignedItem, + boolean oldInstalled, boolean newInstalled, UserInfo user) { + super(CdbMqttTopic.MACHINE_DESIGN_ASSIGNED_ITEM_UPDATE, user); + if (machine == null) { + throw new IllegalArgumentException("machine is required"); + } + this.machine = new CdbItemSnapshot(machine); + this.oldAssignedItem = oldAssignedItem == null ? null : new CdbItemSnapshot(oldAssignedItem); + this.newAssignedItem = newAssignedItem == null ? null : new CdbItemSnapshot(newAssignedItem); + this.oldInstalled = oldInstalled; + this.newInstalled = newInstalled; + } + + public CdbItemSnapshot getMachine() { return machine; } + public CdbItemSnapshot getOldAssignedItem() { return oldAssignedItem; } + public CdbItemSnapshot getNewAssignedItem() { return newAssignedItem; } + public boolean isOldInstalled() { return oldInstalled; } + public boolean isNewInstalled() { return newInstalled; } +} diff --git a/src/java/CdbWebPortal/src/java/gov/anl/aps/cdb/mqtt/CdbMachineMoveEvent.java b/src/java/CdbWebPortal/src/java/gov/anl/aps/cdb/mqtt/CdbMachineMoveEvent.java new file mode 100644 index 000000000..65fed6523 --- /dev/null +++ b/src/java/CdbWebPortal/src/java/gov/anl/aps/cdb/mqtt/CdbMachineMoveEvent.java @@ -0,0 +1,32 @@ +/* + * Copyright (c) UChicago Argonne, LLC. All rights reserved. + * See LICENSE file. + */ +package gov.anl.aps.cdb.mqtt; + +import gov.anl.aps.cdb.portal.model.db.entities.ItemDomainMachineDesign; +import gov.anl.aps.cdb.portal.model.db.entities.UserInfo; + +/** Scalar snapshot of a completed machine move. */ +public final class CdbMachineMoveEvent extends CdbMqttEvent { + + private final CdbItemSnapshot machine; + private final CdbItemSnapshot oldParent; + private final CdbItemSnapshot newParent; + + public CdbMachineMoveEvent(ItemDomainMachineDesign machine, + ItemDomainMachineDesign oldParent, ItemDomainMachineDesign newParent, + UserInfo user) { + super(CdbMqttTopic.MACHINE_DESIGN_MOVE, user); + if (machine == null || newParent == null) { + throw new IllegalArgumentException("machine and new parent are required"); + } + this.machine = new CdbItemSnapshot(machine); + this.oldParent = oldParent == null ? null : new CdbItemSnapshot(oldParent); + this.newParent = new CdbItemSnapshot(newParent); + } + + public CdbItemSnapshot getMachine() { return machine; } + public CdbItemSnapshot getOldParent() { return oldParent; } + public CdbItemSnapshot getNewParent() { return newParent; } +} diff --git a/src/java/CdbWebPortal/src/java/gov/anl/aps/cdb/mqtt/CdbMachineRepresentedElementEvent.java b/src/java/CdbWebPortal/src/java/gov/anl/aps/cdb/mqtt/CdbMachineRepresentedElementEvent.java new file mode 100644 index 000000000..086926b93 --- /dev/null +++ b/src/java/CdbWebPortal/src/java/gov/anl/aps/cdb/mqtt/CdbMachineRepresentedElementEvent.java @@ -0,0 +1,44 @@ +/* + * Copyright (c) UChicago Argonne, LLC. All rights reserved. + * See LICENSE file. + */ +package gov.anl.aps.cdb.mqtt; + +import gov.anl.aps.cdb.portal.model.db.entities.ItemDomainMachineDesign; +import gov.anl.aps.cdb.portal.model.db.entities.ItemElement; +import gov.anl.aps.cdb.portal.model.db.entities.UserInfo; + +/** Scalar snapshot of a completed represented-element change. */ +public final class CdbMachineRepresentedElementEvent extends CdbMqttEvent { + + private final CdbItemSnapshot machine; + private final CdbItemElementSnapshot oldRepresentedElement; + private final CdbItemElementSnapshot newRepresentedElement; + private final CdbItemSnapshot sourceTemplate; + + public CdbMachineRepresentedElementEvent(ItemDomainMachineDesign machine, + ItemElement oldRepresentedElement, ItemElement newRepresentedElement, + UserInfo user) { + this(machine, oldRepresentedElement, newRepresentedElement, null, user); + } + + public CdbMachineRepresentedElementEvent(ItemDomainMachineDesign machine, + ItemElement oldRepresentedElement, ItemElement newRepresentedElement, + ItemDomainMachineDesign sourceTemplate, UserInfo user) { + super(CdbMqttTopic.MACHINE_DESIGN_REPRESENTED_ELEMENT_UPDATE, user); + if (machine == null) { + throw new IllegalArgumentException("machine is required"); + } + this.machine = new CdbItemSnapshot(machine); + this.oldRepresentedElement = oldRepresentedElement == null + ? null : new CdbItemElementSnapshot(oldRepresentedElement); + this.newRepresentedElement = newRepresentedElement == null + ? null : new CdbItemElementSnapshot(newRepresentedElement); + this.sourceTemplate = sourceTemplate == null ? null : new CdbItemSnapshot(sourceTemplate); + } + + public CdbItemSnapshot getMachine() { return machine; } + public CdbItemElementSnapshot getOldRepresentedElement() { return oldRepresentedElement; } + public CdbItemElementSnapshot getNewRepresentedElement() { return newRepresentedElement; } + public CdbItemSnapshot getSourceTemplate() { return sourceTemplate; } +} diff --git a/src/java/CdbWebPortal/src/java/gov/anl/aps/cdb/mqtt/CdbMachineTrashEvent.java b/src/java/CdbWebPortal/src/java/gov/anl/aps/cdb/mqtt/CdbMachineTrashEvent.java new file mode 100644 index 000000000..bf5be2225 --- /dev/null +++ b/src/java/CdbWebPortal/src/java/gov/anl/aps/cdb/mqtt/CdbMachineTrashEvent.java @@ -0,0 +1,39 @@ +/* + * Copyright (c) UChicago Argonne, LLC. All rights reserved. + * See LICENSE file. + */ +package gov.anl.aps.cdb.mqtt; + +import gov.anl.aps.cdb.portal.model.db.entities.ItemDomainMachineDesign; +import gov.anl.aps.cdb.portal.model.db.entities.UserInfo; + +/** Scalar snapshot of a completed machine trash or restore operation. */ +public final class CdbMachineTrashEvent extends CdbMqttEvent { + + private final CdbMqttOperation operation; + private final CdbItemSnapshot affectedMachine; + private final CdbItemSnapshot requestedHierarchyRoot; + private final int affectedMachineCount; + + public CdbMachineTrashEvent(CdbMqttTopic topic, ItemDomainMachineDesign machine, + ItemDomainMachineDesign requestedRoot, int affectedCount, UserInfo user) { + super(topic, user); + if (topic != CdbMqttTopic.MACHINE_DESIGN_TRASH + && topic != CdbMqttTopic.MACHINE_DESIGN_RESTORE) { + throw new IllegalArgumentException("trash or restore topic is required"); + } + if (machine == null || requestedRoot == null || affectedCount < 1) { + throw new IllegalArgumentException("machine, root, and affected count are required"); + } + operation = topic == CdbMqttTopic.MACHINE_DESIGN_TRASH + ? CdbMqttOperation.TRASH : CdbMqttOperation.RESTORE; + this.affectedMachine = new CdbItemSnapshot(machine); + this.requestedHierarchyRoot = new CdbItemSnapshot(requestedRoot); + this.affectedMachineCount = affectedCount; + } + + public CdbMqttOperation getOperation() { return operation; } + public CdbItemSnapshot getAffectedMachine() { return affectedMachine; } + public CdbItemSnapshot getRequestedHierarchyRoot() { return requestedHierarchyRoot; } + public int getAffectedMachineCount() { return affectedMachineCount; } +} diff --git a/src/java/CdbWebPortal/src/java/gov/anl/aps/cdb/mqtt/CdbMqttEvent.java b/src/java/CdbWebPortal/src/java/gov/anl/aps/cdb/mqtt/CdbMqttEvent.java new file mode 100644 index 000000000..9e7d3fa34 --- /dev/null +++ b/src/java/CdbWebPortal/src/java/gov/anl/aps/cdb/mqtt/CdbMqttEvent.java @@ -0,0 +1,32 @@ +/* + * Copyright (c) UChicago Argonne, LLC. All rights reserved. + * See LICENSE file. + */ +package gov.anl.aps.cdb.mqtt; + +import gov.anl.aps.cdb.base.mqtt.MqttEvent; +import gov.anl.aps.cdb.portal.model.db.entities.UserInfo; + +/** Base for CDB MQTT events with common routing and actor context. */ +public abstract class CdbMqttEvent extends MqttEvent { + + private final String topic; + private final String triggeredByUsername; + + protected CdbMqttEvent(CdbMqttTopic topic, UserInfo triggeredByUser) { + if (topic == null) { + throw new IllegalArgumentException("topic is required"); + } + this.topic = topic.getValue(); + triggeredByUsername = triggeredByUser == null ? null : triggeredByUser.getUsername(); + } + + @Override + public final String getTopic() { + return topic; + } + + public final String getTriggeredByUsername() { + return triggeredByUsername; + } +} diff --git a/src/java/CdbWebPortal/src/java/gov/anl/aps/cdb/mqtt/CdbMqttOperation.java b/src/java/CdbWebPortal/src/java/gov/anl/aps/cdb/mqtt/CdbMqttOperation.java new file mode 100644 index 000000000..bf4246645 --- /dev/null +++ b/src/java/CdbWebPortal/src/java/gov/anl/aps/cdb/mqtt/CdbMqttOperation.java @@ -0,0 +1,27 @@ +/* + * Copyright (c) UChicago Argonne, LLC. All rights reserved. + * See LICENSE file. + */ +package gov.anl.aps.cdb.mqtt; + +import com.fasterxml.jackson.annotation.JsonValue; + +/** Operation represented by a CDB MQTT event. */ +public enum CdbMqttOperation { + ADD("add"), + UPDATE("update"), + DELETE("delete"), + TRASH("trash"), + RESTORE("restore"); + + private final String value; + + CdbMqttOperation(String value) { + this.value = value; + } + + @JsonValue + public String getValue() { + return value; + } +} diff --git a/src/java/CdbWebPortal/src/java/gov/anl/aps/cdb/mqtt/CdbMqttPublisher.java b/src/java/CdbWebPortal/src/java/gov/anl/aps/cdb/mqtt/CdbMqttPublisher.java new file mode 100644 index 000000000..f2bd5f135 --- /dev/null +++ b/src/java/CdbWebPortal/src/java/gov/anl/aps/cdb/mqtt/CdbMqttPublisher.java @@ -0,0 +1,50 @@ +/* + * Copyright (c) UChicago Argonne, LLC. All rights reserved. + * See LICENSE file. + */ +package gov.anl.aps.cdb.mqtt; + +import com.fasterxml.jackson.databind.ObjectMapper; +import gov.anl.aps.cdb.base.mqtt.MqttEvent; +import gov.anl.aps.cdb.base.mqtt.MqttPublisher; +import gov.anl.aps.cdb.base.mqtt.exception.MqttException; +import gov.anl.aps.cdb.base.mqtt.exception.MqttNotConfiguredException; +import gov.anl.aps.cdb.base.mqtt.exception.MqttPublishException; +import gov.anl.aps.cdb.base.mqtt.jca.JcaMqttPublisher; +import gov.anl.aps.cdb.base.mqtt.jca.JndiMqttConnectionProvider; +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; + +/** Application adapter that makes MQTT publishing best effort. */ +public final class CdbMqttPublisher { + + public static final String JNDI_RESOURCE_NAME = "cdb/MQTT/resource"; + private static final Logger LOGGER = LogManager.getLogger(CdbMqttPublisher.class.getName()); + private static final MqttPublisher PUBLISHER = new JcaMqttPublisher( + new JndiMqttConnectionProvider(JNDI_RESOURCE_NAME), new ObjectMapper()); + + private CdbMqttPublisher() { + } + + public static void publish(MqttEvent event) { + try { + PUBLISHER.publish(event); + } catch (MqttNotConfiguredException ex) { + LOGGER.warn("MQTT is unavailable; skipped event for " + event.getTopic() + + ". " + ex.getMessage()); + } catch (MqttPublishException ex) { + LOGGER.warn("MQTT broker is unavailable or disconnected; skipped event for " + + event.getTopic() + ". " + getRootCauseMessage(ex)); + } catch (MqttException | RuntimeException ex) { + LOGGER.error("Failed to prepare MQTT event for " + event.getTopic(), ex); + } + } + + private static String getRootCauseMessage(Throwable error) { + Throwable cause = error; + while (cause.getCause() != null && cause.getCause() != cause) { + cause = cause.getCause(); + } + return cause.getMessage() == null ? cause.getClass().getSimpleName() : cause.getMessage(); + } +} diff --git a/src/java/CdbWebPortal/src/java/gov/anl/aps/cdb/mqtt/CdbMqttTopic.java b/src/java/CdbWebPortal/src/java/gov/anl/aps/cdb/mqtt/CdbMqttTopic.java new file mode 100644 index 000000000..ac4d3cc10 --- /dev/null +++ b/src/java/CdbWebPortal/src/java/gov/anl/aps/cdb/mqtt/CdbMqttTopic.java @@ -0,0 +1,31 @@ +/* + * Copyright (c) UChicago Argonne, LLC. All rights reserved. + * See LICENSE file. + */ +package gov.anl.aps.cdb.mqtt; + +public enum CdbMqttTopic { + ADD("cdb/entity/add"), + UPDATE("cdb/entity/update"), + DELETE("cdb/entity/delete"), + ITEM_PROPERTY_ADD("cdb/item/property/add"), + ITEM_PROPERTY_UPDATE("cdb/item/property/update"), + ITEM_PROPERTY_DELETE("cdb/item/property/delete"), + MACHINE_DESIGN_ASSIGNED_ITEM_UPDATE("cdb/machine-design/assigned-item/update"), + MACHINE_DESIGN_MOVE("cdb/machine-design/move"), + MACHINE_DESIGN_REPRESENTED_ELEMENT_UPDATE("cdb/machine-design/represented-element/update"), + MACHINE_DESIGN_TRASH("cdb/machine-design/trash"), + MACHINE_DESIGN_RESTORE("cdb/machine-design/restore"), + ITEM_LOCATION_UPDATE("cdb/item/location/update"), + INVENTORY_ASSEMBLY_ELEMENT_UPDATE("cdb/inventory/assembly-element/update"); + + private final String value; + + CdbMqttTopic(String value) { + this.value = value; + } + + public String getValue() { + return value; + } +} diff --git a/src/java/CdbWebPortal/src/java/gov/anl/aps/cdb/mqtt/CdbPropertySnapshot.java b/src/java/CdbWebPortal/src/java/gov/anl/aps/cdb/mqtt/CdbPropertySnapshot.java new file mode 100644 index 000000000..4a7c08c06 --- /dev/null +++ b/src/java/CdbWebPortal/src/java/gov/anl/aps/cdb/mqtt/CdbPropertySnapshot.java @@ -0,0 +1,76 @@ +/* + * Copyright (c) UChicago Argonne, LLC. All rights reserved. + * See LICENSE file. + */ +package gov.anl.aps.cdb.mqtt; + +import gov.anl.aps.cdb.portal.model.db.entities.PropertyMetadata; +import gov.anl.aps.cdb.portal.model.db.entities.PropertyType; +import gov.anl.aps.cdb.portal.model.db.entities.PropertyTypeHandler; +import gov.anl.aps.cdb.portal.model.db.entities.PropertyValue; +import gov.anl.aps.cdb.portal.model.jsf.handlers.MarkdownPropertyTypeHandler; +import java.util.Collections; +import java.util.Map; +import java.util.TreeMap; + +/** Scalar snapshot of a property value. */ +public final class CdbPropertySnapshot { + + private final String propertyValueId; + private final String propertyTypeId; + private final String propertyTypeName; + private final String tag; + private final String value; + private final String text; + private final String units; + private final String description; + private final Map metadata; + private final boolean textOmitted; + + public CdbPropertySnapshot(PropertyValue propertyValue, String text, boolean textOmitted) { + Object id = propertyValue.getId(); + propertyValueId = id == null ? null : id.toString(); + PropertyType propertyType = propertyValue.getPropertyType(); + propertyTypeId = propertyType == null || propertyType.getId() == null + ? null : propertyType.getId().toString(); + propertyTypeName = propertyType == null ? null : propertyType.getName(); + tag = propertyValue.getTag(); + value = propertyValue.getValue(); + this.text = text; + units = propertyValue.getUnits(); + description = propertyValue.getDescription(); + metadata = snapshotMetadata(propertyValue); + this.textOmitted = textOmitted; + } + + public static boolean isLongText(PropertyValue propertyValue) { + if (propertyValue.getText() != null) { + return true; + } + PropertyType propertyType = propertyValue.getPropertyType(); + PropertyTypeHandler handler = propertyType == null + ? null : propertyType.getPropertyTypeHandler(); + return handler != null && MarkdownPropertyTypeHandler.HANDLER_NAME.equals(handler.getName()); + } + + private static Map snapshotMetadata(PropertyValue propertyValue) { + Map result = new TreeMap<>(); + if (propertyValue.getPropertyMetadataList() != null) { + for (PropertyMetadata propertyMetadata : propertyValue.getPropertyMetadataList()) { + result.put(propertyMetadata.getMetadataKey(), propertyMetadata.getMetadataValue()); + } + } + return Collections.unmodifiableMap(result); + } + + public String getPropertyValueId() { return propertyValueId; } + public String getPropertyTypeId() { return propertyTypeId; } + public String getPropertyTypeName() { return propertyTypeName; } + public String getTag() { return tag; } + public String getValue() { return value; } + public String getText() { return text; } + public String getUnits() { return units; } + public String getDescription() { return description; } + public Map getMetadata() { return metadata; } + public boolean isTextOmitted() { return textOmitted; } +} diff --git a/src/java/CdbWebPortal/src/java/gov/anl/aps/cdb/mqtt/metadata/InventoryAssemblyElementChange.java b/src/java/CdbWebPortal/src/java/gov/anl/aps/cdb/mqtt/metadata/InventoryAssemblyElementChange.java new file mode 100644 index 000000000..face2d4d3 --- /dev/null +++ b/src/java/CdbWebPortal/src/java/gov/anl/aps/cdb/mqtt/metadata/InventoryAssemblyElementChange.java @@ -0,0 +1,27 @@ +/* + * Copyright (c) UChicago Argonne, LLC. All rights reserved. + * See LICENSE file. + */ +package gov.anl.aps.cdb.mqtt.metadata; + +import gov.anl.aps.cdb.portal.model.db.entities.Item; +import gov.anl.aps.cdb.portal.model.db.entities.ItemElement; + +/** Pending pre-update state for an inventory assembly-element assignment. */ +public final class InventoryAssemblyElementChange { + + private final ItemElement assemblyElement; + private final Item oldAssignedInventoryItem; + private final Item newAssignedInventoryItem; + + public InventoryAssemblyElementChange(ItemElement assemblyElement, + Item oldAssignedInventoryItem, Item newAssignedInventoryItem) { + this.assemblyElement = assemblyElement; + this.oldAssignedInventoryItem = oldAssignedInventoryItem; + this.newAssignedInventoryItem = newAssignedInventoryItem; + } + + public ItemElement getAssemblyElement() { return assemblyElement; } + public Item getOldAssignedInventoryItem() { return oldAssignedInventoryItem; } + public Item getNewAssignedInventoryItem() { return newAssignedInventoryItem; } +} diff --git a/src/java/CdbWebPortal/src/java/gov/anl/aps/cdb/mqtt/metadata/ItemLocationChange.java b/src/java/CdbWebPortal/src/java/gov/anl/aps/cdb/mqtt/metadata/ItemLocationChange.java new file mode 100644 index 000000000..2cef9b216 --- /dev/null +++ b/src/java/CdbWebPortal/src/java/gov/anl/aps/cdb/mqtt/metadata/ItemLocationChange.java @@ -0,0 +1,29 @@ +/* + * Copyright (c) UChicago Argonne, LLC. All rights reserved. + * See LICENSE file. + */ +package gov.anl.aps.cdb.mqtt.metadata; + +import gov.anl.aps.cdb.portal.model.db.entities.Item; + +/** Pending pre-update state for an item-location change. */ +public final class ItemLocationChange { + + private final Item oldLocation; + private final String oldLocationDetails; + private final Item newLocation; + private final String newLocationDetails; + + public ItemLocationChange(Item oldLocation, String oldLocationDetails, + Item newLocation, String newLocationDetails) { + this.oldLocation = oldLocation; + this.oldLocationDetails = oldLocationDetails; + this.newLocation = newLocation; + this.newLocationDetails = newLocationDetails; + } + + public Item getOldLocation() { return oldLocation; } + public String getOldLocationDetails() { return oldLocationDetails; } + public Item getNewLocation() { return newLocation; } + public String getNewLocationDetails() { return newLocationDetails; } +} diff --git a/src/java/CdbWebPortal/src/java/gov/anl/aps/cdb/mqtt/metadata/MachineAssignedItemChange.java b/src/java/CdbWebPortal/src/java/gov/anl/aps/cdb/mqtt/metadata/MachineAssignedItemChange.java new file mode 100644 index 000000000..12fc30228 --- /dev/null +++ b/src/java/CdbWebPortal/src/java/gov/anl/aps/cdb/mqtt/metadata/MachineAssignedItemChange.java @@ -0,0 +1,22 @@ +/* + * Copyright (c) UChicago Argonne, LLC. All rights reserved. + * See LICENSE file. + */ +package gov.anl.aps.cdb.mqtt.metadata; + +import gov.anl.aps.cdb.portal.model.db.entities.Item; + +/** Pending pre-update state for a machine assigned-item change. */ +public final class MachineAssignedItemChange { + + private final Item oldAssignedItem; + private final boolean oldInstalled; + + public MachineAssignedItemChange(Item oldAssignedItem, boolean oldInstalled) { + this.oldAssignedItem = oldAssignedItem; + this.oldInstalled = oldInstalled; + } + + public Item getOldAssignedItem() { return oldAssignedItem; } + public boolean isOldInstalled() { return oldInstalled; } +} diff --git a/src/java/CdbWebPortal/src/java/gov/anl/aps/cdb/mqtt/metadata/MachineRepresentedElementChange.java b/src/java/CdbWebPortal/src/java/gov/anl/aps/cdb/mqtt/metadata/MachineRepresentedElementChange.java new file mode 100644 index 000000000..e5453b478 --- /dev/null +++ b/src/java/CdbWebPortal/src/java/gov/anl/aps/cdb/mqtt/metadata/MachineRepresentedElementChange.java @@ -0,0 +1,28 @@ +/* + * Copyright (c) UChicago Argonne, LLC. All rights reserved. + * See LICENSE file. + */ +package gov.anl.aps.cdb.mqtt.metadata; + +import gov.anl.aps.cdb.portal.model.db.entities.ItemDomainMachineDesign; +import gov.anl.aps.cdb.portal.model.db.entities.ItemElement; + +/** Pending pre-update state for a machine represented-element change. */ +public final class MachineRepresentedElementChange { + + private final ItemElement oldRepresentedElement; + private final ItemDomainMachineDesign sourceTemplate; + + public MachineRepresentedElementChange(ItemElement oldRepresentedElement) { + this(oldRepresentedElement, null); + } + + public MachineRepresentedElementChange(ItemElement oldRepresentedElement, + ItemDomainMachineDesign sourceTemplate) { + this.oldRepresentedElement = oldRepresentedElement; + this.sourceTemplate = sourceTemplate; + } + + public ItemElement getOldRepresentedElement() { return oldRepresentedElement; } + public ItemDomainMachineDesign getSourceTemplate() { return sourceTemplate; } +} diff --git a/src/java/CdbWebPortal/src/java/gov/anl/aps/cdb/mqtt/metadata/MachineTrashChange.java b/src/java/CdbWebPortal/src/java/gov/anl/aps/cdb/mqtt/metadata/MachineTrashChange.java new file mode 100644 index 000000000..9754dcb6b --- /dev/null +++ b/src/java/CdbWebPortal/src/java/gov/anl/aps/cdb/mqtt/metadata/MachineTrashChange.java @@ -0,0 +1,27 @@ +/* + * Copyright (c) UChicago Argonne, LLC. All rights reserved. + * See LICENSE file. + */ +package gov.anl.aps.cdb.mqtt.metadata; + +import gov.anl.aps.cdb.mqtt.CdbMqttTopic; +import gov.anl.aps.cdb.portal.model.db.entities.ItemDomainMachineDesign; + +/** Pending context for a machine trash or restore event. */ +public final class MachineTrashChange { + + private final CdbMqttTopic topic; + private final ItemDomainMachineDesign requestedHierarchyRoot; + private final int affectedMachineCount; + + public MachineTrashChange(CdbMqttTopic topic, + ItemDomainMachineDesign requestedRoot, int affectedCount) { + this.topic = topic; + this.requestedHierarchyRoot = requestedRoot; + this.affectedMachineCount = affectedCount; + } + + public CdbMqttTopic getTopic() { return topic; } + public ItemDomainMachineDesign getRequestedHierarchyRoot() { return requestedHierarchyRoot; } + public int getAffectedMachineCount() { return affectedMachineCount; } +} diff --git a/src/java/CdbWebPortal/src/java/gov/anl/aps/cdb/mqtt/metadata/PropertyValueChange.java b/src/java/CdbWebPortal/src/java/gov/anl/aps/cdb/mqtt/metadata/PropertyValueChange.java new file mode 100644 index 000000000..511dd46ac --- /dev/null +++ b/src/java/CdbWebPortal/src/java/gov/anl/aps/cdb/mqtt/metadata/PropertyValueChange.java @@ -0,0 +1,39 @@ +/* + * Copyright (c) UChicago Argonne, LLC. All rights reserved. + * See LICENSE file. + */ +package gov.anl.aps.cdb.mqtt.metadata; + +import gov.anl.aps.cdb.portal.model.db.entities.PropertyValue; + +/** Pending pre-update state for a property-value change. */ +public final class PropertyValueChange { + + public enum Type { ADD, UPDATE, DELETE } + + private final Type type; + private final PropertyValue oldValue; + private final PropertyValue newValue; + + private PropertyValueChange(Type type, PropertyValue oldValue, PropertyValue newValue) { + this.type = type; + this.oldValue = oldValue; + this.newValue = newValue; + } + + public static PropertyValueChange added(PropertyValue value) { + return new PropertyValueChange(Type.ADD, null, value); + } + + public static PropertyValueChange updated(PropertyValue oldValue, PropertyValue newValue) { + return new PropertyValueChange(Type.UPDATE, oldValue, newValue); + } + + public static PropertyValueChange deleted(PropertyValue value) { + return new PropertyValueChange(Type.DELETE, value, null); + } + + public Type getType() { return type; } + public PropertyValue getOldValue() { return oldValue; } + public PropertyValue getNewValue() { return newValue; } +} diff --git a/src/java/CdbWebPortal/src/java/gov/anl/aps/cdb/portal/controllers/ItemDomainMachineDesignBaseController.java b/src/java/CdbWebPortal/src/java/gov/anl/aps/cdb/portal/controllers/ItemDomainMachineDesignBaseController.java index 30c55152b..9a8fa0bac 100644 --- a/src/java/CdbWebPortal/src/java/gov/anl/aps/cdb/portal/controllers/ItemDomainMachineDesignBaseController.java +++ b/src/java/CdbWebPortal/src/java/gov/anl/aps/cdb/portal/controllers/ItemDomainMachineDesignBaseController.java @@ -7,6 +7,8 @@ import gov.anl.aps.cdb.common.constants.CdbRole; import gov.anl.aps.cdb.portal.controllers.extensions.CableWizard; import gov.anl.aps.cdb.common.exceptions.CdbException; +import gov.anl.aps.cdb.mqtt.CdbMqttTopic; +import gov.anl.aps.cdb.mqtt.metadata.MachineTrashChange; import gov.anl.aps.cdb.portal.constants.EntityTypeName; import gov.anl.aps.cdb.portal.constants.ItemDomainName; import gov.anl.aps.cdb.portal.constants.ItemElementRelationshipTypeNames; @@ -3332,6 +3334,8 @@ public void moveToTrash() { ItemDomainMachineDesign current = getCurrent(); ItemDomainMachineDesign parentMachineDesign = current.getParentMachineDesign(); + prepareTrashChanges(moveToTrashItemsToUpdate, rootItemToDelete, + CdbMqttTopic.MACHINE_DESIGN_TRASH, false); // mark items moving to trash as deleted and remove relationship between root item and its parent ValidInfo moveValidInfo = handleMoveToTrash(moveToTrashItemsToUpdate, moveToTrashElementsToDelete); if (!moveValidInfo.isValid()) { @@ -3370,6 +3374,24 @@ protected void performMoveToTrashOperationsForItem(ItemDomainMachineDesign item) item.setIsDeleted(); } + protected void prepareTrashChanges(List items, + ItemDomainMachineDesign requestedRoot, CdbMqttTopic topic, + boolean expectedDeletedState) { + List changedItems = new ArrayList<>(); + for (ItemDomainMachineDesign item : items) { + ItemDomainMachineDesign originalItem = findById(item.getId()); + if (originalItem != null + && originalItem.getIsItemDeleted() == expectedDeletedState) { + changedItems.add(item); + } + } + int affectedCount = changedItems.size(); + for (ItemDomainMachineDesign item : changedItems) { + item.setPendingTrashChange( + new MachineTrashChange(topic, requestedRoot, affectedCount)); + } + } + // // public EntityInfo getEntityInfoBranchUpdate() { diff --git a/src/java/CdbWebPortal/src/java/gov/anl/aps/cdb/portal/controllers/ItemDomainMachineDesignDeletedItemsController.java b/src/java/CdbWebPortal/src/java/gov/anl/aps/cdb/portal/controllers/ItemDomainMachineDesignDeletedItemsController.java index 91e694bdc..cc33699b3 100644 --- a/src/java/CdbWebPortal/src/java/gov/anl/aps/cdb/portal/controllers/ItemDomainMachineDesignDeletedItemsController.java +++ b/src/java/CdbWebPortal/src/java/gov/anl/aps/cdb/portal/controllers/ItemDomainMachineDesignDeletedItemsController.java @@ -5,6 +5,7 @@ package gov.anl.aps.cdb.portal.controllers; import gov.anl.aps.cdb.common.constants.CdbRole; +import gov.anl.aps.cdb.mqtt.CdbMqttTopic; import gov.anl.aps.cdb.common.exceptions.CdbException; import gov.anl.aps.cdb.portal.constants.EntityTypeName; import gov.anl.aps.cdb.portal.controllers.settings.ItemDomainMachineDesignDeletedItemSettings; @@ -196,6 +197,8 @@ public void restoreDeletedItem() { } } + prepareTrashChanges(itemsToRestore, itemToRestore, + CdbMqttTopic.MACHINE_DESIGN_RESTORE, true); // remove 'deleted' entity type for all items for (ItemDomainMachineDesign item : itemsToRestore) { item.unsetIsDeleted(); diff --git a/src/java/CdbWebPortal/src/java/gov/anl/aps/cdb/portal/controllers/LocatableItemController.java b/src/java/CdbWebPortal/src/java/gov/anl/aps/cdb/portal/controllers/LocatableItemController.java index cab450cce..23eafc652 100644 --- a/src/java/CdbWebPortal/src/java/gov/anl/aps/cdb/portal/controllers/LocatableItemController.java +++ b/src/java/CdbWebPortal/src/java/gov/anl/aps/cdb/portal/controllers/LocatableItemController.java @@ -8,6 +8,7 @@ import gov.anl.aps.cdb.common.exceptions.InvalidRequest; import gov.anl.aps.cdb.portal.constants.ItemDomainName; import gov.anl.aps.cdb.portal.constants.ItemElementRelationshipTypeNames; +import gov.anl.aps.cdb.mqtt.metadata.ItemLocationChange; import gov.anl.aps.cdb.portal.model.db.beans.ItemElementRelationshipFacade; import gov.anl.aps.cdb.portal.model.db.beans.ItemFacade; import gov.anl.aps.cdb.portal.model.db.beans.RelationshipTypeFacade; @@ -1005,6 +1006,10 @@ public void updateItemLocation(LocatableItem item, UserInfo updateUser) throws I } if (newItemWithNewLocation || locationDifferentOnCurrentItem) { + Item oldLocation = existingItem == null ? null : existingItem.getLocationItem(); + String oldLocationDetails = existingItem == null + ? null : normalizeLocationDetails(existingItem.getLocationDetails()); + String newLocationDetails = normalizeLocationDetails(item.getLocationDetails()); // Verify that the location can be changed. if ((existingItem != null) && (locationEditable(existingItem)) == false) { Item location = existingItem.getActiveLocation(); @@ -1064,6 +1069,8 @@ public void updateItemLocation(LocatableItem item, UserInfo updateUser) throws I } ierhList.add(ierh); + item.setPendingLocationChange(new ItemLocationChange( + oldLocation, oldLocationDetails, item.getLocationItem(), newLocationDetails)); } else { logger.error("updateItemLocation(): item location relationship unexpectedly null for item: " + item.toString()); @@ -1071,6 +1078,10 @@ public void updateItemLocation(LocatableItem item, UserInfo updateUser) throws I } } + private static String normalizeLocationDetails(String details) { + return details == null || details.isEmpty() ? null : details; + } + private RelationshipType getLocationRelationshipType() { return relationshipTypeFacade.findByName(ItemElementRelationshipTypeNames.itemLocation.getValue()); } diff --git a/src/java/CdbWebPortal/src/java/gov/anl/aps/cdb/portal/controllers/utilities/CdbEntityControllerUtility.java b/src/java/CdbWebPortal/src/java/gov/anl/aps/cdb/portal/controllers/utilities/CdbEntityControllerUtility.java index 21c171c37..0b1770fae 100644 --- a/src/java/CdbWebPortal/src/java/gov/anl/aps/cdb/portal/controllers/utilities/CdbEntityControllerUtility.java +++ b/src/java/CdbWebPortal/src/java/gov/anl/aps/cdb/portal/controllers/utilities/CdbEntityControllerUtility.java @@ -5,6 +5,10 @@ package gov.anl.aps.cdb.portal.controllers.utilities; import gov.anl.aps.cdb.common.exceptions.CdbException; +import gov.anl.aps.cdb.base.mqtt.MqttEvent; +import gov.anl.aps.cdb.mqtt.CdbEntityEvent; +import gov.anl.aps.cdb.mqtt.CdbMqttPublisher; +import gov.anl.aps.cdb.mqtt.CdbMqttTopic; import gov.anl.aps.cdb.common.utilities.StringUtility; import gov.anl.aps.cdb.portal.constants.SystemLogLevel; import gov.anl.aps.cdb.portal.model.db.beans.CdbEntityFacade; @@ -14,6 +18,8 @@ import gov.anl.aps.cdb.portal.model.db.entities.UserInfo; import gov.anl.aps.cdb.portal.utilities.SearchResult; import gov.anl.aps.cdb.portal.utilities.SessionUtility; +import java.util.ArrayList; +import java.util.Collections; import java.util.Date; import java.util.LinkedList; import java.util.List; @@ -55,6 +61,7 @@ public EntityType create(EntityType entity, UserInfo createdByUserInfo) throws C addCreatedSystemLog(entity, createdByUserInfo); entity.setPersitanceErrorMessage(null); + publishMqttEvent(CdbMqttTopic.ADD, entity, createdByUserInfo); clearCaches(); return entity; @@ -79,7 +86,10 @@ public void createList(List entities, UserInfo createdByUserInfo) th } getEntityDbFacade().create(entities); - addCdbEntitySystemLog(SystemLogLevel.entityInfo, "Created " + entities.size() + " entities.", createdByUserInfo); + addCdbEntitySystemLog(SystemLogLevel.entityInfo, "Created " + entities.size() + " entities.", createdByUserInfo); + for (EntityType entity : entities) { + publishMqttEvent(CdbMqttTopic.ADD, entity, createdByUserInfo); + } setPersistenceErrorMessageForList(entities, null); clearCaches(); } catch (CdbException ex) { @@ -100,9 +110,12 @@ public EntityType update(EntityType entity, UserInfo updatedByUserInfo) throws C try { logger.debug("Updating " + getDisplayEntityTypeName() + " " + getEntityInstanceName(entity)); prepareEntityUpdate(entity, updatedByUserInfo); + List specializedEvents = prepareSpecializedUpdateEvents(entity, updatedByUserInfo); EntityType updatedEntity = getEntityDbFacade().edit(entity); addCdbEntitySystemLog(SystemLogLevel.entityInfo, "Updated: " + entity.getSystemLogString(), updatedByUserInfo); entity.setPersitanceErrorMessage(null); + publishMqttEvent(CdbMqttTopic.UPDATE, updatedEntity, updatedByUserInfo); + publishMqttEvents(specializedEvents); clearCaches(); return updatedEntity; @@ -126,7 +139,10 @@ public EntityType updateOnRemoval(EntityType entity, UserInfo updatedByUserInfo) try { logger.debug("Updating " + getDisplayEntityTypeName() + " " + getEntityInstanceName(entity)); prepareEntityUpdateOnRemoval(entity); + List specializedEvents = prepareSpecializedUpdateEvents(entity, updatedByUserInfo); EntityType updatedEntity = getEntityDbFacade().edit(entity); + publishMqttEvent(CdbMqttTopic.UPDATE, updatedEntity, updatedByUserInfo); + publishMqttEvents(specializedEvents); clearCaches(); return updatedEntity; @@ -148,15 +164,19 @@ public EntityType updateOnRemoval(EntityType entity, UserInfo updatedByUserInfo) public void updateList(List entities, UserInfo updatedByUserInfo) throws CdbException, RuntimeException { try { + List specializedEvents = new ArrayList<>(); for (EntityType entity : entities) { logger.debug("Updating " + getDisplayEntityTypeName() + " " + getEntityInstanceName(entity)); prepareEntityUpdate(entity, updatedByUserInfo); + specializedEvents.addAll(prepareSpecializedUpdateEvents(entity, updatedByUserInfo)); } getEntityDbFacade().edit(entities); for (EntityType entity : entities) { entity.setPersitanceErrorMessage(null); addCdbEntitySystemLog(SystemLogLevel.entityInfo, "Updated: " + entity.getSystemLogString(), updatedByUserInfo); + publishMqttEvent(CdbMqttTopic.UPDATE, entity, updatedByUserInfo); } + publishMqttEvents(specializedEvents); clearCaches(); } catch (CdbException ex) { logger.error("Could not update " + getDisplayEntityTypeName() + " entities: " + ex.getMessage()); @@ -177,7 +197,8 @@ public void destroy(EntityType entity, UserInfo destroyedByUserInfo) throws CdbE prepareEntityDestroy(entity, destroyedByUserInfo); getEntityDbFacade().remove(entity); - addCdbEntitySystemLog(SystemLogLevel.entityInfo, "Deleted: " + entity.getSystemLogString(), destroyedByUserInfo); + addCdbEntitySystemLog(SystemLogLevel.entityInfo, "Deleted: " + entity.getSystemLogString(), destroyedByUserInfo); + publishMqttEvent(CdbMqttTopic.DELETE, entity, destroyedByUserInfo); clearCaches(); } catch (CdbException ex) { entity.setPersitanceErrorMessage(ex.getMessage()); @@ -214,6 +235,14 @@ public void destroyList( getEntityDbFacade().remove(entities, updateEntity); addCdbEntitySystemLog(SystemLogLevel.entityInfo, "Deleted: " + entities.size() + " entities.", destroyedByUserInfo); + for (EntityType entity : entities) { + if (entity != null) { + publishMqttEvent(CdbMqttTopic.DELETE, entity, destroyedByUserInfo); + } + } + if (updateEntity != null) { + publishMqttEvent(CdbMqttTopic.UPDATE, updateEntity, destroyedByUserInfo); + } setPersistenceErrorMessageForList(entities, null); clearCaches(); } catch (CdbException ex) { @@ -229,6 +258,29 @@ public void destroyList( } } + protected List prepareSpecializedUpdateEvents( + EntityType entity, UserInfo userInfo) throws CdbException { + return Collections.emptyList(); + } + + protected void publishMqttEvents(List events) { + for (MqttEvent event : events) { + publishMqttEvent(event); + } + } + + protected void publishMqttEvent(CdbMqttTopic topic, EntityType entity, UserInfo userInfo) { + try { + publishMqttEvent(new CdbEntityEvent(topic, entity, userInfo)); + } catch (RuntimeException ex) { + logger.error("Failed to create MQTT event for " + getDisplayEntityTypeName(), ex); + } + } + + protected void publishMqttEvent(MqttEvent event) { + CdbMqttPublisher.publish(event); + } + /** * On database operation clear cache of related cached entity when needed. */ diff --git a/src/java/CdbWebPortal/src/java/gov/anl/aps/cdb/portal/controllers/utilities/ItemControllerUtility.java b/src/java/CdbWebPortal/src/java/gov/anl/aps/cdb/portal/controllers/utilities/ItemControllerUtility.java index ac9b4ed6b..90db5accd 100644 --- a/src/java/CdbWebPortal/src/java/gov/anl/aps/cdb/portal/controllers/utilities/ItemControllerUtility.java +++ b/src/java/CdbWebPortal/src/java/gov/anl/aps/cdb/portal/controllers/utilities/ItemControllerUtility.java @@ -4,12 +4,25 @@ */ package gov.anl.aps.cdb.portal.controllers.utilities; +import gov.anl.aps.cdb.base.mqtt.MqttEvent; import gov.anl.aps.cdb.common.exceptions.CdbException; import gov.anl.aps.cdb.common.exceptions.InvalidRequest; import gov.anl.aps.cdb.common.exceptions.ObjectAlreadyExists; +import gov.anl.aps.cdb.mqtt.CdbInventoryAssemblyElementEvent; +import gov.anl.aps.cdb.mqtt.CdbItemLocationEvent; +import gov.anl.aps.cdb.mqtt.CdbItemPropertyEvent; +import gov.anl.aps.cdb.mqtt.CdbMachineAssignedItemEvent; +import gov.anl.aps.cdb.mqtt.CdbMachineRepresentedElementEvent; +import gov.anl.aps.cdb.mqtt.CdbMachineTrashEvent; import gov.anl.aps.cdb.portal.constants.ItemElementRelationshipTypeNames; import gov.anl.aps.cdb.portal.constants.ListName; import gov.anl.aps.cdb.portal.controllers.PropertyTypeController; +import gov.anl.aps.cdb.mqtt.metadata.InventoryAssemblyElementChange; +import gov.anl.aps.cdb.mqtt.metadata.ItemLocationChange; +import gov.anl.aps.cdb.mqtt.metadata.MachineAssignedItemChange; +import gov.anl.aps.cdb.mqtt.metadata.MachineRepresentedElementChange; +import gov.anl.aps.cdb.mqtt.metadata.MachineTrashChange; +import gov.anl.aps.cdb.mqtt.metadata.PropertyValueChange; import gov.anl.aps.cdb.portal.model.db.beans.AllowedPropertyMetadataValueFacade; import gov.anl.aps.cdb.portal.model.db.beans.DomainFacade; import gov.anl.aps.cdb.portal.model.db.beans.ItemFacadeBase; @@ -21,6 +34,8 @@ import gov.anl.aps.cdb.portal.model.db.entities.EntityInfo; import gov.anl.aps.cdb.portal.model.db.entities.Item; import gov.anl.aps.cdb.portal.model.db.entities.ItemConnector; +import gov.anl.aps.cdb.portal.model.db.entities.ItemDomainInventoryBase; +import gov.anl.aps.cdb.portal.model.db.entities.ItemDomainMachineDesign; import gov.anl.aps.cdb.portal.model.db.entities.ItemElement; import gov.anl.aps.cdb.portal.model.db.entities.ItemElementRelationship; import gov.anl.aps.cdb.portal.model.db.entities.ItemSource; @@ -46,8 +61,10 @@ import java.util.Arrays; import java.util.Collections; import java.util.Date; +import java.util.HashSet; import java.util.List; import java.util.Objects; +import java.util.Set; import java.util.function.Function; import java.util.regex.Pattern; import org.apache.logging.log4j.LogManager; @@ -125,9 +142,13 @@ public void prepareEntityUpdate(ItemDomainEntity item, UserInfo updatedByUser) t // Prepare history Item originalItem = findById(item.getId()); + prepareMachineAssignedItemChange(originalItem, item); + prepareMachineRepresentedElementChange(originalItem, item); // Full item element list contains self element as well as all the elements List originalElementList = originalItem.getFullItemElementList(); List newElementList = item.getFullItemElementList(); + prepareInventoryAssemblyElementChanges(originalItem, item, + originalElementList, newElementList); logger.debug("Verifying elements for item " + item); ItemElementUtility.prepareItemElementHistory(originalElementList, newElementList, entityInfo); //Verify reverse hierarchy updates @@ -139,7 +160,9 @@ public void prepareEntityUpdate(ItemDomainEntity item, UserInfo updatedByUser) t List originalPropertyValueList = originalItem.getPropertyValueList(); List newPropertyValueList = item.getPropertyValueList(); logger.debug("Verifying properties for item " + item); - PropertyValueUtility.preparePropertyValueHistory(originalPropertyValueList, newPropertyValueList, entityInfo); + List propertyChanges = PropertyValueUtility.preparePropertyValueHistory( + originalPropertyValueList, newPropertyValueList, entityInfo); + item.setPendingPropertyValueChanges(propertyChanges); item.clearPropertyValueCache(); List derivedFromItemList = item.getDerivedFromItemList(); @@ -156,6 +179,147 @@ public void prepareEntityUpdate(ItemDomainEntity item, UserInfo updatedByUser) t } + protected void prepareInventoryAssemblyElementChanges(Item originalItem, Item item, + List originalElementList, List newElementList) { + if (!(item instanceof ItemDomainInventoryBase)) { + return; + } + ItemDomainInventoryBase inventoryAssembly = (ItemDomainInventoryBase) item; + for (ItemElement newElement : newElementList) { + if (newElement.equals(item.getSelfElement())) { + continue; + } + int index = originalElementList.indexOf(newElement); + Item oldAssignedItem = index < 0 + ? null : originalElementList.get(index).getContainedItem(); + Item newAssignedItem = newElement.getContainedItem(); + if (!Objects.equals(oldAssignedItem, newAssignedItem)) { + inventoryAssembly.addPendingAssemblyElementChange( + new InventoryAssemblyElementChange( + newElement, oldAssignedItem, newAssignedItem)); + } + } + } + + protected void prepareMachineAssignedItemChange(Item originalItem, Item item) { + if (item instanceof ItemDomainMachineDesign) { + ItemDomainMachineDesign machine = (ItemDomainMachineDesign) item; + ItemDomainMachineDesign originalMachine = (ItemDomainMachineDesign) originalItem; + Item oldAssignedItem = originalMachine.getAssignedItem(); + Item newAssignedItem = machine.getAssignedItem(); + if (!Objects.equals(oldAssignedItem, newAssignedItem) + || originalMachine.isIsHoused() != machine.isIsHoused()) { + machine.setPendingAssignedItemChange( + oldAssignedItem, originalMachine.isIsHoused()); + } + } + } + + protected void prepareMachineRepresentedElementChange(Item originalItem, Item item) { + if (item instanceof ItemDomainMachineDesign) { + ItemDomainMachineDesign machine = (ItemDomainMachineDesign) item; + ItemDomainMachineDesign originalMachine = (ItemDomainMachineDesign) originalItem; + if (!Objects.equals(originalMachine.getRepresentsCatalogElement(), + machine.getRepresentsCatalogElement())) { + machine.setPendingRepresentedElementChange( + originalMachine.getRepresentsCatalogElement()); + } + } + } + + @Override + protected List prepareSpecializedUpdateEvents( + ItemDomainEntity item, UserInfo userInfo) throws CdbException { + List events = new ArrayList<>(); + if (item instanceof ItemDomainInventoryBase) { + addInventoryAssemblyElementEvents(events, + (ItemDomainInventoryBase) item, userInfo, new HashSet<>()); + } + if (item instanceof LocatableItem) { + LocatableItem locatableItem = (LocatableItem) item; + ItemLocationChange locationChange = locatableItem.getPendingLocationChange(); + if (locationChange != null) { + events.add(new CdbItemLocationEvent(locatableItem, + locationChange.getOldLocation(), locationChange.getNewLocation(), + locationChange.getOldLocationDetails(), + locationChange.getNewLocationDetails(), userInfo)); + } + } + if (item instanceof ItemDomainMachineDesign) { + ItemDomainMachineDesign machine = (ItemDomainMachineDesign) item; + MachineAssignedItemChange change = machine.getPendingAssignedItemChange(); + if (change != null) { + events.add(new CdbMachineAssignedItemEvent(machine, + change.getOldAssignedItem(), machine.getAssignedItem(), + change.isOldInstalled(), machine.isIsHoused(), userInfo)); + } + addRepresentedElementEvents(events, machine, userInfo); + MachineTrashChange trashChange = machine.getPendingTrashChange(); + if (trashChange != null) { + events.add(new CdbMachineTrashEvent(trashChange.getTopic(), machine, + trashChange.getRequestedHierarchyRoot(), + trashChange.getAffectedMachineCount(), userInfo)); + } + } + for (PropertyValueChange change : item.getPendingPropertyValueChanges()) { + switch (change.getType()) { + case ADD: + events.add(CdbItemPropertyEvent.added(item, change.getNewValue(), userInfo)); + break; + case UPDATE: + events.add(CdbItemPropertyEvent.updated(item, + change.getOldValue(), change.getNewValue(), userInfo)); + break; + case DELETE: + events.add(CdbItemPropertyEvent.deleted(item, change.getOldValue(), userInfo)); + break; + default: + throw new IllegalStateException("Unsupported property change type"); + } + } + return events; + } + + private void addInventoryAssemblyElementEvents(List events, + ItemDomainInventoryBase inventoryAssembly, UserInfo userInfo, + Set visited) { + if (!visited.add(inventoryAssembly)) { + return; + } + for (Object value : inventoryAssembly.getPendingAssemblyElementChanges()) { + InventoryAssemblyElementChange change = (InventoryAssemblyElementChange) value; + events.add(new CdbInventoryAssemblyElementEvent(inventoryAssembly, + change.getAssemblyElement(), change.getOldAssignedInventoryItem(), + change.getNewAssignedInventoryItem(), userInfo)); + if (change.getNewAssignedInventoryItem() instanceof ItemDomainInventoryBase) { + addInventoryAssemblyElementEvents(events, + (ItemDomainInventoryBase) change.getNewAssignedInventoryItem(), + userInfo, visited); + } + } + } + + private void addRepresentedElementEvents(List events, + ItemDomainMachineDesign machine, UserInfo userInfo) { + MachineRepresentedElementChange change = machine.getPendingRepresentedElementChange(); + if (change != null) { + events.add(new CdbMachineRepresentedElementEvent(machine, + change.getOldRepresentedElement(), machine.getRepresentsCatalogElement(), + change.getSourceTemplate(), userInfo)); + } + for (ItemDomainMachineDesign itemToUpdate : machine.getItemsToUpdate()) { + addRepresentedElementEvents(events, itemToUpdate, userInfo); + } + } + + @Override + protected void prepareEntityUpdateOnRemoval(ItemDomainEntity item) throws CdbException { + Item originalItem = findById(item.getId()); + List propertyChanges = PropertyValueUtility.findRemovedPropertyValues( + originalItem.getPropertyValueList(), item.getPropertyValueList()); + item.setPendingPropertyValueChanges(propertyChanges); + } + @Override protected void prepareEntityDestroy(ItemDomainEntity item, UserInfo userInfo) throws CdbException { super.prepareEntityDestroy(item, userInfo); diff --git a/src/java/CdbWebPortal/src/java/gov/anl/aps/cdb/portal/controllers/utilities/ItemDomainMachineDesignBaseControllerUtility.java b/src/java/CdbWebPortal/src/java/gov/anl/aps/cdb/portal/controllers/utilities/ItemDomainMachineDesignBaseControllerUtility.java index ea9a828a6..26769b174 100644 --- a/src/java/CdbWebPortal/src/java/gov/anl/aps/cdb/portal/controllers/utilities/ItemDomainMachineDesignBaseControllerUtility.java +++ b/src/java/CdbWebPortal/src/java/gov/anl/aps/cdb/portal/controllers/utilities/ItemDomainMachineDesignBaseControllerUtility.java @@ -8,6 +8,7 @@ import gov.anl.aps.cdb.common.exceptions.InvalidArgument; import gov.anl.aps.cdb.common.exceptions.InvalidObjectState; import gov.anl.aps.cdb.common.utilities.ObjectUtility; +import gov.anl.aps.cdb.mqtt.CdbMachineMoveEvent; import gov.anl.aps.cdb.portal.constants.EntityTypeName; import gov.anl.aps.cdb.portal.constants.ItemDomainName; import gov.anl.aps.cdb.portal.constants.ItemElementRelationshipTypeNames; @@ -32,6 +33,7 @@ import java.util.ArrayList; import java.util.LinkedList; import java.util.List; +import java.util.Objects; import org.apache.logging.log4j.LogManager; import org.apache.logging.log4j.Logger; import org.primefaces.model.DefaultTreeNode; @@ -342,8 +344,13 @@ public ItemElement performMachineMove( public ItemElement performMachineMoveWithUpdate( ItemDomainMachineDesign newParent, ItemDomainMachineDesign child, UserInfo sessionUser) throws CdbException { + ItemDomainMachineDesign oldParent = child.getParentMachineDesign(); + if (Objects.equals(oldParent, newParent)) { + return child.getParentMachineElement(); + } ItemElement currentItemElement = performMachineMove(newParent, child, sessionUser); update(newParent, sessionUser); + publishMqttEvent(new CdbMachineMoveEvent(child, oldParent, newParent, sessionUser)); return currentItemElement; } @@ -849,7 +856,14 @@ private void updateRepresentingAssemblyElementForMachine(ItemDomainMachineDesign } } + ItemElement oldRepresentedElement = node.getRepresentsCatalogElement(); node.setRepresentsCatalogElement(representedElement); + if (updateFromTemplate && !Objects.equals(oldRepresentedElement, representedElement)) { + Item template = node.getCreatedFromTemplate(); + node.setPendingRepresentedElementChange(oldRepresentedElement, + template instanceof ItemDomainMachineDesign + ? (ItemDomainMachineDesign) template : null); + } if (node.getIsItemTemplate()) { List itemsCreatedFromThisTemplateItem diff --git a/src/java/CdbWebPortal/src/java/gov/anl/aps/cdb/portal/model/db/entities/Item.java b/src/java/CdbWebPortal/src/java/gov/anl/aps/cdb/portal/model/db/entities/Item.java index c10f3c817..9473af397 100644 --- a/src/java/CdbWebPortal/src/java/gov/anl/aps/cdb/portal/model/db/entities/Item.java +++ b/src/java/CdbWebPortal/src/java/gov/anl/aps/cdb/portal/model/db/entities/Item.java @@ -20,6 +20,7 @@ import gov.anl.aps.cdb.portal.model.db.beans.PropertyTypeFacade; import gov.anl.aps.cdb.portal.model.db.entities.comparator.ItemElementRelevantSortOrderComparator; import gov.anl.aps.cdb.portal.model.db.utilities.ItemElementUtility; +import gov.anl.aps.cdb.mqtt.metadata.PropertyValueChange; import gov.anl.aps.cdb.portal.utilities.SearchResult; import gov.anl.aps.cdb.portal.view.objects.ItemMetadataFieldInfo; import gov.anl.aps.cdb.portal.view.objects.ItemMetadataPropertyInfo; @@ -504,6 +505,8 @@ ) public class Item extends CdbDomainEntity implements Serializable { + private transient List pendingPropertyValueChanges = new ArrayList<>(); + private static final Logger LOGGER = LogManager.getLogger(Item.class.getName()); public static final String ATTRIBUTE_DOMAIN_NAME = "domainName"; @@ -1595,6 +1598,17 @@ public void setPropertyValueList(List propertyValueList) { this.getSelfElement().setPropertyValueList(propertyValueList); } + @JsonIgnore + public void setPendingPropertyValueChanges(List changes) { + pendingPropertyValueChanges = changes == null ? new ArrayList<>() : new ArrayList<>(changes); + } + + @JsonIgnore + public List getPendingPropertyValueChanges() { + return pendingPropertyValueChanges == null + ? new ArrayList<>() : new ArrayList<>(pendingPropertyValueChanges); + } + @Override public List getPropertyValueDisplayList() { if (propertyValueDisplayList == null) { diff --git a/src/java/CdbWebPortal/src/java/gov/anl/aps/cdb/portal/model/db/entities/ItemDomainInventoryBase.java b/src/java/CdbWebPortal/src/java/gov/anl/aps/cdb/portal/model/db/entities/ItemDomainInventoryBase.java index 317ce6a4b..cbd007308 100644 --- a/src/java/CdbWebPortal/src/java/gov/anl/aps/cdb/portal/model/db/entities/ItemDomainInventoryBase.java +++ b/src/java/CdbWebPortal/src/java/gov/anl/aps/cdb/portal/model/db/entities/ItemDomainInventoryBase.java @@ -6,10 +6,12 @@ import com.fasterxml.jackson.annotation.JsonIgnore; import gov.anl.aps.cdb.common.exceptions.CdbException; +import gov.anl.aps.cdb.mqtt.metadata.InventoryAssemblyElementChange; import gov.anl.aps.cdb.portal.import_export.import_.objects.ValidInfo; import gov.anl.aps.cdb.portal.model.db.utilities.ItemElementUtility; import gov.anl.aps.cdb.portal.model.jsf.beans.SparePartsBean; import gov.anl.aps.cdb.portal.view.objects.InventoryBillOfMaterialItem; +import java.util.ArrayList; import java.util.List; import org.primefaces.model.TreeNode; @@ -22,6 +24,7 @@ public abstract class ItemDomainInventoryBase inventoryDomainBillOfMaterialList = null; + private transient List pendingAssemblyElementChanges = new ArrayList<>(); private transient InventoryBillOfMaterialItem containedInBOM; @@ -56,6 +59,16 @@ public List getInventoryDomainBillOfMaterialList() public void setInventoryDomainBillOfMaterialList(List inventoryDomainBillOfMaterialList) { this.inventoryDomainBillOfMaterialList = inventoryDomainBillOfMaterialList; } + + @JsonIgnore + public void addPendingAssemblyElementChange(InventoryAssemblyElementChange change) { + pendingAssemblyElementChanges.add(change); + } + + @JsonIgnore + public List getPendingAssemblyElementChanges() { + return new ArrayList<>(pendingAssemblyElementChanges); + } @JsonIgnore public abstract String generateUnitName(int itemNumber); diff --git a/src/java/CdbWebPortal/src/java/gov/anl/aps/cdb/portal/model/db/entities/ItemDomainMachineDesign.java b/src/java/CdbWebPortal/src/java/gov/anl/aps/cdb/portal/model/db/entities/ItemDomainMachineDesign.java index 01f1d0492..5ded0971a 100644 --- a/src/java/CdbWebPortal/src/java/gov/anl/aps/cdb/portal/model/db/entities/ItemDomainMachineDesign.java +++ b/src/java/CdbWebPortal/src/java/gov/anl/aps/cdb/portal/model/db/entities/ItemDomainMachineDesign.java @@ -23,6 +23,9 @@ import gov.anl.aps.cdb.portal.controllers.utilities.ItemDomainMachineDesignIOCControllerUtility; import gov.anl.aps.cdb.portal.controllers.utilities.ItemDomainMachineDesignInventoryControllerUtility; import gov.anl.aps.cdb.portal.controllers.utilities.ItemDomainMachineDesignPowerControllerUtility; +import gov.anl.aps.cdb.mqtt.metadata.MachineAssignedItemChange; +import gov.anl.aps.cdb.mqtt.metadata.MachineRepresentedElementChange; +import gov.anl.aps.cdb.mqtt.metadata.MachineTrashChange; import gov.anl.aps.cdb.portal.utilities.SearchResult; import gov.anl.aps.cdb.portal.view.objects.KeyValueObject; import gov.anl.aps.cdb.portal.view.objects.MachineDesignConnectorListObject; @@ -63,6 +66,9 @@ public class ItemDomainMachineDesign extends LocatableStatusItem { private transient String importTemplateAndParameters = null; private transient Float importSortOrder = null; private transient String moveToTrashErrorMsg = null; + private transient MachineAssignedItemChange pendingAssignedItemChange; + private transient MachineRepresentedElementChange pendingRepresentedElementChange; + private transient MachineTrashChange pendingTrashChange; private transient String moveToTrashWarningMsg = null; private transient ItemElement currentHierarchyItemElement; @@ -573,6 +579,16 @@ public void setAssignedItem(Item item) { selfElement.setContainedItem2(item); } + @JsonIgnore + public void setPendingAssignedItemChange(Item oldAssignedItem, boolean oldInstalled) { + pendingAssignedItemChange = new MachineAssignedItemChange(oldAssignedItem, oldInstalled); + } + + @JsonIgnore + public MachineAssignedItemChange getPendingAssignedItemChange() { + return pendingAssignedItemChange; + } + @JsonIgnore public String getAssignedItemString() { if (getAssignedItem() != null) { @@ -597,6 +613,31 @@ public void setRepresentsCatalogElement(ItemElement representsCatalogElement) { selfElement.setRepresentsItemElement(representsCatalogElement); } + @JsonIgnore + public void setPendingRepresentedElementChange(ItemElement oldRepresentedElement) { + pendingRepresentedElementChange = new MachineRepresentedElementChange(oldRepresentedElement); + } + + @JsonIgnore + public void setPendingRepresentedElementChange(ItemElement oldRepresentedElement, + ItemDomainMachineDesign sourceTemplate) { + pendingRepresentedElementChange = new MachineRepresentedElementChange( + oldRepresentedElement, sourceTemplate); + } + + @JsonIgnore + public MachineRepresentedElementChange getPendingRepresentedElementChange() { + return pendingRepresentedElementChange; + } + + @JsonIgnore + public void setPendingTrashChange(MachineTrashChange pendingTrashChange) { + this.pendingTrashChange = pendingTrashChange; + } + + @JsonIgnore + public MachineTrashChange getPendingTrashChange() { return pendingTrashChange; } + public boolean isIsHoused() { ItemElement selfElement = getSelfElement(); return selfElement.getIsHoused(); diff --git a/src/java/CdbWebPortal/src/java/gov/anl/aps/cdb/portal/model/db/entities/LocatableItem.java b/src/java/CdbWebPortal/src/java/gov/anl/aps/cdb/portal/model/db/entities/LocatableItem.java index ea47a9e45..d105e5c42 100644 --- a/src/java/CdbWebPortal/src/java/gov/anl/aps/cdb/portal/model/db/entities/LocatableItem.java +++ b/src/java/CdbWebPortal/src/java/gov/anl/aps/cdb/portal/model/db/entities/LocatableItem.java @@ -5,6 +5,7 @@ package gov.anl.aps.cdb.portal.model.db.entities; import com.fasterxml.jackson.annotation.JsonIgnore; +import gov.anl.aps.cdb.mqtt.metadata.ItemLocationChange; import gov.anl.aps.cdb.portal.controllers.LocatableItemController; import gov.anl.aps.cdb.portal.view.objects.LocationHistoryObject; import java.util.List; @@ -33,6 +34,7 @@ public abstract class LocatableItem extends Item { private transient boolean loadedImportLocationItem = false; private transient boolean loadedImportLocationDetails = false; private transient List locationHistoryListObject = null; + private transient ItemLocationChange pendingLocationChange; // Needed to determine whenever location was removed in edit process. private transient Boolean originalLocationLoaded = false; @@ -245,4 +247,12 @@ public void setLocationHistoryListObject(List locationHis this.locationHistoryListObject = locationHistoryListObject; } + @JsonIgnore + public void setPendingLocationChange(ItemLocationChange pendingLocationChange) { + this.pendingLocationChange = pendingLocationChange; + } + + @JsonIgnore + public ItemLocationChange getPendingLocationChange() { return pendingLocationChange; } + } diff --git a/src/java/CdbWebPortal/src/java/gov/anl/aps/cdb/portal/model/db/utilities/PropertyValueUtility.java b/src/java/CdbWebPortal/src/java/gov/anl/aps/cdb/portal/model/db/utilities/PropertyValueUtility.java index 2239b7db0..79db0f7b9 100644 --- a/src/java/CdbWebPortal/src/java/gov/anl/aps/cdb/portal/model/db/utilities/PropertyValueUtility.java +++ b/src/java/CdbWebPortal/src/java/gov/anl/aps/cdb/portal/model/db/utilities/PropertyValueUtility.java @@ -4,6 +4,7 @@ */ package gov.anl.aps.cdb.portal.model.db.utilities; +import gov.anl.aps.cdb.mqtt.metadata.PropertyValueChange; import gov.anl.aps.cdb.portal.constants.DisplayType; import gov.anl.aps.cdb.portal.model.db.entities.AllowedPropertyValue; import gov.anl.aps.cdb.portal.model.db.entities.EntityInfo; @@ -16,6 +17,7 @@ import gov.anl.aps.cdb.portal.utilities.SearchResult; import java.util.ArrayList; import java.util.List; +import java.util.Objects; import java.util.regex.Pattern; import org.apache.logging.log4j.LogManager; import org.apache.logging.log4j.Logger; @@ -78,14 +80,16 @@ public static List prepareImagePropertyValueList(List originalPropertyValueList, + public static List preparePropertyValueHistory(List originalPropertyValueList, List newPropertyValueList, EntityInfo entityInfo) { + List changes = new ArrayList<>(); for (PropertyValue newPropertyValue : newPropertyValueList) { int index = originalPropertyValueList.indexOf(newPropertyValue); if (index >= 0) { // Original property was there. PropertyValue originalPropertyValue = originalPropertyValueList.get(index); - if (!newPropertyValue.equalsByTagAndValueAndUnitsAndDescriptionAndMetadata(originalPropertyValue)) { + if (!newPropertyValue.equalsByTagAndValueAndUnitsAndDescriptionAndMetadata(originalPropertyValue) + || !Objects.equals(newPropertyValue.getText(), originalPropertyValue.getText())) { // Property value was modified. logger.debug("Property value for type " + originalPropertyValue.getPropertyType() + " was modified (original value: " + originalPropertyValue + "; new value: " + newPropertyValue + ")"); @@ -97,6 +101,7 @@ public static void preparePropertyValueHistory(List originalPrope PropertyValueHistory propertyValueHistory = new PropertyValueHistory(); propertyValueHistory.updateFromPropertyValue(originalPropertyValue); propertyValueHistoryList.add(propertyValueHistory); + changes.add(PropertyValueChange.updated(originalPropertyValue, newPropertyValue)); } } else { // New property value. @@ -104,9 +109,25 @@ public static void preparePropertyValueHistory(List originalPrope + ": " + newPropertyValue); newPropertyValue.setEnteredByUser(entityInfo.getLastModifiedByUser()); newPropertyValue.setEnteredOnDateTime(entityInfo.getLastModifiedOnDateTime()); + changes.add(PropertyValueChange.added(newPropertyValue)); } } + changes.addAll(findRemovedPropertyValues( + originalPropertyValueList, newPropertyValueList)); + return changes; + } + + public static List findRemovedPropertyValues( + List originalPropertyValueList, + List newPropertyValueList) { + List changes = new ArrayList<>(); + for (PropertyValue originalPropertyValue : originalPropertyValueList) { + if (!newPropertyValueList.contains(originalPropertyValue)) { + changes.add(PropertyValueChange.deleted(originalPropertyValue)); + } + } + return changes; } public static PropertyTypeHandlerInterface getPropertyTypeHandler(PropertyValue propertyValue) { diff --git a/src/lib/mqtt-rar-0.8.0.rar b/src/lib/mqtt-rar-0.8.0.rar new file mode 100644 index 000000000..5f21fdcae Binary files /dev/null and b/src/lib/mqtt-rar-0.8.0.rar differ diff --git a/tools/developer_tools/cdb-base-mqtt/.gitignore b/tools/developer_tools/cdb-base-mqtt/.gitignore new file mode 100644 index 000000000..2f7896d1d --- /dev/null +++ b/tools/developer_tools/cdb-base-mqtt/.gitignore @@ -0,0 +1 @@ +target/ diff --git a/tools/developer_tools/cdb-base-mqtt/README.md b/tools/developer_tools/cdb-base-mqtt/README.md new file mode 100644 index 000000000..7a00f06e6 --- /dev/null +++ b/tools/developer_tools/cdb-base-mqtt/README.md @@ -0,0 +1,29 @@ +# CDB Base MQTT + +Shared Java 8 MQTT event base class, publishing API, and Payara JCA/JNDI transport used by ComponentDB and BELY. + +`gov.anl.aps.cdb.base.mqtt.MqttEvent` owns the event timestamp and requires applications to supply a string topic. Application-specific payload fields and topic definitions remain in each portal. + +## Build and test + +A JDK and Maven are required. + +```sh +mvn clean verify +sha256sum -c SHA256SUMS +``` + +The versioned thin JAR is written to `target/cdb-base-mqtt-1.0.0.jar`. Jackson is provided by each consuming portal and is intentionally not bundled. + +GitHub Actions runs `mvn verify` on pushes and pull requests that change this module or its workflow. It also supports manual runs and uploads the Surefire reports as a workflow artifact. + +## Release artifact + +The checked-in `SHA256SUMS` records the released JAR checksum and verifies the artifact under `target/`. After changing this library: + +1. Increment the version in `pom.xml`. +2. Run `mvn clean verify`. +3. Update `SHA256SUMS` with the checksum of the generated JAR. +4. Copy that exact JAR into each portal's checked-in `lib/` directory. +5. Register the versioned JAR in each portal's NetBeans Ant classpath. +6. Verify each copied JAR with `sha256sum` before building the portals. diff --git a/tools/developer_tools/cdb-base-mqtt/SHA256SUMS b/tools/developer_tools/cdb-base-mqtt/SHA256SUMS new file mode 100644 index 000000000..c381ed494 --- /dev/null +++ b/tools/developer_tools/cdb-base-mqtt/SHA256SUMS @@ -0,0 +1 @@ +42a4db3b8f35d7a3785849c0f3365af9f5dec7cffa915899c460c5f90e5e8819 target/cdb-base-mqtt-1.0.0.jar diff --git a/tools/developer_tools/cdb-base-mqtt/pom.xml b/tools/developer_tools/cdb-base-mqtt/pom.xml new file mode 100644 index 000000000..3292ba2fe --- /dev/null +++ b/tools/developer_tools/cdb-base-mqtt/pom.xml @@ -0,0 +1,56 @@ + + 4.0.0 + + gov.anl.aps + cdb-base-mqtt + 1.0.0 + + + 1.8 + 1.8 + 2026-09-30T00:00:00Z + UTF-8 + 5.10.3 + + + + + com.fasterxml.jackson.core + jackson-annotations + 2.11.2 + provided + + + com.fasterxml.jackson.core + jackson-databind + 2.11.2 + provided + + + org.junit.jupiter + junit-jupiter + ${junit.version} + test + + + + + + + org.apache.maven.plugins + maven-compiler-plugin + 3.13.0 + + 8 + + + + org.apache.maven.plugins + maven-surefire-plugin + 3.2.5 + + + + diff --git a/tools/developer_tools/cdb-base-mqtt/src/main/java/gov/anl/aps/cdb/base/mqtt/MqttEvent.java b/tools/developer_tools/cdb-base-mqtt/src/main/java/gov/anl/aps/cdb/base/mqtt/MqttEvent.java new file mode 100644 index 000000000..6a3482bc1 --- /dev/null +++ b/tools/developer_tools/cdb-base-mqtt/src/main/java/gov/anl/aps/cdb/base/mqtt/MqttEvent.java @@ -0,0 +1,26 @@ +/* + * Copyright (c) UChicago Argonne, LLC. All rights reserved. + * See LICENSE file. + */ +package gov.anl.aps.cdb.base.mqtt; + +import com.fasterxml.jackson.annotation.JsonFormat; +import com.fasterxml.jackson.annotation.JsonIgnore; +import java.util.Date; + +/** Base for an application-owned payload and its destination topic. */ +public abstract class MqttEvent { + private final Date eventTimestamp; + + protected MqttEvent() { + eventTimestamp = new Date(); + } + + @JsonIgnore + public abstract String getTopic(); + + @JsonFormat(shape = JsonFormat.Shape.STRING) + public Date getEventTimestamp() { + return new Date(eventTimestamp.getTime()); + } +} diff --git a/tools/developer_tools/cdb-base-mqtt/src/main/java/gov/anl/aps/cdb/base/mqtt/MqttPublisher.java b/tools/developer_tools/cdb-base-mqtt/src/main/java/gov/anl/aps/cdb/base/mqtt/MqttPublisher.java new file mode 100644 index 000000000..873d73a07 --- /dev/null +++ b/tools/developer_tools/cdb-base-mqtt/src/main/java/gov/anl/aps/cdb/base/mqtt/MqttPublisher.java @@ -0,0 +1,11 @@ +/* + * Copyright (c) UChicago Argonne, LLC. All rights reserved. + * See LICENSE file. + */ +package gov.anl.aps.cdb.base.mqtt; + +import gov.anl.aps.cdb.base.mqtt.exception.MqttException; + +public interface MqttPublisher { + void publish(MqttEvent event) throws MqttException; +} diff --git a/tools/developer_tools/cdb-base-mqtt/src/main/java/gov/anl/aps/cdb/base/mqtt/exception/MqttException.java b/tools/developer_tools/cdb-base-mqtt/src/main/java/gov/anl/aps/cdb/base/mqtt/exception/MqttException.java new file mode 100644 index 000000000..91982bbe0 --- /dev/null +++ b/tools/developer_tools/cdb-base-mqtt/src/main/java/gov/anl/aps/cdb/base/mqtt/exception/MqttException.java @@ -0,0 +1,10 @@ +/* + * Copyright (c) UChicago Argonne, LLC. All rights reserved. + * See LICENSE file. + */ +package gov.anl.aps.cdb.base.mqtt.exception; + +public abstract class MqttException extends Exception { + protected MqttException(String message) { super(message); } + protected MqttException(String message, Throwable cause) { super(message, cause); } +} diff --git a/tools/developer_tools/cdb-base-mqtt/src/main/java/gov/anl/aps/cdb/base/mqtt/exception/MqttNotConfiguredException.java b/tools/developer_tools/cdb-base-mqtt/src/main/java/gov/anl/aps/cdb/base/mqtt/exception/MqttNotConfiguredException.java new file mode 100644 index 000000000..dbefec5be --- /dev/null +++ b/tools/developer_tools/cdb-base-mqtt/src/main/java/gov/anl/aps/cdb/base/mqtt/exception/MqttNotConfiguredException.java @@ -0,0 +1,10 @@ +/* + * Copyright (c) UChicago Argonne, LLC. All rights reserved. + * See LICENSE file. + */ +package gov.anl.aps.cdb.base.mqtt.exception; + +public class MqttNotConfiguredException extends MqttException { + public MqttNotConfiguredException(String message) { super(message); } + public MqttNotConfiguredException(String message, Throwable cause) { super(message, cause); } +} diff --git a/tools/developer_tools/cdb-base-mqtt/src/main/java/gov/anl/aps/cdb/base/mqtt/exception/MqttPublishException.java b/tools/developer_tools/cdb-base-mqtt/src/main/java/gov/anl/aps/cdb/base/mqtt/exception/MqttPublishException.java new file mode 100644 index 000000000..529ea5025 --- /dev/null +++ b/tools/developer_tools/cdb-base-mqtt/src/main/java/gov/anl/aps/cdb/base/mqtt/exception/MqttPublishException.java @@ -0,0 +1,9 @@ +/* + * Copyright (c) UChicago Argonne, LLC. All rights reserved. + * See LICENSE file. + */ +package gov.anl.aps.cdb.base.mqtt.exception; + +public class MqttPublishException extends MqttException { + public MqttPublishException(String message, Throwable cause) { super(message, cause); } +} diff --git a/tools/developer_tools/cdb-base-mqtt/src/main/java/gov/anl/aps/cdb/base/mqtt/exception/MqttSerializationException.java b/tools/developer_tools/cdb-base-mqtt/src/main/java/gov/anl/aps/cdb/base/mqtt/exception/MqttSerializationException.java new file mode 100644 index 000000000..19ad831ed --- /dev/null +++ b/tools/developer_tools/cdb-base-mqtt/src/main/java/gov/anl/aps/cdb/base/mqtt/exception/MqttSerializationException.java @@ -0,0 +1,9 @@ +/* + * Copyright (c) UChicago Argonne, LLC. All rights reserved. + * See LICENSE file. + */ +package gov.anl.aps.cdb.base.mqtt.exception; + +public class MqttSerializationException extends MqttException { + public MqttSerializationException(String message, Throwable cause) { super(message, cause); } +} diff --git a/tools/developer_tools/cdb-base-mqtt/src/main/java/gov/anl/aps/cdb/base/mqtt/jca/JcaMqttPublisher.java b/tools/developer_tools/cdb-base-mqtt/src/main/java/gov/anl/aps/cdb/base/mqtt/jca/JcaMqttPublisher.java new file mode 100644 index 000000000..76bac90f1 --- /dev/null +++ b/tools/developer_tools/cdb-base-mqtt/src/main/java/gov/anl/aps/cdb/base/mqtt/jca/JcaMqttPublisher.java @@ -0,0 +1,39 @@ +/* + * Copyright (c) UChicago Argonne, LLC. All rights reserved. + * See LICENSE file. + */ +package gov.anl.aps.cdb.base.mqtt.jca; + +import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.databind.ObjectMapper; +import gov.anl.aps.cdb.base.mqtt.MqttEvent; +import gov.anl.aps.cdb.base.mqtt.MqttPublisher; +import gov.anl.aps.cdb.base.mqtt.exception.MqttException; +import gov.anl.aps.cdb.base.mqtt.exception.MqttPublishException; +import gov.anl.aps.cdb.base.mqtt.exception.MqttSerializationException; +import java.nio.charset.StandardCharsets; + +public class JcaMqttPublisher implements MqttPublisher { + private final MqttConnectionProvider connections; + private final ObjectMapper mapper; + private final int qos; + private final boolean retained; + + public JcaMqttPublisher(MqttConnectionProvider connections, ObjectMapper mapper) { this(connections, mapper, 0, false); } + public JcaMqttPublisher(MqttConnectionProvider connections, ObjectMapper mapper, int qos, boolean retained) { + if (connections == null || mapper == null) throw new IllegalArgumentException("connections and mapper are required"); + if (qos < 0 || qos > 2) throw new IllegalArgumentException("qos must be between 0 and 2"); + this.connections = connections; this.mapper = mapper; this.qos = qos; this.retained = retained; + } + + @Override public void publish(MqttEvent event) throws MqttException { + if (event == null || event.getTopic() == null || event.getTopic().trim().isEmpty()) throw new IllegalArgumentException("event topic is required"); + final byte[] payload; + try { payload = mapper.writeValueAsString(event).getBytes(StandardCharsets.UTF_8); } + catch (JsonProcessingException ex) { throw new MqttSerializationException("Failed to serialize MQTT event", ex); } + try (MqttConnection connection = connections.getConnection()) { + connection.publish(event.getTopic(), payload, qos, retained); + } catch (MqttException ex) { throw ex; } + catch (Exception ex) { throw new MqttPublishException("Failed to publish MQTT event to " + event.getTopic(), ex); } + } +} diff --git a/tools/developer_tools/cdb-base-mqtt/src/main/java/gov/anl/aps/cdb/base/mqtt/jca/JndiMqttConnectionProvider.java b/tools/developer_tools/cdb-base-mqtt/src/main/java/gov/anl/aps/cdb/base/mqtt/jca/JndiMqttConnectionProvider.java new file mode 100644 index 000000000..cfd83e7e8 --- /dev/null +++ b/tools/developer_tools/cdb-base-mqtt/src/main/java/gov/anl/aps/cdb/base/mqtt/jca/JndiMqttConnectionProvider.java @@ -0,0 +1,61 @@ +/* + * Copyright (c) UChicago Argonne, LLC. All rights reserved. + * See LICENSE file. + */ +package gov.anl.aps.cdb.base.mqtt.jca; + +import gov.anl.aps.cdb.base.mqtt.exception.MqttNotConfiguredException; +import java.lang.reflect.InvocationTargetException; +import java.lang.reflect.Method; +import javax.naming.InitialContext; +import javax.naming.NamingException; + +/** Looks up a Payara MQTT JCA factory without binding consumers to its API version. */ +public class JndiMqttConnectionProvider implements MqttConnectionProvider { + private final String jndiName; + + public JndiMqttConnectionProvider(String jndiName) { + if (jndiName == null || jndiName.trim().isEmpty()) throw new IllegalArgumentException("jndiName is required"); + this.jndiName = jndiName; + } + + @Override public MqttConnection getConnection() throws MqttNotConfiguredException { + try { + Object factory = new InitialContext().lookup(jndiName); + Object connection = factory.getClass().getMethod("getConnection").invoke(factory); + if (connection == null) { + throw new MqttNotConfiguredException( + "MQTT resource could not connect to its broker: " + jndiName, null); + } + return new ReflectiveConnection(connection); + } catch (NamingException | ReflectiveOperationException | LinkageError ex) { + throw new MqttNotConfiguredException("MQTT resource is unavailable: " + jndiName, unwrap(ex)); + } + } + + private static Throwable unwrap(Throwable error) { + return error instanceof InvocationTargetException && error.getCause() != null ? error.getCause() : error; + } + + private static final class ReflectiveConnection implements MqttConnection { + private final Object delegate; + private final Method publish; + private final Method close; + ReflectiveConnection(Object delegate) throws NoSuchMethodException { + this.delegate = delegate; + publish = delegate.getClass().getMethod("publish", String.class, byte[].class, int.class, boolean.class); + close = delegate.getClass().getMethod("close"); + } + @Override public void publish(String topic, byte[] payload, int qos, boolean retained) throws Exception { + try { publish.invoke(delegate, topic, payload, qos, retained); } + catch (InvocationTargetException ex) { throw asException(unwrap(ex)); } + } + @Override public void close() throws Exception { + try { close.invoke(delegate); } + catch (InvocationTargetException ex) { throw asException(unwrap(ex)); } + } + private static Exception asException(Throwable error) { + return error instanceof Exception ? (Exception) error : new Exception(error); + } + } +} diff --git a/tools/developer_tools/cdb-base-mqtt/src/main/java/gov/anl/aps/cdb/base/mqtt/jca/MqttConnection.java b/tools/developer_tools/cdb-base-mqtt/src/main/java/gov/anl/aps/cdb/base/mqtt/jca/MqttConnection.java new file mode 100644 index 000000000..19741d331 --- /dev/null +++ b/tools/developer_tools/cdb-base-mqtt/src/main/java/gov/anl/aps/cdb/base/mqtt/jca/MqttConnection.java @@ -0,0 +1,10 @@ +/* + * Copyright (c) UChicago Argonne, LLC. All rights reserved. + * See LICENSE file. + */ +package gov.anl.aps.cdb.base.mqtt.jca; + +public interface MqttConnection extends AutoCloseable { + void publish(String topic, byte[] payload, int qos, boolean retained) throws Exception; + @Override void close() throws Exception; +} diff --git a/tools/developer_tools/cdb-base-mqtt/src/main/java/gov/anl/aps/cdb/base/mqtt/jca/MqttConnectionProvider.java b/tools/developer_tools/cdb-base-mqtt/src/main/java/gov/anl/aps/cdb/base/mqtt/jca/MqttConnectionProvider.java new file mode 100644 index 000000000..e2209bfa9 --- /dev/null +++ b/tools/developer_tools/cdb-base-mqtt/src/main/java/gov/anl/aps/cdb/base/mqtt/jca/MqttConnectionProvider.java @@ -0,0 +1,11 @@ +/* + * Copyright (c) UChicago Argonne, LLC. All rights reserved. + * See LICENSE file. + */ +package gov.anl.aps.cdb.base.mqtt.jca; + +import gov.anl.aps.cdb.base.mqtt.exception.MqttNotConfiguredException; + +public interface MqttConnectionProvider { + MqttConnection getConnection() throws MqttNotConfiguredException; +} diff --git a/tools/developer_tools/cdb-base-mqtt/src/test/java/gov/anl/aps/cdb/base/mqtt/JcaMqttPublisherTest.java b/tools/developer_tools/cdb-base-mqtt/src/test/java/gov/anl/aps/cdb/base/mqtt/JcaMqttPublisherTest.java new file mode 100644 index 000000000..9cb4142b7 --- /dev/null +++ b/tools/developer_tools/cdb-base-mqtt/src/test/java/gov/anl/aps/cdb/base/mqtt/JcaMqttPublisherTest.java @@ -0,0 +1,56 @@ +/* + * Copyright (c) UChicago Argonne, LLC. All rights reserved. + * See LICENSE file. + */ +package gov.anl.aps.cdb.base.mqtt; + +import com.fasterxml.jackson.databind.ObjectMapper; +import gov.anl.aps.cdb.base.mqtt.exception.MqttNotConfiguredException; +import gov.anl.aps.cdb.base.mqtt.exception.MqttPublishException; +import gov.anl.aps.cdb.base.mqtt.exception.MqttSerializationException; +import gov.anl.aps.cdb.base.mqtt.jca.JcaMqttPublisher; +import gov.anl.aps.cdb.base.mqtt.jca.MqttConnection; +import gov.anl.aps.cdb.base.mqtt.jca.MqttConnectionProvider; +import java.nio.charset.StandardCharsets; +import org.junit.jupiter.api.Test; +import static org.junit.jupiter.api.Assertions.*; + +class JcaMqttPublisherTest { + @Test void publishesConfiguredArgumentsAndCloses() throws Exception { + FakeConnection connection = new FakeConnection(); + JcaMqttPublisher publisher = new JcaMqttPublisher(() -> connection, new ObjectMapper(), 1, true); + publisher.publish(new Event("value")); + assertEquals("cdb/add", connection.topic); + String payload = new String(connection.payload, StandardCharsets.UTF_8); + assertTrue(payload.contains("\"value\":\"value\"")); + assertTrue(payload.contains("\"eventTimestamp\":")); + assertFalse(payload.contains("\"topic\":")); + assertEquals(1, connection.qos); assertTrue(connection.retained); assertTrue(connection.closed); + } + @Test void reportsMissingConfiguration() { + MqttConnectionProvider provider = () -> { throw new MqttNotConfiguredException("missing"); }; + assertThrows(MqttNotConfiguredException.class, () -> new JcaMqttPublisher(provider, new ObjectMapper()).publish(new Event("x"))); + } + @Test void reportsSerializationFailureWithoutOpeningConnection() { + boolean[] opened = {false}; + ObjectMapper mapper = new ObjectMapper() { @Override public String writeValueAsString(Object value) throws com.fasterxml.jackson.core.JsonProcessingException { throw new com.fasterxml.jackson.core.JsonProcessingException("bad") {}; } }; + assertThrows(MqttSerializationException.class, () -> new JcaMqttPublisher(() -> { opened[0]=true; return new FakeConnection(); }, mapper).publish(new Event("x"))); + assertFalse(opened[0]); + } + @Test void closesAfterPublishFailure() { + FakeConnection connection = new FakeConnection(); connection.failPublish = true; + assertThrows(MqttPublishException.class, () -> new JcaMqttPublisher(() -> connection, new ObjectMapper()).publish(new Event("x"))); + assertTrue(connection.closed); + } + @Test void rejectsInvalidQos() { assertThrows(IllegalArgumentException.class, () -> new JcaMqttPublisher(() -> null, new ObjectMapper(), 3, false)); } + + static final class Event extends MqttEvent { + private final String value; Event(String value){this.value=value;} + @Override public String getTopic(){return "cdb/add";} public String getValue(){return value;} + } + static final class FakeConnection implements MqttConnection { + String topic; byte[] payload; int qos; boolean retained; boolean closed; boolean failPublish; + @Override public void publish(String topic, byte[] payload, int qos, boolean retained) throws Exception { this.topic=topic;this.payload=payload;this.qos=qos;this.retained=retained;if(failPublish)throw new Exception("broker"); } + @Override public void close(){closed=true;} + } +} diff --git a/tools/developer_tools/cdb-base-mqtt/src/test/java/gov/anl/aps/cdb/base/mqtt/JndiMqttConnectionProviderTest.java b/tools/developer_tools/cdb-base-mqtt/src/test/java/gov/anl/aps/cdb/base/mqtt/JndiMqttConnectionProviderTest.java new file mode 100644 index 000000000..60d5a0830 --- /dev/null +++ b/tools/developer_tools/cdb-base-mqtt/src/test/java/gov/anl/aps/cdb/base/mqtt/JndiMqttConnectionProviderTest.java @@ -0,0 +1,16 @@ +/* + * Copyright (c) UChicago Argonne, LLC. All rights reserved. + * See LICENSE file. + */ +package gov.anl.aps.cdb.base.mqtt; + +import gov.anl.aps.cdb.base.mqtt.exception.MqttNotConfiguredException; +import gov.anl.aps.cdb.base.mqtt.jca.JndiMqttConnectionProvider; +import org.junit.jupiter.api.Test; +import static org.junit.jupiter.api.Assertions.assertThrows; + +class JndiMqttConnectionProviderTest { + @Test void missingJndiResourceIsReportedAsNotConfigured() { + assertThrows(MqttNotConfiguredException.class, () -> new JndiMqttConnectionProvider("java:comp/env/missing-mqtt").getConnection()); + } +} diff --git a/tools/developer_tools/code_testing/CdbWebPortalTest/pom.xml b/tools/developer_tools/code_testing/CdbWebPortalTest/pom.xml index 9545f919f..c63b0f0e9 100644 --- a/tools/developer_tools/code_testing/CdbWebPortalTest/pom.xml +++ b/tools/developer_tools/code_testing/CdbWebPortalTest/pom.xml @@ -66,6 +66,20 @@ + + gov.anl.aps.cdb + cdb-base-mqtt + 1.0.0 + system + ${project.basedir}/../../../../src/java/CdbWebPortal/lib/cdb-base-mqtt-1.0.0.jar + + + gov.anl.aps.mqtt + mqtt-jca-api + 1.0.0 + system + ${project.basedir}/../../../../src/java/CdbWebPortal/lib/mqtt-jca-api-1.0.0.jar + junit junit diff --git a/tools/developer_tools/code_testing/CdbWebPortalTest/src/test/java/gov/anl/aps/cdb/mqtt/CdbInventoryAssemblyElementEventTest.java b/tools/developer_tools/code_testing/CdbWebPortalTest/src/test/java/gov/anl/aps/cdb/mqtt/CdbInventoryAssemblyElementEventTest.java new file mode 100644 index 000000000..c5142e027 --- /dev/null +++ b/tools/developer_tools/code_testing/CdbWebPortalTest/src/test/java/gov/anl/aps/cdb/mqtt/CdbInventoryAssemblyElementEventTest.java @@ -0,0 +1,74 @@ +/* + * Copyright (c) UChicago Argonne, LLC. All rights reserved. + * See LICENSE file. + */ +package gov.anl.aps.cdb.mqtt; + +import com.fasterxml.jackson.databind.ObjectMapper; +import gov.anl.aps.cdb.portal.model.db.entities.Domain; +import gov.anl.aps.cdb.portal.model.db.entities.Item; +import gov.anl.aps.cdb.portal.model.db.entities.ItemDomainCatalog; +import gov.anl.aps.cdb.portal.model.db.entities.ItemDomainInventory; +import gov.anl.aps.cdb.portal.model.db.entities.ItemElement; +import java.util.ArrayList; +import java.util.Collections; +import org.junit.Test; +import static org.junit.Assert.*; + +public class CdbInventoryAssemblyElementEventTest { + + @Test + public void serializesAssemblySlotAndAssignmentTransition() throws Exception { + ItemDomainInventory assembly = inventory(1, "Assembly"); + ItemDomainInventory oldItem = inventory(2, "Old"); + ItemDomainInventory newItem = inventory(3, "New"); + ItemDomainCatalog catalogAssembly = catalog(4, "Catalog Assembly"); + ItemDomainCatalog catalogPart = catalog(5, "Catalog Part"); + ItemElement catalogElement = element(6, catalogAssembly, catalogPart); + ItemElement assemblyElement = element(7, assembly, newItem); + assemblyElement.setDerivedFromItemElement(catalogElement); + + CdbInventoryAssemblyElementEvent event = new CdbInventoryAssemblyElementEvent( + assembly, assemblyElement, oldItem, newItem, null); + String json = new ObjectMapper().writeValueAsString(event); + + assertEquals("cdb/inventory/assembly-element/update", event.getTopic()); + assertEquals("1", event.getInventoryAssembly().getId()); + assertEquals("7", event.getAssemblyElement().getId()); + assertEquals("6", event.getCatalogElement().getId()); + assertEquals("2", event.getOldAssignedInventoryItem().getId()); + assertEquals("3", event.getNewAssignedInventoryItem().getId()); + assertFalse(json.contains("\"topic\"")); + } + + private static ItemElement element(int id, Item parent, Item contained) { + ItemElement element = new ItemElement(); + element.setId(id); + element.setName("E" + id); + element.setParentItem(parent); + element.setContainedItem(contained); + return element; + } + + private static ItemDomainInventory inventory(int id, String name) { + ItemDomainInventory item = new ItemDomainInventory(); + initialize(item, id, name, 2, "inventory"); + return item; + } + + private static ItemDomainCatalog catalog(int id, String name) { + ItemDomainCatalog item = new ItemDomainCatalog(); + initialize(item, id, name, 1, "catalog"); + return item; + } + + private static void initialize(Item item, int id, String name, int domainId, String domain) { + item.setId(id); + item.setName(name); + item.setDomain(new Domain(domainId, domain)); + item.setEntityTypeListFromApi(new ArrayList<>()); + ItemElement self = new ItemElement(); + self.setParentItem(item); + item.setFullItemElementList(new ArrayList<>(Collections.singletonList(self))); + } +} diff --git a/tools/developer_tools/code_testing/CdbWebPortalTest/src/test/java/gov/anl/aps/cdb/mqtt/CdbItemLocationEventTest.java b/tools/developer_tools/code_testing/CdbWebPortalTest/src/test/java/gov/anl/aps/cdb/mqtt/CdbItemLocationEventTest.java new file mode 100644 index 000000000..8b6d1bbe4 --- /dev/null +++ b/tools/developer_tools/code_testing/CdbWebPortalTest/src/test/java/gov/anl/aps/cdb/mqtt/CdbItemLocationEventTest.java @@ -0,0 +1,70 @@ +/* + * Copyright (c) UChicago Argonne, LLC. All rights reserved. + * See LICENSE file. + */ +package gov.anl.aps.cdb.mqtt; + +import com.fasterxml.jackson.databind.ObjectMapper; +import gov.anl.aps.cdb.portal.model.db.entities.Domain; +import gov.anl.aps.cdb.portal.model.db.entities.Item; +import gov.anl.aps.cdb.portal.model.db.entities.ItemDomainInventory; +import gov.anl.aps.cdb.portal.model.db.entities.ItemDomainLocation; +import gov.anl.aps.cdb.portal.model.db.entities.ItemElement; +import java.util.ArrayList; +import java.util.Collections; +import org.junit.Test; +import static org.junit.Assert.*; + +public class CdbItemLocationEventTest { + + @Test + public void serializesLocationAndDetailsTransition() throws Exception { + ItemDomainInventory affected = inventory(1, "Device"); + ItemDomainLocation oldLocation = location(2, "Old"); + ItemDomainLocation newLocation = location(3, "New"); + CdbItemLocationEvent event = new CdbItemLocationEvent(affected, + oldLocation, newLocation, "rack 1", "rack 2", null); + + String json = new ObjectMapper().writeValueAsString(event); + + assertEquals("cdb/item/location/update", event.getTopic()); + assertEquals("1", event.getItem().getId()); + assertEquals("2", event.getOldLocation().getId()); + assertEquals("3", event.getNewLocation().getId()); + assertEquals("rack 1", event.getOldLocationDetails()); + assertEquals("rack 2", event.getNewLocationDetails()); + assertFalse(json.contains("\"topic\"")); + } + + @Test + public void supportsSetAndClearTransitions() { + ItemDomainInventory affected = inventory(1, "Device"); + ItemDomainLocation location = location(2, "Location"); + assertNull(new CdbItemLocationEvent(affected, null, location, + null, null, null).getOldLocation()); + assertNull(new CdbItemLocationEvent(affected, location, null, + null, null, null).getNewLocation()); + } + + private static ItemDomainInventory inventory(int id, String name) { + ItemDomainInventory item = new ItemDomainInventory(); + initialize(item, id, name, 2, "inventory"); + return item; + } + + private static ItemDomainLocation location(int id, String name) { + ItemDomainLocation item = new ItemDomainLocation(); + initialize(item, id, name, 3, "location"); + return item; + } + + private static void initialize(Item item, int id, String name, int domainId, String domain) { + item.setId(id); + item.setName(name); + item.setDomain(new Domain(domainId, domain)); + item.setEntityTypeListFromApi(new ArrayList<>()); + ItemElement self = new ItemElement(); + self.setParentItem(item); + item.setFullItemElementList(new ArrayList<>(Collections.singletonList(self))); + } +} diff --git a/tools/developer_tools/code_testing/CdbWebPortalTest/src/test/java/gov/anl/aps/cdb/mqtt/CdbItemPropertyEventTest.java b/tools/developer_tools/code_testing/CdbWebPortalTest/src/test/java/gov/anl/aps/cdb/mqtt/CdbItemPropertyEventTest.java new file mode 100644 index 000000000..7f2ab529d --- /dev/null +++ b/tools/developer_tools/code_testing/CdbWebPortalTest/src/test/java/gov/anl/aps/cdb/mqtt/CdbItemPropertyEventTest.java @@ -0,0 +1,115 @@ +/* + * Copyright (c) UChicago Argonne, LLC. All rights reserved. + * See LICENSE file. + */ +package gov.anl.aps.cdb.mqtt; + +import com.fasterxml.jackson.databind.ObjectMapper; +import gov.anl.aps.cdb.portal.model.db.entities.ItemDomainCatalog; +import gov.anl.aps.cdb.portal.model.db.entities.ItemElement; +import gov.anl.aps.cdb.portal.model.db.entities.PropertyMetadata; +import gov.anl.aps.cdb.portal.model.db.entities.PropertyType; +import gov.anl.aps.cdb.portal.model.db.entities.PropertyTypeHandler; +import gov.anl.aps.cdb.portal.model.db.entities.PropertyValue; +import gov.anl.aps.cdb.portal.model.db.entities.UserInfo; +import gov.anl.aps.cdb.portal.model.jsf.handlers.MarkdownPropertyTypeHandler; +import java.util.ArrayList; +import java.util.Collections; +import org.junit.Test; +import static org.junit.Assert.*; + +public class CdbItemPropertyEventTest { + + @Test + public void serializesScalarAddedPropertyWithoutTopic() throws Exception { + ItemDomainCatalog item = item(10, "Magnet"); + UserInfo user = new UserInfo(); + user.setUsername("operator"); + PropertyValue value = property(20, 30, "serial", "A1"); + value.setUnits("unit"); + value.setDescription("description"); + PropertyMetadata metadata = new PropertyMetadata(1, "source", "manual"); + metadata.setPropertyValue(value); + value.setPropertyMetadataList(Collections.singletonList(metadata)); + + CdbItemPropertyEvent event = CdbItemPropertyEvent.added(item, value, user); + String json = new ObjectMapper().writeValueAsString(event); + + assertEquals("cdb/item/property/add", event.getTopic()); + assertEquals(CdbMqttOperation.ADD, event.getOperation()); + assertEquals("10", event.getItemId()); + assertEquals("operator", event.getTriggeredByUsername()); + assertEquals("A1", event.getNewProperty().getValue()); + assertEquals("manual", event.getNewProperty().getMetadata().get("source")); + assertNull(event.getOldProperty()); + assertFalse(json.contains("\"topic\"")); + assertTrue(json.contains("\"eventTimestamp\"")); + } + + @Test + public void longTextUpdateContainsChangedLineDiffInsteadOfFullText() { + ItemDomainCatalog item = item(10, "Magnet"); + PropertyValue oldValue = markdown(20, "same\nold\ntail"); + PropertyValue newValue = markdown(20, "same\nnew\ntail"); + + CdbItemPropertyEvent event = CdbItemPropertyEvent.updated( + item, oldValue, newValue, null); + + assertEquals("-old\n+new\n", event.getTextDiff()); + assertNull(event.getOldProperty().getText()); + assertNull(event.getNewProperty().getText()); + assertTrue(event.getOldProperty().isTextOmitted()); + assertTrue(event.getNewProperty().isTextOmitted()); + assertFalse(event.isDiffTruncated()); + } + + @Test + public void capsLongTextPayloadsAndReportsTruncation() { + PropertyValue value = markdown(20, + String.join("", Collections.nCopies(CdbItemPropertyEvent.MAX_TEXT_LENGTH + 1, "x"))); + + CdbItemPropertyEvent event = CdbItemPropertyEvent.added(item(10, "Magnet"), value, null); + + assertEquals(CdbItemPropertyEvent.MAX_TEXT_LENGTH, + event.getNewProperty().getText().length()); + assertFalse(event.getNewProperty().isTextOmitted()); + assertTrue(event.isDiffTruncated()); + } + + @Test + public void deleteCarriesOnlyOldProperty() { + CdbItemPropertyEvent event = CdbItemPropertyEvent.deleted( + item(10, "Magnet"), property(20, 30, "serial", "A1"), null); + assertEquals("cdb/item/property/delete", event.getTopic()); + assertNotNull(event.getOldProperty()); + assertNull(event.getNewProperty()); + } + + private static PropertyValue markdown(int id, String text) { + PropertyValue value = property(id, 30, "notes", null); + value.setText(text); + value.getPropertyType().setPropertyTypeHandler( + new PropertyTypeHandler(1, MarkdownPropertyTypeHandler.HANDLER_NAME)); + return value; + } + + private static PropertyValue property(int id, int typeId, String typeName, String valueText) { + PropertyType type = new PropertyType(typeId, typeName); + PropertyValue value = new PropertyValue(); + value.setId(id); + value.setPropertyType(type); + value.setValue(valueText); + value.setPropertyMetadataList(new ArrayList<>()); + return value; + } + + private static ItemDomainCatalog item(int id, String name) { + ItemDomainCatalog item = new ItemDomainCatalog(); + item.setId(id); + item.setName(name); + ItemElement self = new ItemElement(); + self.setParentItem(item); + item.setFullItemElementList(new ArrayList<>(Collections.singletonList(self))); + return item; + } +} diff --git a/tools/developer_tools/code_testing/CdbWebPortalTest/src/test/java/gov/anl/aps/cdb/mqtt/CdbMachineAssignedItemEventTest.java b/tools/developer_tools/code_testing/CdbWebPortalTest/src/test/java/gov/anl/aps/cdb/mqtt/CdbMachineAssignedItemEventTest.java new file mode 100644 index 000000000..22335f916 --- /dev/null +++ b/tools/developer_tools/code_testing/CdbWebPortalTest/src/test/java/gov/anl/aps/cdb/mqtt/CdbMachineAssignedItemEventTest.java @@ -0,0 +1,76 @@ +/* + * Copyright (c) UChicago Argonne, LLC. All rights reserved. + * See LICENSE file. + */ +package gov.anl.aps.cdb.mqtt; + +import com.fasterxml.jackson.databind.ObjectMapper; +import gov.anl.aps.cdb.portal.model.db.entities.Item; +import gov.anl.aps.cdb.portal.model.db.entities.Domain; +import gov.anl.aps.cdb.portal.model.db.entities.ItemDomainCatalog; +import gov.anl.aps.cdb.portal.model.db.entities.ItemDomainMachineDesign; +import gov.anl.aps.cdb.portal.model.db.entities.ItemElement; +import gov.anl.aps.cdb.portal.model.db.entities.UserInfo; +import java.util.ArrayList; +import java.util.Collections; +import org.junit.Test; +import static org.junit.Assert.*; + +public class CdbMachineAssignedItemEventTest { + + @Test + public void serializesAssignTransitionWithoutTopicProperty() throws Exception { + ItemDomainMachineDesign machine = machine(1, "M1"); + Item assigned = catalog(2, "Catalog"); + UserInfo user = new UserInfo(); + user.setUsername("operator"); + + CdbMachineAssignedItemEvent event = new CdbMachineAssignedItemEvent( + machine, null, assigned, true, false, user); + String json = new ObjectMapper().writeValueAsString(event); + + assertEquals("cdb/machine-design/assigned-item/update", event.getTopic()); + assertEquals("1", event.getMachine().getId()); + assertEquals("machineDesign", event.getMachine().getDomain()); + assertEquals(Integer.valueOf(1), event.getMachine().getDomainId()); + assertNull(event.getOldAssignedItem()); + assertEquals("2", event.getNewAssignedItem().getId()); + assertEquals("catalog", event.getNewAssignedItem().getDomain()); + assertTrue(json.contains("\"domain_id\":1")); + assertTrue(event.isOldInstalled()); + assertFalse(event.isNewInstalled()); + assertEquals("operator", event.getTriggeredByUsername()); + assertFalse(json.contains("\"topic\"")); + } + + @Test + public void supportsUnassignTransition() { + CdbMachineAssignedItemEvent event = new CdbMachineAssignedItemEvent( + machine(1, "M1"), catalog(2, "Catalog"), null, + false, true, null); + assertEquals("2", event.getOldAssignedItem().getId()); + assertNull(event.getNewAssignedItem()); + } + + private static ItemDomainMachineDesign machine(int id, String name) { + ItemDomainMachineDesign item = new ItemDomainMachineDesign(); + initialize(item, id, name); + return item; + } + + private static ItemDomainCatalog catalog(int id, String name) { + ItemDomainCatalog item = new ItemDomainCatalog(); + initialize(item, id, name); + return item; + } + + private static void initialize(Item item, int id, String name) { + item.setId(id); + item.setName(name); + item.setDomain(new Domain(1, item instanceof ItemDomainMachineDesign + ? "machineDesign" : "catalog")); + ItemElement self = new ItemElement(); + self.setParentItem(item); + item.setFullItemElementList(new ArrayList<>(Collections.singletonList(self))); + } +} diff --git a/tools/developer_tools/code_testing/CdbWebPortalTest/src/test/java/gov/anl/aps/cdb/mqtt/CdbMachineMoveEventTest.java b/tools/developer_tools/code_testing/CdbWebPortalTest/src/test/java/gov/anl/aps/cdb/mqtt/CdbMachineMoveEventTest.java new file mode 100644 index 000000000..292ea5119 --- /dev/null +++ b/tools/developer_tools/code_testing/CdbWebPortalTest/src/test/java/gov/anl/aps/cdb/mqtt/CdbMachineMoveEventTest.java @@ -0,0 +1,61 @@ +/* + * Copyright (c) UChicago Argonne, LLC. All rights reserved. + * See LICENSE file. + */ +package gov.anl.aps.cdb.mqtt; + +import com.fasterxml.jackson.databind.ObjectMapper; +import gov.anl.aps.cdb.portal.model.db.entities.Domain; +import gov.anl.aps.cdb.portal.model.db.entities.EntityType; +import gov.anl.aps.cdb.portal.model.db.entities.ItemDomainMachineDesign; +import gov.anl.aps.cdb.portal.model.db.entities.ItemElement; +import gov.anl.aps.cdb.portal.model.db.entities.UserInfo; +import java.util.ArrayList; +import java.util.Collections; +import org.junit.Test; +import static org.junit.Assert.*; + +public class CdbMachineMoveEventTest { + + @Test + public void serializesMoveContextAndActorWithoutTopicProperty() throws Exception { + UserInfo user = new UserInfo(); + user.setUsername("operator"); + ItemDomainMachineDesign moved = machine(1, "Moved"); + moved.setEntityTypeListFromApi(Collections.singletonList(new EntityType(9, "ioc"))); + CdbMachineMoveEvent event = new CdbMachineMoveEvent( + moved, machine(2, "Old"), machine(3, "New"), user); + + String json = new ObjectMapper().writeValueAsString(event); + + assertEquals("cdb/machine-design/move", event.getTopic()); + assertEquals("1", event.getMachine().getId()); + assertEquals("2", event.getOldParent().getId()); + assertEquals("3", event.getNewParent().getId()); + assertEquals("machineDesign", event.getMachine().getDomain()); + assertEquals(Integer.valueOf(9), event.getMachine().getEntityTypes().get(0).getId()); + assertEquals("ioc", event.getMachine().getEntityTypes().get(0).getName()); + assertEquals("operator", event.getTriggeredByUsername()); + assertFalse(json.contains("\"topic\"")); + assertTrue(json.contains("\"eventTimestamp\"")); + } + + @Test + public void supportsMoveFromTopLevel() { + CdbMachineMoveEvent event = new CdbMachineMoveEvent( + machine(1, "Moved"), null, machine(3, "New"), null); + assertNull(event.getOldParent()); + assertNull(event.getTriggeredByUsername()); + } + + private static ItemDomainMachineDesign machine(int id, String name) { + ItemDomainMachineDesign item = new ItemDomainMachineDesign(); + item.setId(id); + item.setName(name); + item.setDomain(new Domain(4, "machineDesign")); + ItemElement self = new ItemElement(); + self.setParentItem(item); + item.setFullItemElementList(new ArrayList<>(Collections.singletonList(self))); + return item; + } +} diff --git a/tools/developer_tools/code_testing/CdbWebPortalTest/src/test/java/gov/anl/aps/cdb/mqtt/CdbMachineRepresentedElementEventTest.java b/tools/developer_tools/code_testing/CdbWebPortalTest/src/test/java/gov/anl/aps/cdb/mqtt/CdbMachineRepresentedElementEventTest.java new file mode 100644 index 000000000..ff2687d28 --- /dev/null +++ b/tools/developer_tools/code_testing/CdbWebPortalTest/src/test/java/gov/anl/aps/cdb/mqtt/CdbMachineRepresentedElementEventTest.java @@ -0,0 +1,77 @@ +/* + * Copyright (c) UChicago Argonne, LLC. All rights reserved. + * See LICENSE file. + */ +package gov.anl.aps.cdb.mqtt; + +import com.fasterxml.jackson.databind.ObjectMapper; +import gov.anl.aps.cdb.portal.model.db.entities.Domain; +import gov.anl.aps.cdb.portal.model.db.entities.Item; +import gov.anl.aps.cdb.portal.model.db.entities.ItemDomainCatalog; +import gov.anl.aps.cdb.portal.model.db.entities.ItemDomainMachineDesign; +import gov.anl.aps.cdb.portal.model.db.entities.ItemElement; +import java.util.ArrayList; +import java.util.Collections; +import org.junit.Test; +import static org.junit.Assert.*; + +public class CdbMachineRepresentedElementEventTest { + + @Test + public void serializesElementAndCatalogContext() throws Exception { + ItemDomainMachineDesign machine = machine(1, "Machine"); + ItemDomainCatalog assembly = catalog(2, "Assembly"); + ItemDomainCatalog part = catalog(3, "Part"); + ItemElement element = element(4, "E1", assembly, part); + + CdbMachineRepresentedElementEvent event + = new CdbMachineRepresentedElementEvent(machine, null, element, null); + String json = new ObjectMapper().writeValueAsString(event); + + assertEquals("cdb/machine-design/represented-element/update", event.getTopic()); + assertNull(event.getOldRepresentedElement()); + assertEquals("4", event.getNewRepresentedElement().getId()); + assertEquals("2", event.getNewRepresentedElement().getParentItem().getId()); + assertEquals("3", event.getNewRepresentedElement().getContainedItem().getId()); + assertNull(event.getSourceTemplate()); + assertFalse(json.contains("\"topic\"")); + } + + @Test + public void includesTemplateContextForPropagatedChange() { + CdbMachineRepresentedElementEvent event = new CdbMachineRepresentedElementEvent( + machine(1, "Instance"), null, null, machine(2, "Template"), null); + assertEquals("2", event.getSourceTemplate().getId()); + } + + private static ItemElement element(int id, String name, Item parent, Item contained) { + ItemElement element = new ItemElement(); + element.setId(id); + element.setName(name); + element.setParentItem(parent); + element.setContainedItem(contained); + return element; + } + + private static ItemDomainMachineDesign machine(int id, String name) { + ItemDomainMachineDesign item = new ItemDomainMachineDesign(); + initialize(item, id, name, 4, "machineDesign"); + return item; + } + + private static ItemDomainCatalog catalog(int id, String name) { + ItemDomainCatalog item = new ItemDomainCatalog(); + initialize(item, id, name, 1, "catalog"); + return item; + } + + private static void initialize(Item item, int id, String name, int domainId, String domain) { + item.setId(id); + item.setName(name); + item.setDomain(new Domain(domainId, domain)); + item.setEntityTypeListFromApi(new ArrayList<>()); + ItemElement self = new ItemElement(); + self.setParentItem(item); + item.setFullItemElementList(new ArrayList<>(Collections.singletonList(self))); + } +} diff --git a/tools/developer_tools/code_testing/CdbWebPortalTest/src/test/java/gov/anl/aps/cdb/mqtt/CdbMachineTrashEventTest.java b/tools/developer_tools/code_testing/CdbWebPortalTest/src/test/java/gov/anl/aps/cdb/mqtt/CdbMachineTrashEventTest.java new file mode 100644 index 000000000..cb01865ea --- /dev/null +++ b/tools/developer_tools/code_testing/CdbWebPortalTest/src/test/java/gov/anl/aps/cdb/mqtt/CdbMachineTrashEventTest.java @@ -0,0 +1,59 @@ +/* + * Copyright (c) UChicago Argonne, LLC. All rights reserved. + * See LICENSE file. + */ +package gov.anl.aps.cdb.mqtt; + +import com.fasterxml.jackson.databind.ObjectMapper; +import gov.anl.aps.cdb.portal.model.db.entities.Domain; +import gov.anl.aps.cdb.portal.model.db.entities.ItemDomainMachineDesign; +import gov.anl.aps.cdb.portal.model.db.entities.ItemElement; +import java.util.ArrayList; +import java.util.Collections; +import org.junit.Test; +import static org.junit.Assert.*; + +public class CdbMachineTrashEventTest { + + @Test + public void serializesHierarchyTrashContext() throws Exception { + CdbMachineTrashEvent event = new CdbMachineTrashEvent( + CdbMqttTopic.MACHINE_DESIGN_TRASH, + machine(2, "Child"), machine(1, "Root"), 3, null); + String json = new ObjectMapper().writeValueAsString(event); + + assertEquals("cdb/machine-design/trash", event.getTopic()); + assertEquals(CdbMqttOperation.TRASH, event.getOperation()); + assertEquals("2", event.getAffectedMachine().getId()); + assertEquals("1", event.getRequestedHierarchyRoot().getId()); + assertEquals(3, event.getAffectedMachineCount()); + assertFalse(json.contains("\"topic\"")); + } + + @Test + public void supportsRestoreOperation() { + CdbMachineTrashEvent event = new CdbMachineTrashEvent( + CdbMqttTopic.MACHINE_DESIGN_RESTORE, + machine(1, "Root"), machine(1, "Root"), 1, null); + assertEquals(CdbMqttOperation.RESTORE, event.getOperation()); + assertEquals("cdb/machine-design/restore", event.getTopic()); + } + + @Test(expected = IllegalArgumentException.class) + public void rejectsUnrelatedTopic() { + new CdbMachineTrashEvent(CdbMqttTopic.UPDATE, + machine(1, "Root"), machine(1, "Root"), 1, null); + } + + private static ItemDomainMachineDesign machine(int id, String name) { + ItemDomainMachineDesign item = new ItemDomainMachineDesign(); + item.setId(id); + item.setName(name); + item.setDomain(new Domain(4, "machineDesign")); + item.setEntityTypeListFromApi(new ArrayList<>()); + ItemElement self = new ItemElement(); + self.setParentItem(item); + item.setFullItemElementList(new ArrayList<>(Collections.singletonList(self))); + return item; + } +} diff --git a/tools/developer_tools/code_testing/CdbWebPortalTest/src/test/java/gov/anl/aps/cdb/portal/controllers/utilities/CdbEntityControllerUtilityEventLifecycleTest.java b/tools/developer_tools/code_testing/CdbWebPortalTest/src/test/java/gov/anl/aps/cdb/portal/controllers/utilities/CdbEntityControllerUtilityEventLifecycleTest.java new file mode 100644 index 000000000..5a9368385 --- /dev/null +++ b/tools/developer_tools/code_testing/CdbWebPortalTest/src/test/java/gov/anl/aps/cdb/portal/controllers/utilities/CdbEntityControllerUtilityEventLifecycleTest.java @@ -0,0 +1,198 @@ +/* + * Copyright (c) UChicago Argonne, LLC. All rights reserved. + * See LICENSE file. + */ +package gov.anl.aps.cdb.portal.controllers.utilities; + +import gov.anl.aps.cdb.base.mqtt.MqttEvent; +import gov.anl.aps.cdb.common.exceptions.CdbException; +import gov.anl.aps.cdb.mqtt.CdbEntityEvent; +import gov.anl.aps.cdb.portal.constants.SystemLogLevel; +import gov.anl.aps.cdb.portal.model.db.beans.CdbEntityFacade; +import gov.anl.aps.cdb.portal.model.db.entities.CdbEntity; +import gov.anl.aps.cdb.portal.model.db.entities.UserInfo; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collections; +import java.util.List; +import javax.persistence.EntityManager; +import org.junit.Test; +import static org.junit.Assert.*; + +public class CdbEntityControllerUtilityEventLifecycleTest { + + @Test + public void publishesGenericThenSpecializedEventAfterSuccessfulUpdate() throws Exception { + TestUtility utility = new TestUtility(new TestFacade()); + TestEntity entity = new TestEntity(1); + utility.specializedEvents = Collections.singletonList(new TestEvent("specialized")); + + utility.update(entity, null); + + assertEquals(2, utility.publishedEvents.size()); + assertTrue(utility.publishedEvents.get(0) instanceof CdbEntityEvent); + assertEquals("specialized", utility.publishedEvents.get(1).getTopic()); + assertTrue(utility.facade.editCalled); + } + + @Test + public void publishesCollectedEventsForListOnlyAfterSuccessfulEdit() throws Exception { + TestUtility utility = new TestUtility(new TestFacade()); + utility.createEventPerEntity = true; + + utility.updateList(Arrays.asList(new TestEntity(1), new TestEntity(2)), null); + + assertEquals(4, utility.publishedEvents.size()); + assertTrue(utility.publishedEvents.get(0) instanceof CdbEntityEvent); + assertTrue(utility.publishedEvents.get(1) instanceof CdbEntityEvent); + assertEquals("specialized/1", utility.publishedEvents.get(2).getTopic()); + assertEquals("specialized/2", utility.publishedEvents.get(3).getTopic()); + } + + @Test + public void doesNotPublishAnyEventWhenPersistenceFails() throws Exception { + TestFacade facade = new TestFacade(); + facade.failEdit = true; + TestUtility utility = new TestUtility(facade); + utility.specializedEvents = Collections.singletonList(new TestEvent("specialized")); + + try { + utility.update(new TestEntity(1), null); + fail("Expected persistence failure"); + } catch (IllegalStateException expected) { + assertEquals("edit failed", expected.getMessage()); + } + + assertTrue(utility.publishedEvents.isEmpty()); + } + + @Test + public void removalUpdatePublishesSpecializedEventOnlyAfterSuccessfulEdit() throws Exception { + TestUtility utility = new TestUtility(new TestFacade()); + utility.specializedEvents = Collections.singletonList(new TestEvent("specialized/remove")); + + utility.updateOnRemoval(new TestEntity(1), null); + + assertEquals(2, utility.publishedEvents.size()); + assertTrue(utility.publishedEvents.get(0) instanceof CdbEntityEvent); + assertEquals("specialized/remove", utility.publishedEvents.get(1).getTopic()); + } + + @Test + public void updateWithoutSpecializedEventsStillPublishesGenericEvent() throws Exception { + TestUtility utility = new TestUtility(new TestFacade()); + + utility.update(new TestEntity(1), null); + + assertEquals(1, utility.publishedEvents.size()); + assertTrue(utility.publishedEvents.get(0) instanceof CdbEntityEvent); + } + + private static final class TestEvent extends MqttEvent { + private final String topic; + + TestEvent(String topic) { + this.topic = topic; + } + + @Override + public String getTopic() { + return topic; + } + } + + private static final class TestEntity extends CdbEntity { + private final Integer id; + + TestEntity(Integer id) { + this.id = id; + } + + @Override + public Integer getId() { + return id; + } + } + + private static final class TestFacade extends CdbEntityFacade { + private boolean editCalled; + private boolean failEdit; + + TestFacade() { + super(TestEntity.class); + } + + @Override + protected EntityManager getEntityManager() { + throw new UnsupportedOperationException("Not used by this test"); + } + + @Override + public TestEntity edit(TestEntity entity) { + editCalled = true; + if (failEdit) { + throw new IllegalStateException("edit failed"); + } + return entity; + } + + @Override + public void edit(List entities) { + editCalled = true; + if (failEdit) { + throw new IllegalStateException("edit failed"); + } + } + } + + private static final class TestUtility + extends CdbEntityControllerUtility { + private final TestFacade facade; + private final List publishedEvents = new ArrayList<>(); + private List specializedEvents = Collections.emptyList(); + private boolean createEventPerEntity; + + TestUtility(TestFacade facade) { + this.facade = facade; + } + + @Override + protected TestFacade getEntityDbFacade() { + return facade; + } + + @Override + public TestEntity createEntityInstance(UserInfo sessionUser) { + return new TestEntity(null); + } + + @Override + public String getEntityTypeName() { + return "testEntity"; + } + + @Override + protected List prepareSpecializedUpdateEvents( + TestEntity entity, UserInfo userInfo) { + if (createEventPerEntity) { + return Collections.singletonList(new TestEvent("specialized/" + entity.getId())); + } + return specializedEvents; + } + + @Override + protected void publishMqttEvent(MqttEvent event) { + publishedEvents.add(event); + } + + @Override + protected void addCdbEntitySystemLog( + SystemLogLevel logLevel, String message, UserInfo sessionUser) throws CdbException { + } + + @Override + protected void addCdbEntityWarningSystemLog(String warningMessage, + Exception exception, CdbEntity entity, UserInfo sessionUser) throws CdbException { + } + } +} diff --git a/tools/developer_tools/code_testing/CdbWebPortalTest/src/test/java/gov/anl/aps/cdb/portal/controllers/utilities/InventoryAssemblyElementEventDetectionTest.java b/tools/developer_tools/code_testing/CdbWebPortalTest/src/test/java/gov/anl/aps/cdb/portal/controllers/utilities/InventoryAssemblyElementEventDetectionTest.java new file mode 100644 index 000000000..7b97c652e --- /dev/null +++ b/tools/developer_tools/code_testing/CdbWebPortalTest/src/test/java/gov/anl/aps/cdb/portal/controllers/utilities/InventoryAssemblyElementEventDetectionTest.java @@ -0,0 +1,124 @@ +/* + * Copyright (c) UChicago Argonne, LLC. All rights reserved. + * See LICENSE file. + */ +package gov.anl.aps.cdb.portal.controllers.utilities; + +import gov.anl.aps.cdb.base.mqtt.MqttEvent; +import gov.anl.aps.cdb.mqtt.CdbInventoryAssemblyElementEvent; +import gov.anl.aps.cdb.mqtt.metadata.InventoryAssemblyElementChange; +import gov.anl.aps.cdb.portal.model.db.entities.Item; +import gov.anl.aps.cdb.portal.model.db.entities.ItemDomainInventory; +import gov.anl.aps.cdb.portal.model.db.entities.ItemElement; +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; +import org.junit.Test; +import static org.junit.Assert.*; + +public class InventoryAssemblyElementEventDetectionTest { + + @Test + public void convertsAssignReplaceAndClearMetadataToEvents() throws Exception { + ItemDomainInventory assembly = inventory(1, "Assembly"); + ItemElement element = element(10, assembly); + ItemDomainInventory first = inventory(2, "First"); + ItemDomainInventory second = inventory(3, "Second"); + assembly.addPendingAssemblyElementChange( + new InventoryAssemblyElementChange(element, null, first)); + assembly.addPendingAssemblyElementChange( + new InventoryAssemblyElementChange(element, first, second)); + assembly.addPendingAssemblyElementChange( + new InventoryAssemblyElementChange(element, second, null)); + + List events = new TestUtility().events(assembly); + + assertEquals(3, events.size()); + CdbInventoryAssemblyElementEvent assign + = (CdbInventoryAssemblyElementEvent) events.get(0); + assertNull(assign.getOldAssignedInventoryItem()); + assertEquals("2", assign.getNewAssignedInventoryItem().getId()); + CdbInventoryAssemblyElementEvent clear + = (CdbInventoryAssemblyElementEvent) events.get(2); + assertEquals("3", clear.getOldAssignedInventoryItem().getId()); + assertNull(clear.getNewAssignedInventoryItem()); + } + + @Test + public void persistedElementComparisonDetectsAssignmentAndSuppressesNoOp() throws Exception { + ItemDomainInventory original = inventory(1, "Assembly"); + ItemElement originalElement = element(10, original); + original.getFullItemElementList().add(originalElement); + ItemDomainInventory edited = inventory(1, "Assembly"); + ItemElement editedElement = element(10, edited); + ItemDomainInventory assigned = inventory(2, "Assigned"); + editedElement.setContainedItem(assigned); + edited.getFullItemElementList().add(editedElement); + TestUtility utility = new TestUtility(); + + utility.detect(original, edited); + + List events = utility.events(edited); + assertEquals(1, events.size()); + CdbInventoryAssemblyElementEvent event + = (CdbInventoryAssemblyElementEvent) events.get(0); + assertNull(event.getOldAssignedInventoryItem()); + assertEquals("2", event.getNewAssignedInventoryItem().getId()); + } + + @Test + public void includesChangesFromNewlyGeneratedChildAssembly() throws Exception { + ItemDomainInventory root = inventory(1, "Root"); + ItemDomainInventory childAssembly = inventory(2, "Child Assembly"); + ItemDomainInventory grandchild = inventory(3, "Grandchild"); + ItemElement rootElement = element(10, root); + ItemElement childElement = element(11, childAssembly); + root.addPendingAssemblyElementChange( + new InventoryAssemblyElementChange(rootElement, null, childAssembly)); + childAssembly.addPendingAssemblyElementChange( + new InventoryAssemblyElementChange(childElement, null, grandchild)); + + List events = new TestUtility().events(root); + + assertEquals(2, events.size()); + assertEquals("1", ((CdbInventoryAssemblyElementEvent) events.get(0)) + .getInventoryAssembly().getId()); + assertEquals("2", ((CdbInventoryAssemblyElementEvent) events.get(1)) + .getInventoryAssembly().getId()); + } + + @Test + public void unchangedAssemblyProducesNoEvent() throws Exception { + assertTrue(new TestUtility().events(inventory(1, "Assembly")).isEmpty()); + } + + private static ItemElement element(int id, Item parent) { + ItemElement element = new ItemElement(); + element.setId(id); + element.setName("E" + id); + element.setParentItem(parent); + return element; + } + + private static ItemDomainInventory inventory(int id, String name) { + ItemDomainInventory item = new ItemDomainInventory(); + item.setId(id); + item.setName(name); + item.setEntityTypeListFromApi(new ArrayList<>()); + ItemElement self = new ItemElement(); + self.setParentItem(item); + item.setFullItemElementList(new ArrayList<>(Collections.singletonList(self))); + return item; + } + + private static final class TestUtility extends ItemDomainInventoryControllerUtility { + void detect(ItemDomainInventory original, ItemDomainInventory edited) { + prepareInventoryAssemblyElementChanges(original, edited, + original.getFullItemElementList(), edited.getFullItemElementList()); + } + + List events(ItemDomainInventory item) throws Exception { + return prepareSpecializedUpdateEvents(item, null); + } + } +} diff --git a/tools/developer_tools/code_testing/CdbWebPortalTest/src/test/java/gov/anl/aps/cdb/portal/controllers/utilities/ItemDomainMachineDesignControllerUtilityTest.java b/tools/developer_tools/code_testing/CdbWebPortalTest/src/test/java/gov/anl/aps/cdb/portal/controllers/utilities/ItemDomainMachineDesignControllerUtilityTest.java index 66865f120..2a050b12e 100644 --- a/tools/developer_tools/code_testing/CdbWebPortalTest/src/test/java/gov/anl/aps/cdb/portal/controllers/utilities/ItemDomainMachineDesignControllerUtilityTest.java +++ b/tools/developer_tools/code_testing/CdbWebPortalTest/src/test/java/gov/anl/aps/cdb/portal/controllers/utilities/ItemDomainMachineDesignControllerUtilityTest.java @@ -4,9 +4,11 @@ */ package gov.anl.aps.cdb.portal.controllers.utilities; +import gov.anl.aps.cdb.base.mqtt.MqttEvent; import gov.anl.aps.cdb.common.exceptions.CdbException; import gov.anl.aps.cdb.common.exceptions.InvalidArgument; import gov.anl.aps.cdb.common.exceptions.InvalidObjectState; +import gov.anl.aps.cdb.mqtt.CdbMachineMoveEvent; import gov.anl.aps.cdb.portal.model.db.beans.ItemDomainMachineDesignFacade; import gov.anl.aps.cdb.portal.model.db.entities.EntityInfo; import gov.anl.aps.cdb.portal.model.db.entities.EntityType; @@ -51,6 +53,10 @@ public void movesExistingChildToNewParentAndUpdatesParent() throws Exception { assertEquals("E1", membership.getName()); assertTrue(newParent.getFullItemElementList().contains(membership)); assertEquals(1, utility.updateCount); + assertEquals(1, utility.publishedEvents.size()); + CdbMachineMoveEvent event = (CdbMachineMoveEvent) utility.publishedEvents.get(0); + assertEquals(oldParent.getId(), integerId(event.getOldParent().getId())); + assertEquals(newParent.getId(), integerId(event.getNewParent().getId())); } @Test @@ -65,6 +71,36 @@ public void movesTopLevelMachineByCreatingMembership() throws Exception { assertEquals("E1", result.getName()); assertTrue(newParent.getFullItemElementList().contains(result)); assertEquals(1, utility.updateCount); + CdbMachineMoveEvent event = (CdbMachineMoveEvent) utility.publishedEvents.get(0); + assertNull(event.getOldParent()); + assertEquals(newParent.getId(), integerId(event.getNewParent().getId())); + } + + @Test + public void suppressesNoOpMove() throws Exception { + ItemDomainMachineDesign parent = machine("Parent"); + ItemDomainMachineDesign moved = machine("Moved"); + ItemElement membership = child(parent, moved, "E1"); + + ItemElement result = utility.performMachineMoveWithUpdate(parent, moved, user); + + assertSame(membership, result); + assertEquals(0, utility.updateCount); + assertTrue(utility.publishedEvents.isEmpty()); + } + + @Test + public void doesNotPublishMoveWhenUpdateFails() { + ItemDomainMachineDesign oldParent = machine("Old"); + ItemDomainMachineDesign newParent = machine("New"); + ItemDomainMachineDesign moved = machine("Moved"); + child(oldParent, moved, "E1"); + utility.failUpdate = true; + + expect(CdbException.class, + () -> utility.performMachineMoveWithUpdate(newParent, moved, user)); + + assertTrue(utility.publishedEvents.isEmpty()); } @Test @@ -297,6 +333,36 @@ public void allowsNestedRepresentationBelowRepresentedParent() throws Exception assertSame(nestedPartElement, representedChild.getRepresentsCatalogElement()); } + @Test + public void propagatesRepresentedElementChangeToTemplateInstance() throws Exception { + ItemDomainCatalog templateAssembly = catalog("Template Assembly"); + ItemElement templateElement = child(templateAssembly, catalog("Part"), "E1"); + ItemDomainMachineDesign templateParent = machine("Template Parent"); + templateParent.setAssignedItem(templateAssembly); + TestMachine templateNode = testMachine("Template Node"); + templateNode.setEntityTypeListFromApi( + Collections.singletonList(entityType(1, "Template"))); + child(templateParent, templateNode, "T1"); + + ItemDomainCatalog instanceAssembly = catalog("Instance Assembly"); + ItemElement instanceElement = child(instanceAssembly, catalog("Part"), "E1"); + ItemDomainMachineDesign instanceParent = machine("Instance Parent"); + instanceParent.setAssignedItem(instanceAssembly); + TestMachine instanceNode = testMachine("Instance Node"); + instanceNode.assignCreatedFromTemplate(templateNode); + child(instanceParent, instanceNode, "I1"); + templateNode.addTemplateInstance(instanceNode); + + utility.updateRepresentingAssemblyElementForMachine( + templateNode, templateElement, true); + + assertSame(templateElement, templateNode.getRepresentsCatalogElement()); + assertSame(instanceElement, instanceNode.getRepresentsCatalogElement()); + assertSame(templateNode, + instanceNode.getPendingRepresentedElementChange().getSourceTemplate()); + assertTrue(templateNode.getItemsToUpdate().contains(instanceNode)); + } + @Test public void availableElementsExcludeElementsRepresentedBySiblings() { RepresentationFixture fixture = representationFixture(); @@ -373,6 +439,10 @@ public void searchDelegatesToNoEntityTypeFacade() { assertEquals("sector*", facade.searchString); } + private static Integer integerId(String id) { + return id == null ? null : Integer.valueOf(id); + } + private static UserInfo user(int id) { UserInfo value = new UserInfo(); value.setId(id); @@ -388,8 +458,11 @@ private static EntityType entityType(int id, String name) { return new EntityType(id, name); } + private static int nextMachineId = 1; + private static ItemDomainMachineDesign machine(String name) { ItemDomainMachineDesign item = new ItemDomainMachineDesign(); + item.setId(nextMachineId++); item.setName(name); item.setEntityTypeListFromApi(new ArrayList<>()); item.setItemProjectList(new ArrayList<>()); @@ -456,9 +529,20 @@ private interface ThrowingAction { private static class TestMachine extends ItemDomainMachineDesign { + private final List templateInstances = new ArrayList<>(); + void assignCreatedFromTemplate(Item template) { createdFromTemplate = template; } + + void addTemplateInstance(Item item) { + templateInstances.add(item); + } + + @Override + public List getItemsCreatedFromThisTemplateItem() { + return templateInstances; + } } private static TestMachine testMachine(String name) { @@ -493,6 +577,8 @@ public List searchEntitiesNoEntityType(String value) { private static class TestUtility extends ItemDomainMachineDesignControllerUtility { private int updateCount; + private boolean failUpdate; + private final List publishedEvents = new ArrayList<>(); void useFacade(ItemDomainMachineDesignFacade facade) { itemFacade = facade; @@ -508,9 +594,17 @@ public ItemDomainMachineDesign createEntityInstanceBasedOnParent( @Override public ItemDomainMachineDesign update(ItemDomainMachineDesign entity, - UserInfo updatedByUserInfo) { + UserInfo updatedByUserInfo) throws CdbException { updateCount++; + if (failUpdate) { + throw new CdbException("update failed"); + } return entity; } + + @Override + protected void publishMqttEvent(MqttEvent event) { + publishedEvents.add(event); + } } } diff --git a/tools/developer_tools/code_testing/CdbWebPortalTest/src/test/java/gov/anl/aps/cdb/portal/controllers/utilities/ItemLocationEventDetectionTest.java b/tools/developer_tools/code_testing/CdbWebPortalTest/src/test/java/gov/anl/aps/cdb/portal/controllers/utilities/ItemLocationEventDetectionTest.java new file mode 100644 index 000000000..884413b4c --- /dev/null +++ b/tools/developer_tools/code_testing/CdbWebPortalTest/src/test/java/gov/anl/aps/cdb/portal/controllers/utilities/ItemLocationEventDetectionTest.java @@ -0,0 +1,69 @@ +/* + * Copyright (c) UChicago Argonne, LLC. All rights reserved. + * See LICENSE file. + */ +package gov.anl.aps.cdb.portal.controllers.utilities; + +import gov.anl.aps.cdb.base.mqtt.MqttEvent; +import gov.anl.aps.cdb.mqtt.CdbItemLocationEvent; +import gov.anl.aps.cdb.mqtt.metadata.ItemLocationChange; +import gov.anl.aps.cdb.portal.model.db.entities.Item; +import gov.anl.aps.cdb.portal.model.db.entities.ItemDomainInventory; +import gov.anl.aps.cdb.portal.model.db.entities.ItemDomainLocation; +import gov.anl.aps.cdb.portal.model.db.entities.ItemElement; +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; +import org.junit.Test; +import static org.junit.Assert.*; + +public class ItemLocationEventDetectionTest { + + @Test + public void convertsPendingLocationChangeToEvent() throws Exception { + ItemDomainInventory affected = inventory(1, "Device"); + ItemDomainLocation oldLocation = location(2, "Old"); + ItemDomainLocation newLocation = location(3, "New"); + affected.setPendingLocationChange(new ItemLocationChange( + oldLocation, "old details", newLocation, "new details")); + + List events = new TestUtility().events(affected); + + assertEquals(1, events.size()); + CdbItemLocationEvent event = (CdbItemLocationEvent) events.get(0); + assertEquals("2", event.getOldLocation().getId()); + assertEquals("3", event.getNewLocation().getId()); + } + + @Test + public void itemWithoutPendingChangeProducesNoLocationEvent() throws Exception { + assertTrue(new TestUtility().events(inventory(1, "Device")).isEmpty()); + } + + private static ItemDomainInventory inventory(int id, String name) { + ItemDomainInventory item = new ItemDomainInventory(); + initialize(item, id, name); + return item; + } + + private static ItemDomainLocation location(int id, String name) { + ItemDomainLocation item = new ItemDomainLocation(); + initialize(item, id, name); + return item; + } + + private static void initialize(Item item, int id, String name) { + item.setId(id); + item.setName(name); + item.setEntityTypeListFromApi(new ArrayList<>()); + ItemElement self = new ItemElement(); + self.setParentItem(item); + item.setFullItemElementList(new ArrayList<>(Collections.singletonList(self))); + } + + private static final class TestUtility extends ItemDomainInventoryControllerUtility { + List events(ItemDomainInventory item) throws Exception { + return prepareSpecializedUpdateEvents(item, null); + } + } +} diff --git a/tools/developer_tools/code_testing/CdbWebPortalTest/src/test/java/gov/anl/aps/cdb/portal/controllers/utilities/ItemPropertyEventDetectionTest.java b/tools/developer_tools/code_testing/CdbWebPortalTest/src/test/java/gov/anl/aps/cdb/portal/controllers/utilities/ItemPropertyEventDetectionTest.java new file mode 100644 index 000000000..b4f33b056 --- /dev/null +++ b/tools/developer_tools/code_testing/CdbWebPortalTest/src/test/java/gov/anl/aps/cdb/portal/controllers/utilities/ItemPropertyEventDetectionTest.java @@ -0,0 +1,145 @@ +/* + * Copyright (c) UChicago Argonne, LLC. All rights reserved. + * See LICENSE file. + */ +package gov.anl.aps.cdb.portal.controllers.utilities; + +import gov.anl.aps.cdb.base.mqtt.MqttEvent; +import gov.anl.aps.cdb.mqtt.CdbItemPropertyEvent; +import gov.anl.aps.cdb.portal.model.db.beans.ItemDomainCatalogFacade; +import gov.anl.aps.cdb.portal.model.db.entities.EntityInfo; +import gov.anl.aps.cdb.portal.model.db.entities.ItemDomainCatalog; +import gov.anl.aps.cdb.portal.model.db.entities.ItemElement; +import gov.anl.aps.cdb.portal.model.db.entities.PropertyType; +import gov.anl.aps.cdb.portal.model.db.entities.PropertyValue; +import gov.anl.aps.cdb.portal.model.db.entities.UserInfo; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collections; +import java.util.Date; +import java.util.List; +import org.junit.Test; +import static org.junit.Assert.*; + +public class ItemPropertyEventDetectionTest { + + @Test + public void classifiesAddUpdateAndDeleteByStableId() throws Exception { + ItemDomainCatalog original = item(1, + property(10, "unchanged", "a"), + property(11, "updated", "old"), + property(12, "deleted", "gone")); + ItemDomainCatalog edited = item(1, + property(10, "unchanged", "a"), + property(11, "updated", "new"), + property(null, "added", "here")); + TestUtility utility = new TestUtility(original); + + List events = utility.detect(edited); + + assertEquals(3, events.size()); + assertEquals("cdb/item/property/update", events.get(0).getTopic()); + assertEquals("cdb/item/property/add", events.get(1).getTopic()); + assertEquals("cdb/item/property/delete", events.get(2).getTopic()); + CdbItemPropertyEvent update = (CdbItemPropertyEvent) events.get(0); + assertEquals("old", update.getOldProperty().getValue()); + assertEquals("new", update.getNewProperty().getValue()); + } + + @Test + public void identicalPropertiesProduceNoEvents() throws Exception { + ItemDomainCatalog original = item(1, property(10, "status", "Installed")); + ItemDomainCatalog edited = item(1, property(10, "status", "Installed")); + + assertTrue(new TestUtility(original).detect(edited).isEmpty()); + } + + @Test + public void removalLifecycleProducesDeleteEvent() throws Exception { + ItemDomainCatalog original = item(1, property(10, "removed", "old")); + ItemDomainCatalog edited = item(1); + TestUtility utility = new TestUtility(original); + + utility.detectRemoval(edited); + List events = utility.detectPending(edited); + + assertEquals(1, events.size()); + CdbItemPropertyEvent event = (CdbItemPropertyEvent) events.get(0); + assertEquals("cdb/item/property/delete", event.getTopic()); + assertEquals("old", event.getOldProperty().getValue()); + assertNull(event.getNewProperty()); + } + + @Test + public void statusPropertyUsesOrdinaryPropertyUpdateEvent() throws Exception { + ItemDomainCatalog original = item(1, property(10, "inventory_status", "Available")); + ItemDomainCatalog edited = item(1, property(10, "inventory_status", "Installed")); + + CdbItemPropertyEvent event = (CdbItemPropertyEvent) + new TestUtility(original).detect(edited).get(0); + + assertEquals("inventory_status", event.getNewProperty().getPropertyTypeName()); + assertEquals("Available", event.getOldProperty().getValue()); + assertEquals("Installed", event.getNewProperty().getValue()); + } + + private static PropertyValue property(Integer id, String typeName, String valueText) { + PropertyValue value = new PropertyValue(); + value.setId(id); + PropertyType propertyType = new PropertyType(100 + (id == null ? 0 : id), typeName); + propertyType.setPropertyTypeMetadataList(new ArrayList<>()); + value.setPropertyType(propertyType); + value.setValue(valueText); + value.setPropertyMetadataList(new ArrayList<>()); + return value; + } + + private static ItemDomainCatalog item(int id, PropertyValue... values) { + ItemDomainCatalog item = new ItemDomainCatalog(); + item.setId(id); + item.setName("Item"); + ItemElement self = new ItemElement(); + self.setParentItem(item); + self.setPropertyValueList(new ArrayList<>(Arrays.asList(values))); + item.setFullItemElementList(new ArrayList<>(Collections.singletonList(self))); + return item; + } + + private static final class TestUtility extends ItemDomainCatalogControllerUtility { + private final ItemDomainCatalog original; + + TestUtility(ItemDomainCatalog original) { + this.original = original; + } + + @Override + public ItemDomainCatalog findById(Integer id) { + return original; + } + + void detectRemoval(ItemDomainCatalog edited) throws Exception { + prepareEntityUpdateOnRemoval(edited); + } + + List detectPending(ItemDomainCatalog edited) throws Exception { + return prepareSpecializedUpdateEvents(edited, new UserInfo()); + } + + List detect(ItemDomainCatalog edited) throws Exception { + EntityInfo entityInfo = new EntityInfo(); + entityInfo.setLastModifiedByUser(new UserInfo()); + entityInfo.setLastModifiedOnDateTime(new Date()); + for (PropertyValue value : original.getPropertyValueList()) { + value.setPropertyValueHistoryList(new ArrayList<>()); + } + for (PropertyValue value : edited.getPropertyValueList()) { + value.setPropertyValueHistoryList(new ArrayList<>()); + } + edited.setPendingPropertyValueChanges( + gov.anl.aps.cdb.portal.model.db.utilities.PropertyValueUtility + .preparePropertyValueHistory(original.getPropertyValueList(), + edited.getPropertyValueList(), entityInfo)); + return prepareSpecializedUpdateEvents(edited, new UserInfo()); + } + } +} diff --git a/tools/developer_tools/code_testing/CdbWebPortalTest/src/test/java/gov/anl/aps/cdb/portal/controllers/utilities/MachineAssignedItemEventDetectionTest.java b/tools/developer_tools/code_testing/CdbWebPortalTest/src/test/java/gov/anl/aps/cdb/portal/controllers/utilities/MachineAssignedItemEventDetectionTest.java new file mode 100644 index 000000000..6e2e094fa --- /dev/null +++ b/tools/developer_tools/code_testing/CdbWebPortalTest/src/test/java/gov/anl/aps/cdb/portal/controllers/utilities/MachineAssignedItemEventDetectionTest.java @@ -0,0 +1,104 @@ +/* + * Copyright (c) UChicago Argonne, LLC. All rights reserved. + * See LICENSE file. + */ +package gov.anl.aps.cdb.portal.controllers.utilities; + +import gov.anl.aps.cdb.base.mqtt.MqttEvent; +import gov.anl.aps.cdb.mqtt.CdbMachineAssignedItemEvent; +import gov.anl.aps.cdb.portal.model.db.entities.Item; +import gov.anl.aps.cdb.portal.model.db.entities.ItemDomainCatalog; +import gov.anl.aps.cdb.portal.model.db.entities.ItemDomainMachineDesign; +import gov.anl.aps.cdb.portal.model.db.entities.ItemElement; +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; +import org.junit.Test; +import static org.junit.Assert.*; + +public class MachineAssignedItemEventDetectionTest { + + @Test + public void detectsAssignReplaceAndUnassign() throws Exception { + ItemDomainCatalog first = catalog(10, "First"); + ItemDomainCatalog second = catalog(11, "Second"); + assertTransition(null, first); + assertTransition(first, second); + assertTransition(first, null); + } + + @Test + public void detectsInstalledStateOnlyChange() throws Exception { + ItemDomainCatalog assigned = catalog(10, "Assigned"); + ItemDomainMachineDesign original = machine(1, assigned, true); + ItemDomainMachineDesign edited = machine(1, assigned, false); + TestUtility utility = new TestUtility(); + + utility.detect(original, edited); + CdbMachineAssignedItemEvent event = event(utility.events(edited)); + + assertTrue(event.isOldInstalled()); + assertFalse(event.isNewInstalled()); + } + + @Test + public void suppressesNoOp() throws Exception { + ItemDomainCatalog assigned = catalog(10, "Assigned"); + ItemDomainMachineDesign original = machine(1, assigned, true); + ItemDomainMachineDesign edited = machine(1, assigned, true); + TestUtility utility = new TestUtility(); + + utility.detect(original, edited); + + assertTrue(utility.events(edited).isEmpty()); + } + + private static void assertTransition(Item oldItem, Item newItem) throws Exception { + ItemDomainMachineDesign original = machine(1, oldItem, true); + ItemDomainMachineDesign edited = machine(1, newItem, true); + TestUtility utility = new TestUtility(); + utility.detect(original, edited); + CdbMachineAssignedItemEvent event = event(utility.events(edited)); + assertEquals(oldItem == null ? null : oldItem.getId().toString(), + event.getOldAssignedItem() == null ? null : event.getOldAssignedItem().getId()); + assertEquals(newItem == null ? null : newItem.getId().toString(), + event.getNewAssignedItem() == null ? null : event.getNewAssignedItem().getId()); + } + + private static CdbMachineAssignedItemEvent event(List events) { + assertEquals(1, events.size()); + return (CdbMachineAssignedItemEvent) events.get(0); + } + + private static ItemDomainMachineDesign machine(int id, Item assigned, boolean installed) { + ItemDomainMachineDesign machine = new ItemDomainMachineDesign(); + initialize(machine, id, "Machine"); + machine.setAssignedItem(assigned); + machine.setIsHoused(installed); + return machine; + } + + private static ItemDomainCatalog catalog(int id, String name) { + ItemDomainCatalog item = new ItemDomainCatalog(); + initialize(item, id, name); + return item; + } + + private static void initialize(Item item, int id, String name) { + item.setId(id); + item.setName(name); + ItemElement self = new ItemElement(); + self.setParentItem(item); + item.setFullItemElementList(new ArrayList<>(Collections.singletonList(self))); + } + + private static final class TestUtility extends ItemDomainMachineDesignControllerUtility { + void detect(ItemDomainMachineDesign original, ItemDomainMachineDesign edited) { + prepareMachineAssignedItemChange(original, edited); + } + + List events(ItemDomainMachineDesign edited) throws Exception { + return prepareSpecializedUpdateEvents(edited, null); + } + } +} diff --git a/tools/developer_tools/code_testing/CdbWebPortalTest/src/test/java/gov/anl/aps/cdb/portal/controllers/utilities/MachineRepresentedElementEventDetectionTest.java b/tools/developer_tools/code_testing/CdbWebPortalTest/src/test/java/gov/anl/aps/cdb/portal/controllers/utilities/MachineRepresentedElementEventDetectionTest.java new file mode 100644 index 000000000..eb4a667ea --- /dev/null +++ b/tools/developer_tools/code_testing/CdbWebPortalTest/src/test/java/gov/anl/aps/cdb/portal/controllers/utilities/MachineRepresentedElementEventDetectionTest.java @@ -0,0 +1,123 @@ +/* + * Copyright (c) UChicago Argonne, LLC. All rights reserved. + * See LICENSE file. + */ +package gov.anl.aps.cdb.portal.controllers.utilities; + +import gov.anl.aps.cdb.base.mqtt.MqttEvent; +import gov.anl.aps.cdb.mqtt.CdbMachineRepresentedElementEvent; +import gov.anl.aps.cdb.portal.model.db.entities.Item; +import gov.anl.aps.cdb.portal.model.db.entities.ItemDomainCatalog; +import gov.anl.aps.cdb.portal.model.db.entities.ItemDomainMachineDesign; +import gov.anl.aps.cdb.portal.model.db.entities.ItemElement; +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; +import org.junit.Test; +import static org.junit.Assert.*; + +public class MachineRepresentedElementEventDetectionTest { + + @Test + public void detectsSetReplaceAndClear() throws Exception { + ItemElement first = element(10, "E1"); + ItemElement second = element(11, "E2"); + assertTransition(null, first); + assertTransition(first, second); + assertTransition(first, null); + } + + @Test + public void includesPropagatedInstanceEventWithTemplateContext() throws Exception { + ItemElement oldElement = element(10, "E1"); + ItemElement newElement = element(11, "E2"); + ItemDomainMachineDesign template = machine(1, newElement); + ItemDomainMachineDesign instance = machine(2, newElement); + instance.setPendingRepresentedElementChange(oldElement, template); + template.addItemToUpdate(instance); + TestUtility utility = new TestUtility(); + + List events = utility.events(template); + + assertEquals(1, events.size()); + CdbMachineRepresentedElementEvent event + = (CdbMachineRepresentedElementEvent) events.get(0); + assertEquals("2", event.getMachine().getId()); + assertEquals("1", event.getSourceTemplate().getId()); + assertEquals("10", event.getOldRepresentedElement().getId()); + assertEquals("11", event.getNewRepresentedElement().getId()); + } + + @Test + public void suppressesNoOp() throws Exception { + ItemElement represented = element(10, "E1"); + ItemDomainMachineDesign original = machine(1, represented); + ItemDomainMachineDesign edited = machine(1, represented); + TestUtility utility = new TestUtility(); + + utility.detect(original, edited); + + assertTrue(utility.events(edited).isEmpty()); + } + + private static void assertTransition(ItemElement oldElement, ItemElement newElement) + throws Exception { + ItemDomainMachineDesign original = machine(1, oldElement); + ItemDomainMachineDesign edited = machine(1, newElement); + TestUtility utility = new TestUtility(); + utility.detect(original, edited); + List events = utility.events(edited); + assertEquals(1, events.size()); + CdbMachineRepresentedElementEvent event + = (CdbMachineRepresentedElementEvent) events.get(0); + assertEquals(oldElement == null ? null : oldElement.getId().toString(), + event.getOldRepresentedElement() == null + ? null : event.getOldRepresentedElement().getId()); + assertEquals(newElement == null ? null : newElement.getId().toString(), + event.getNewRepresentedElement() == null + ? null : event.getNewRepresentedElement().getId()); + } + + private static ItemElement element(int id, String name) { + ItemDomainCatalog assembly = catalog(100 + id, "Assembly"); + ItemDomainCatalog part = catalog(200 + id, "Part"); + ItemElement element = new ItemElement(); + element.setId(id); + element.setName(name); + element.setParentItem(assembly); + element.setContainedItem(part); + return element; + } + + private static ItemDomainMachineDesign machine(int id, ItemElement represented) { + ItemDomainMachineDesign item = new ItemDomainMachineDesign(); + initialize(item, id, "Machine"); + item.setRepresentsCatalogElement(represented); + return item; + } + + private static ItemDomainCatalog catalog(int id, String name) { + ItemDomainCatalog item = new ItemDomainCatalog(); + initialize(item, id, name); + return item; + } + + private static void initialize(Item item, int id, String name) { + item.setId(id); + item.setName(name); + item.setEntityTypeListFromApi(new ArrayList<>()); + ItemElement self = new ItemElement(); + self.setParentItem(item); + item.setFullItemElementList(new ArrayList<>(Collections.singletonList(self))); + } + + private static final class TestUtility extends ItemDomainMachineDesignControllerUtility { + void detect(ItemDomainMachineDesign original, ItemDomainMachineDesign edited) { + prepareMachineRepresentedElementChange(original, edited); + } + + List events(ItemDomainMachineDesign edited) throws Exception { + return prepareSpecializedUpdateEvents(edited, null); + } + } +} diff --git a/tools/developer_tools/code_testing/CdbWebPortalTest/src/test/java/gov/anl/aps/cdb/portal/controllers/utilities/MachineTrashEventDetectionTest.java b/tools/developer_tools/code_testing/CdbWebPortalTest/src/test/java/gov/anl/aps/cdb/portal/controllers/utilities/MachineTrashEventDetectionTest.java new file mode 100644 index 000000000..71ecec5e7 --- /dev/null +++ b/tools/developer_tools/code_testing/CdbWebPortalTest/src/test/java/gov/anl/aps/cdb/portal/controllers/utilities/MachineTrashEventDetectionTest.java @@ -0,0 +1,62 @@ +/* + * Copyright (c) UChicago Argonne, LLC. All rights reserved. + * See LICENSE file. + */ +package gov.anl.aps.cdb.portal.controllers.utilities; + +import gov.anl.aps.cdb.base.mqtt.MqttEvent; +import gov.anl.aps.cdb.mqtt.CdbMachineTrashEvent; +import gov.anl.aps.cdb.mqtt.CdbMqttTopic; +import gov.anl.aps.cdb.mqtt.metadata.MachineTrashChange; +import gov.anl.aps.cdb.portal.model.db.entities.ItemDomainMachineDesign; +import gov.anl.aps.cdb.portal.model.db.entities.ItemElement; +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; +import org.junit.Test; +import static org.junit.Assert.*; + +public class MachineTrashEventDetectionTest { + + @Test + public void createsOneEventPerAffectedMachine() throws Exception { + ItemDomainMachineDesign root = machine(1, "Root"); + ItemDomainMachineDesign child = machine(2, "Child"); + root.setPendingTrashChange(new MachineTrashChange( + CdbMqttTopic.MACHINE_DESIGN_TRASH, root, 2)); + child.setPendingTrashChange(new MachineTrashChange( + CdbMqttTopic.MACHINE_DESIGN_TRASH, root, 2)); + TestUtility utility = new TestUtility(); + + List rootEvents = utility.events(root); + List childEvents = utility.events(child); + + assertEquals(1, rootEvents.size()); + assertEquals(1, childEvents.size()); + CdbMachineTrashEvent childEvent = (CdbMachineTrashEvent) childEvents.get(0); + assertEquals("1", childEvent.getRequestedHierarchyRoot().getId()); + assertEquals(2, childEvent.getAffectedMachineCount()); + } + + @Test + public void itemWithoutPendingTransitionProducesNoEvent() throws Exception { + assertTrue(new TestUtility().events(machine(1, "Root")).isEmpty()); + } + + private static ItemDomainMachineDesign machine(int id, String name) { + ItemDomainMachineDesign item = new ItemDomainMachineDesign(); + item.setId(id); + item.setName(name); + item.setEntityTypeListFromApi(new ArrayList<>()); + ItemElement self = new ItemElement(); + self.setParentItem(item); + item.setFullItemElementList(new ArrayList<>(Collections.singletonList(self))); + return item; + } + + private static final class TestUtility extends ItemDomainMachineDesignControllerUtility { + List events(ItemDomainMachineDesign item) throws Exception { + return prepareSpecializedUpdateEvents(item, null); + } + } +}