diff --git a/.gitignore b/.gitignore index 90726d60b..897eb3eee 100644 --- a/.gitignore +++ b/.gitignore @@ -59,3 +59,5 @@ tools/developer_tools/python-client/cdbCli/service/cli/Spreadsheets/ .env tools/developer_tools/bely-mqtt-message-broker/conda-bld tools/developer_tools/bely-mqtt-message-broker/dev-config +tools/developer_tools/bely-mqtt-message-broker/conda-recipe/bely-mqtt-env.txt +tools/developer_tools/bely-mqtt-message-broker/conda-recipe/src diff --git a/README.md b/README.md index aad5775c1..6607d78d1 100644 --- a/README.md +++ b/README.md @@ -1,9 +1,9 @@ -# BELY +# BELY **Prerequisites:** In order to deploy or develop BELY, you must have some support software installed. Follow the instructions below to achieve this. - + # For red-hat based linux distribution run the following: yum install -y gcc libgcc expect zlib-devel openssl-devel openldap-devel readline-devel git make cmake sed gawk autoconf automake wget mysql mysql-libs mysql-server mysql-devel curl unzip # For debian based linux distributions run the following: @@ -16,28 +16,29 @@ For detailed deployment instructions please refer to our [administrators guide]( # Make a new directory to hold cdb and its support directories. (replace or set DESIRED_CDB_INSTALL_DIRECTORY var with a unix directory.) mkdir $DESIRED_CDB_INSTALL_DIRECTORY - cd $DESIRED_CDB_INSTALL_DIRECTORY - # get the distribution of Component DB (Alternativelly download a release zip and unzip it). + cd $DESIRED_CDB_INSTALL_DIRECTORY + # get the distribution of Component DB (Alternativelly download a release zip and unzip it). git clone https://github.com/AdvancedPhotonSource/ComponentDB.git - # Navigate inside the distribution. + # Navigate inside the distribution. cd ComponentDb # Build support needed for the application make support - # load enviornment variables with new support built. - source setup.sh + # load enviornment variables with new support built. + source setup.sh # Create deployment configuration make configuration - # Create a clean db for the distribution + # Create a clean db for the distribution make clean-db # Prepare web portal configuraiton make configure-web-portal + # Configure MQTT (optional - needed for notification features) + ./sbin/bely_create_mqtt_configuration.sh + ./sbin/bely_configure_mqtt_service.sh # Deploy web portal make deploy-web-portal - # Deploy REST web service - make deploy-web-service - - # All done... output of the command below should print url to the deployed portal. - echo "https://`hostname`:8181/cdb" + + # All done... output of the command below should print url to the deployed portal. + echo "https://`hostname`:8181/bely" # Development @@ -45,50 +46,50 @@ For detailed development instructions please refer to our [developers guide](htt **Getting Started with development:** - # first make a fork of this project. - # create a desired development directory and cdb into it + # first make a fork of this project. + # create a desired development directory and clone into it mkdir $desired_dev_directory cd $desired_dev_directory git clone https://github.com/AdvancedPhotonSource/ComponentDB.git - - # Getting support software cd ComponentDb - make support + + # Getting support software + make support # Get Netbeans make support-netbeans - # Load up the environment + # Load up the environment source setup.sh - # Prepare Dev DB - # mysql could be installed as part of ComponentDB support by running 'make support-mysql' + # Prepare Dev DB + # mysql could be installed as part of support by running 'make support-mysql' # - Afterwards run `./etc/init.d/cdb-mysql start` # if you have mysql installed and started run... - make clean-db # sample-db will be coming later - + make clean-db + # Start development - make dev-config + make dev-config # Open Netbeans - netbeans & + netbeans & ## Preparing Netbeans -Once netbeans is open a few steps need to be taken to prepare netbeans for CDB development. -1. Open CDB Project: File > Open Project +Once netbeans is open a few steps need to be taken to prepare netbeans for BELY development. +1. Open BELY Project: File > Open Project 2. Navigate to $desired_dev_directory/ComponentDB/src/java -3. Select CdbWebPortal and hit Open Project -4. Right click on CdbWebPortal top level under projects +3. Select LogrPortal and hit Open Project +4. Right click on LogrPortal top level under projects 5. Click "Resolve Missing Server Problem" 6. Add Server -> Payara Server - Installation Location: $desired_dev_directory/support-`hostname`/netbeans/payara - Version: 5.2022.5 - - Use the wizard's download + - Use the wizard's download 7. Next -> Use Default Domain Location -> Finish add server instance wizard 8. Select the Newly added "Payara Server" -9. Copy over the required mysql client to new payara server. -```sh +9. Copy over the required mysql client to new payara server. +```sh # cd into the $desired_dev_directory/$distribution_directory -cp src/java/CdbWebPortal/lib/mariadb-java-client-3.1.0.jar ../support-`hostname`/netbeans/payara/glassfish/domains/domain1/lib/ +cp src/java/LogrPortal/lib/mariadb-java-client-3.1.0.jar ../support-`hostname`/netbeans/payara/glassfish/domains/domain1/lib/ ``` 10. Run the project @@ -126,10 +127,5 @@ source setup.sh make test ``` -## Python Web Service Development - # Code is located in $desired_dev_directory/ComponentDB/src/python - # For web service development (Use your favorite python editor) to test run web service using: - ./sbin/cdbWebService.sh - # License [Copyright (c) UChicago Argonne, LLC. All rights reserved.](https://github.com/AdvancedPhotonSource/ComponentDB/blob/master/LICENSE) diff --git a/db/sql/static/populate_notification_provider.sql b/db/sql/static/populate_notification_provider.sql index 722d2b62e..c7c372c54 100644 --- a/db/sql/static/populate_notification_provider.sql +++ b/db/sql/static/populate_notification_provider.sql @@ -1,9 +1,7 @@ LOCK TABLES `notification_provider` WRITE; /*!40000 ALTER TABLE `notification_provider` DISABLE KEYS */; INSERT INTO `notification_provider` VALUES -(1,'apprise', 'Apprise unified notification library supporting email, Discord, Slack, Teams, etc.', '# Apprise Notification URLs - -[Apprise](https://github.com/caronc/apprise) supports many notification services. Below are some common URL formats. +(1,'apprise', 'Sends notifications to email, Discord, Slack, Teams, and more.', '# Notification URL Examples ## Email (SMTP) @@ -29,6 +27,10 @@ slack://TokenA/TokenB/TokenC/#channel json://hostname/path ``` -For the full list of supported services, see the [Apprise Serivces Page](https://appriseit.com/services).'); +For the full list of supported services, see the [Apprise Services Page](https://appriseit.com/services). + +--- + +**Note:** Notifications are powered by [Apprise](https://github.com/caronc/apprise), an open-source notification library hosted locally.'); /*!40000 ALTER TABLE `notification_provider` ENABLE KEYS */; UNLOCK TABLES; diff --git a/db/sql/updates/dev/2026.3.dev1.sql b/db/sql/updates/dev/2026.3.dev1.sql index d5dd9055c..8430dc38b 100644 --- a/db/sql/updates/dev/2026.3.dev1.sql +++ b/db/sql/updates/dev/2026.3.dev1.sql @@ -5,9 +5,9 @@ ALTER TABLE `notification_provider` ADD COLUMN `instructions` TEXT DEFAULT NULL; -UPDATE `notification_provider` SET `instructions` = '# Apprise Notification URLs - -[Apprise](https://github.com/caronc/apprise) supports many notification services. Below are some common URL formats. +UPDATE `notification_provider` SET + `description` = 'Sends notifications to email, Discord, Slack, Teams, and more.', + `instructions` = '# Notification URL Examples ## Email (SMTP) @@ -33,5 +33,9 @@ slack://TokenA/TokenB/TokenC/#channel json://hostname/path ``` -For the full list of supported services, see the [Apprise Serivces Page](https://appriseit.com/services).' +For the full list of supported services, see the [Apprise Services Page](https://appriseit.com/services). + +--- + +**Note:** Notifications are powered by [Apprise](https://github.com/caronc/apprise), an open-source notification library hosted locally.' WHERE `name` = 'apprise'; diff --git a/db/sql/updates/updateTo2026.3.sql b/db/sql/updates/updateTo2026.3.sql index b71e99001..b2fb13e58 100644 --- a/db/sql/updates/updateTo2026.3.sql +++ b/db/sql/updates/updateTo2026.3.sql @@ -119,9 +119,7 @@ CREATE TABLE `notification_configuration_handler_setting` ( -- INSERT INTO `notification_provider` VALUES -(1,'apprise', 'Apprise unified notification library supporting email, Discord, Slack, Teams, etc.', '# Apprise Notification URLs - -[Apprise](https://github.com/caronc/apprise) supports many notification services. Below are some common URL formats. +(1,'apprise', 'Sends notifications to email, Discord, Slack, Teams, and more.', '# Notification URL Examples ## Email (SMTP) @@ -147,7 +145,11 @@ slack://TokenA/TokenB/TokenC/#channel json://hostname/path ``` -For the full list of supported services, see the [Apprise Serivces Page](https://appriseit.com/services).'); +For the full list of supported services, see the [Apprise Services Page](https://appriseit.com/services). + +--- + +**Note:** Notifications are powered by [Apprise](https://github.com/caronc/apprise), an open-source notification library hosted locally.'); -- -- Populate notification_handler_config_key -- diff --git a/docs/release-notes/2026.3.md b/docs/release-notes/2026.3.md index 68428ea98..527c5063b 100644 --- a/docs/release-notes/2026.3.md +++ b/docs/release-notes/2026.3.md @@ -25,6 +25,9 @@ - Add notification configuration API. - Add automatic date parameter conversion for REST API endpoints. +- Add ability to upload attachments to log entries from API. +- Add MD5 checksum endpoint for attachments. +- Add endpoint to fetch log document by name. ## MQTT Integration @@ -66,3 +69,7 @@ ## Other - Upgrade Log4j. + +## Bugs + +- Fix search results returning null logbook type in attribute match map. diff --git a/docs/update/v2026.3.md b/docs/update/v2026.3.md new file mode 100644 index 000000000..2f922e809 --- /dev/null +++ b/docs/update/v2026.3.md @@ -0,0 +1,71 @@ +# Update BELY to v2026.3 +This release contains significant infrastructure changes including an updated Payara installation, MQTT integration for notifications, new notification database tables, and updated search stored procedures. + +## UI Improvements +- Cleaned up the add log entry dialog by removing unnecessary labels, resulting in a wider text area while keeping the dialog size the same + +# Update Instructions + +## 1. Pre-Migration: Stop Services and Backup +```bash +source setup.sh +./etc/init.d/cdb-glassfish stop +make backup +``` + +## 2. Update Source Code +Pull or download the 2026.3 release, then reload the environment: +```bash +source setup.sh +``` + +## 3. Reinstall Payara +The Payara installation script has been updated to create a production domain with improved security defaults, keystore configuration, and JVM options. +```bash +# Backup old Payara to dated snapshot +mv $LOGR_SUPPORT_DIR/payara $LOGR_SUPPORT_DIR/payara-$(date +%Y%m%d) + +# Copy updated install script into existing support directory +cp support/bin/install_glassfish.sh $LOGR_SUPPORT_DIR/bin/ + +# Run Payara install (creates production domain, prompts for passwords, +# configures keystores, enables HTTPS/secure admin, sets JVM options) +$LOGR_SUPPORT_DIR/bin/install_glassfish.sh +``` + +## 4. Configure SSL Certificates (if using custom certs) +If you use custom SSL certificates, re-import them into the new Payara installation: +```bash +./sbin/cdb_update_glassfish_ssl.sh +``` + +## 5. Database Migration +```bash +mysql logr --host=127.0.0.1 --user=logr -p < db/sql/updates/updateTo2026.3.sql +``` +This migration adds notification tables, handler configuration tables, and updated search stored procedures. + +## 6. Configure MQTT (New in 2026.3) +MQTT integration enables real-time notification features. An MQTT broker service (e.g. Mosquitto) must be running and accessible before configuring MQTT. + +```bash +# Create MQTT configuration interactively (saves to $LOGR_INSTALL_DIR/etc/mqtt.conf) +./sbin/bely_create_mqtt_configuration.sh + +# Apply MQTT configuration to Payara (deploys RAR, creates pool/resource, tests connection) +./sbin/bely_configure_mqtt_service.sh +``` + +## 7. Configure and Deploy +```bash +make configure-web-portal +make deploy-cdb-plugin +make deploy-web-portal +``` + +## 8. Configure MQTT Message Broker (Optional) +The MQTT message broker handler processes notification events (email, Slack, Discord). See `tools/developer_tools/bely-mqtt-message-broker/README.md` for setup details. + +## 9. Verification +- Confirm the application is accessible at `https://:8181/bely` +- Check Payara logs for MQTT connectivity diff --git a/etc/version b/etc/version index c69893086..2ff29dfca 100644 --- a/etc/version +++ b/etc/version @@ -1 +1 @@ -2025.10 +2026.3 diff --git a/sbin/cdb_set_user_password.sh b/sbin/cdb_set_user_password.sh new file mode 100755 index 000000000..0f2395a99 --- /dev/null +++ b/sbin/cdb_set_user_password.sh @@ -0,0 +1,115 @@ +#!/bin/bash + +# Copyright (c) UChicago Argonne, LLC. All rights reserved. +# See LICENSE file. + + +# +# Script used for setting/resetting a system user password +# Deployment configuration can be set in etc/$LOGR_DB_NAME.deploy.conf file +# +# Usage: +# +# $0 USERNAME [LOGR_DB_NAME] +# + +LOGR_DB_NAME=logr +LOGR_DB_USER=logr +LOGR_DB_HOST=127.0.0.1 +LOGR_DB_PORT=3306 + +CURRENT_DIR=`pwd` +MY_DIR=`dirname $0` && cd $MY_DIR && MY_DIR=`pwd` +cd $CURRENT_DIR + +if [ -z "${LOGR_ROOT_DIR}" ]; then + LOGR_ROOT_DIR=$MY_DIR/.. +fi +LOGR_ENV_FILE=${LOGR_ROOT_DIR}/setup.sh +if [ ! -f ${LOGR_ENV_FILE} ]; then + echo "Environment file ${LOGR_ENV_FILE} does not exist." + exit 1 +fi +. ${LOGR_ENV_FILE} > /dev/null + +# First argument is the username (required) +if [ -z "$1" ]; then + echo "Usage: $0 USERNAME [LOGR_DB_NAME]" + exit 1 +fi +USERNAME=$1 + +# Use second argument as db name, if provided +if [ ! -z "$2" ]; then + LOGR_DB_NAME=$2 + LOGR_DB_USER=$2 +fi +echo "Using DB name: $LOGR_DB_NAME" + +# Look for deployment file in etc directory, and use it to override +# default entries +deployConfigFile=$LOGR_INSTALL_DIR/etc/${LOGR_DB_NAME}.deploy.conf +if [ -f $deployConfigFile ]; then + echo "Using deployment config file: $deployConfigFile" + . $deployConfigFile +fi + +# Check for database passwd file +databasePasswdFile=$LOGR_INSTALL_DIR/etc/$LOGR_DB_NAME.db.passwd +if [ -f $databasePasswdFile ]; then + LOGR_DB_PASSWORD=`cat $databasePasswdFile` +else + if [ -t 0 ]; then + read -s -p "Enter MySQL $LOGR_DB_NAME password: " LOGR_DB_PASSWORD + echo + else + >&2 echo "ERROR: $databasePasswdFile does not exist" + exit 1 + fi +fi + +if [ -z "$LOGR_DB_PASSWORD" ]; then + >&2 echo "ERROR: database password is blank" + exit 1 +fi + +mysqlCmd="mysql $LOGR_DB_NAME --port=$LOGR_DB_PORT --host=$LOGR_DB_HOST -u $LOGR_DB_USER -p$LOGR_DB_PASSWORD" + +# Validate that the user exists +userExists=`echo "SELECT username FROM user_info WHERE username='$USERNAME';" | eval $mysqlCmd --skip-column-names 2>/dev/null` +if [ -z "$userExists" ]; then + echo "ERROR: User '$USERNAME' not found in the $LOGR_DB_NAME database." + exit 1 +fi + +# Prompt for new password +read -sp "Enter new password for user '$USERNAME': " NEW_PASSWORD +echo +read -sp "Confirm new password: " CONFIRM_PASSWORD +echo + +if [ -z "$NEW_PASSWORD" ]; then + echo "ERROR: Password cannot be blank." + exit 1 +fi + +if [ "$NEW_PASSWORD" != "$CONFIRM_PASSWORD" ]; then + echo "ERROR: Passwords do not match." + exit 1 +fi + +# Hash the password using the existing Python utility +CRYPTED_PASSWORD=`python -c "from cdb.common.utility.cryptUtility import CryptUtility; print(str(CryptUtility.cryptPasswordWithPbkdf2('$NEW_PASSWORD')))"` +if [ $? -ne 0 ]; then + echo "ERROR: Failed to hash password." + exit 1 +fi + +# Update the password in the database +echo "UPDATE user_info SET password = '$CRYPTED_PASSWORD' WHERE username = '$USERNAME';" | eval $mysqlCmd 2>/dev/null +if [ $? -ne 0 ]; then + echo "ERROR: Failed to update password in the database." + exit 1 +fi + +echo "Password for user '$USERNAME' has been updated successfully." diff --git a/sbin/cdb_test.sh b/sbin/cdb_test.sh index 903a84d84..28f901ea1 100755 --- a/sbin/cdb_test.sh +++ b/sbin/cdb_test.sh @@ -27,7 +27,7 @@ $MY_DIR/cdb_backup_db.sh timestamp=`date +%Y%m%d` LOGR_BACKUP_DIR=$LOGR_INSTALL_DIR/backup/logr/$timestamp -yes $db_password | $MY_DIR/cdb_create_db.sh cdb $LOGR_DIST_DIR/db/sql/test || exit 1 +yes $db_password | $MY_DIR/cdb_create_db.sh logr $LOGR_DIST_DIR/db/sql/test || exit 1 # Regenerate the API with current code base API_CLIENT_PATH=$LOGR_DIST_DIR/tools/developer_tools/python-client/ @@ -43,7 +43,7 @@ PRINTF_HEADER="\n\n$HEADER_TEXT Starting %s Test $HEADER_TEXT\n\n\n" printf "$PRINTF_HEADER" "API" cd $API_CLIENT_PATH/test -pytest api_test.py +pytest . # printf "$PRINTF_HEADER" "Selenium" # cd $LOGR_DIST_DIR/tools/developer_tools/portal_testing/PythonSeleniumTest diff --git a/src/java/LogrPortal/lib/swagger-annotations-2.1.5.jar b/src/java/LogrPortal/lib/swagger-annotations-2.1.5.jar deleted file mode 100644 index 28b7fb01e..000000000 Binary files a/src/java/LogrPortal/lib/swagger-annotations-2.1.5.jar and /dev/null differ diff --git a/src/java/LogrPortal/lib/swagger-annotations-2.2.44.jar b/src/java/LogrPortal/lib/swagger-annotations-2.2.44.jar new file mode 100644 index 000000000..d09413c2e Binary files /dev/null and b/src/java/LogrPortal/lib/swagger-annotations-2.2.44.jar differ diff --git a/src/java/LogrPortal/lib/swagger-core-2.1.5.jar b/src/java/LogrPortal/lib/swagger-core-2.1.5.jar deleted file mode 100644 index 00f5229ef..000000000 Binary files a/src/java/LogrPortal/lib/swagger-core-2.1.5.jar and /dev/null differ diff --git a/src/java/LogrPortal/lib/swagger-core-2.2.44.jar b/src/java/LogrPortal/lib/swagger-core-2.2.44.jar new file mode 100644 index 000000000..c1913aac1 Binary files /dev/null and b/src/java/LogrPortal/lib/swagger-core-2.2.44.jar differ diff --git a/src/java/LogrPortal/lib/swagger-integration-2.1.5.jar b/src/java/LogrPortal/lib/swagger-integration-2.1.5.jar deleted file mode 100644 index 3f5570ea9..000000000 Binary files a/src/java/LogrPortal/lib/swagger-integration-2.1.5.jar and /dev/null differ diff --git a/src/java/LogrPortal/lib/swagger-integration-2.2.44.jar b/src/java/LogrPortal/lib/swagger-integration-2.2.44.jar new file mode 100644 index 000000000..96ad8d981 Binary files /dev/null and b/src/java/LogrPortal/lib/swagger-integration-2.2.44.jar differ diff --git a/src/java/LogrPortal/lib/swagger-jaxrs2-2.1.5.jar b/src/java/LogrPortal/lib/swagger-jaxrs2-2.1.5.jar deleted file mode 100644 index 4c8206e53..000000000 Binary files a/src/java/LogrPortal/lib/swagger-jaxrs2-2.1.5.jar and /dev/null differ diff --git a/src/java/LogrPortal/lib/swagger-jaxrs2-2.2.44.jar b/src/java/LogrPortal/lib/swagger-jaxrs2-2.2.44.jar new file mode 100644 index 000000000..10fa10e6f Binary files /dev/null and b/src/java/LogrPortal/lib/swagger-jaxrs2-2.2.44.jar differ diff --git a/src/java/LogrPortal/lib/swagger-models-2.1.5.jar b/src/java/LogrPortal/lib/swagger-models-2.1.5.jar deleted file mode 100644 index 53fffb1f8..000000000 Binary files a/src/java/LogrPortal/lib/swagger-models-2.1.5.jar and /dev/null differ diff --git a/src/java/LogrPortal/lib/swagger-models-2.2.44.jar b/src/java/LogrPortal/lib/swagger-models-2.2.44.jar new file mode 100644 index 000000000..864b1cbda Binary files /dev/null and b/src/java/LogrPortal/lib/swagger-models-2.2.44.jar differ diff --git a/src/java/LogrPortal/nbproject/build-impl.xml b/src/java/LogrPortal/nbproject/build-impl.xml index 554ad7f4c..17ada41fc 100644 --- a/src/java/LogrPortal/nbproject/build-impl.xml +++ b/src/java/LogrPortal/nbproject/build-impl.xml @@ -1013,11 +1013,11 @@ exists or setup the property manually. For example like this: - - - - - + + + + + @@ -1079,11 +1079,11 @@ exists or setup the property manually. For example like this: - - - - - + + + + + diff --git a/src/java/LogrPortal/nbproject/genfiles.properties b/src/java/LogrPortal/nbproject/genfiles.properties index 630d6a289..c4e6cfe2f 100644 --- a/src/java/LogrPortal/nbproject/genfiles.properties +++ b/src/java/LogrPortal/nbproject/genfiles.properties @@ -3,8 +3,8 @@ 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=26f30e3e -nbproject/build-impl.xml.script.CRC32=e243752e +nbproject/build-impl.xml.data.CRC32=c3c1315b +nbproject/build-impl.xml.script.CRC32=f37f2b81 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/LogrPortal/nbproject/project.properties b/src/java/LogrPortal/nbproject/project.properties index 82109d52c..1662297d3 100644 --- a/src/java/LogrPortal/nbproject/project.properties +++ b/src/java/LogrPortal/nbproject/project.properties @@ -89,11 +89,11 @@ file.reference.poi-ooxml-4.0.1.jar=lib/poi-ooxml-4.0.1.jar file.reference.poi-ooxml-schemas-4.0.1.jar=lib/poi-ooxml-schemas-4.0.1.jar file.reference.primefaces-11.0.0.jar=lib/primefaces-11.0.0.jar file.reference.primefaces-extensions-11.0.0.jar=lib/primefaces-extensions-11.0.0.jar -file.reference.swagger-annotations-2.1.5.jar=lib/swagger-annotations-2.1.5.jar -file.reference.swagger-core-2.1.5.jar=lib/swagger-core-2.1.5.jar -file.reference.swagger-integration-2.1.5.jar=lib/swagger-integration-2.1.5.jar -file.reference.swagger-jaxrs2-2.1.5.jar=lib/swagger-jaxrs2-2.1.5.jar -file.reference.swagger-models-2.1.5.jar=lib/swagger-models-2.1.5.jar +file.reference.swagger-annotations-2.2.44.jar=lib/swagger-annotations-2.2.44.jar +file.reference.swagger-core-2.2.44.jar=lib/swagger-core-2.2.44.jar +file.reference.swagger-integration-2.2.44.jar=lib/swagger-integration-2.2.44.jar +file.reference.swagger-jaxrs2-2.2.44.jar=lib/swagger-jaxrs2-2.2.44.jar +file.reference.swagger-models-2.2.44.jar=lib/swagger-models-2.2.44.jar file.reference.xmlbeans-3.0.2.jar=lib/xmlbeans-3.0.2.jar file.reference.commons-collections4-4.2.jar=lib/commons-collections4-4.2.jar file.reference.commons-compress-1.18.jar=lib/commons-compress-1.18.jar @@ -145,11 +145,11 @@ javac.classpath=\ ${file.reference.jackson-dataformat-yaml-2.11.2.jar}:\ ${file.reference.classgraph-4.8.31.jar}:\ ${file.reference.snakeyaml-1.24.jar}:\ - ${file.reference.swagger-annotations-2.1.5.jar}:\ - ${file.reference.swagger-core-2.1.5.jar}:\ - ${file.reference.swagger-jaxrs2-2.1.5.jar}:\ - ${file.reference.swagger-models-2.1.5.jar}:\ - ${file.reference.swagger-integration-2.1.5.jar}:\ + ${file.reference.swagger-annotations-2.2.44.jar}:\ + ${file.reference.swagger-jaxrs2-2.2.44.jar}:\ + ${file.reference.swagger-core-2.2.44.jar}:\ + ${file.reference.swagger-models-2.2.44.jar}:\ + ${file.reference.swagger-integration-2.2.44.jar}:\ ${file.reference.ejb-api-3.0.jar}:\ ${file.reference.javaee-web-api-8.0.1.jar}:\ ${file.reference.javax.faces-api-2.1.jar}:\ diff --git a/src/java/LogrPortal/nbproject/project.xml b/src/java/LogrPortal/nbproject/project.xml index cfb6bdc29..f9b51b0e7 100644 --- a/src/java/LogrPortal/nbproject/project.xml +++ b/src/java/LogrPortal/nbproject/project.xml @@ -131,23 +131,23 @@ WEB-INF/lib - ${file.reference.swagger-annotations-2.1.5.jar} + ${file.reference.swagger-annotations-2.2.44.jar} WEB-INF/lib - ${file.reference.swagger-core-2.1.5.jar} + ${file.reference.swagger-jaxrs2-2.2.44.jar} WEB-INF/lib - ${file.reference.swagger-jaxrs2-2.1.5.jar} + ${file.reference.swagger-core-2.2.44.jar} WEB-INF/lib - ${file.reference.swagger-models-2.1.5.jar} + ${file.reference.swagger-models-2.2.44.jar} WEB-INF/lib - ${file.reference.swagger-integration-2.1.5.jar} + ${file.reference.swagger-integration-2.2.44.jar} WEB-INF/lib diff --git a/src/java/LogrPortal/src/java/gov/anl/aps/logr/portal/controllers/NotificationConfigurationController.java b/src/java/LogrPortal/src/java/gov/anl/aps/logr/portal/controllers/NotificationConfigurationController.java index ee4527e44..2d9b870bb 100644 --- a/src/java/LogrPortal/src/java/gov/anl/aps/logr/portal/controllers/NotificationConfigurationController.java +++ b/src/java/LogrPortal/src/java/gov/anl/aps/logr/portal/controllers/NotificationConfigurationController.java @@ -142,6 +142,45 @@ public void prepareCreateDialog(UserInfo userInfo) { for (NotificationHandlerConfigKey key : handlerKeys) { handlerPreferences.put(key.getId(), "true".equalsIgnoreCase(key.getDefaultValue())); } + + selectDefaultProvider(); + } + + private void selectDefaultProvider() { + List providers = notificationProviderFacade.findAll(); + if (!providers.isEmpty()) { + getCurrent().setNotificationProvider(providers.get(0)); + loadProviderConfigKeys(); + } + } + + /** + * Check if a user already has an email-based notification configuration. + * + * @param user + * @return true if the user has a config with a mailto:// endpoint + */ + public boolean hasEmailConfiguration(UserInfo user) { + List configs = notificationConfigurationFacade.findByUser(user); + for (NotificationConfiguration config : configs) { + String endpoint = config.getNotificationEndpoint(); + if (endpoint != null && endpoint.startsWith("mailto://")) { + return true; + } + } + return false; + } + + /** + * Prepare to create a new email notification configuration with user's + * email pre-filled. Sets up the dialog with provider, name, description, + * and endpoint. + * + * @param userInfo + */ + public void prepareCreateEmailDialog(UserInfo userInfo) { + prepareCreateDialog(userInfo); + populateEmailInfo(); } /** diff --git a/src/java/LogrPortal/src/java/gov/anl/aps/logr/portal/controllers/utilities/ItemDomainLogbookControllerUtility.java b/src/java/LogrPortal/src/java/gov/anl/aps/logr/portal/controllers/utilities/ItemDomainLogbookControllerUtility.java index 75fe1d521..6c169af7c 100644 --- a/src/java/LogrPortal/src/java/gov/anl/aps/logr/portal/controllers/utilities/ItemDomainLogbookControllerUtility.java +++ b/src/java/LogrPortal/src/java/gov/anl/aps/logr/portal/controllers/utilities/ItemDomainLogbookControllerUtility.java @@ -28,6 +28,7 @@ import java.util.Calendar; import java.util.Date; import java.util.HashMap; +import java.util.LinkedHashMap; import java.util.LinkedList; import java.util.List; import java.util.Map; @@ -389,7 +390,7 @@ public void addCommonLogEntryDocumentMatches(SearchResult result, List searchLogEntries(String searchString, boolean ca searchString, itemTypeIdList, entityTypeIdList, userIdList, startModifiedTime, endModifiedTime, startCreatedTime, endCreatedTime); - String patternString = generatePatternString(searchString); - Pattern searchPattern = getSearchPattern(patternString, caseInsensitive); + String[] searchWords = searchString.trim().split("\\s+"); + boolean multiWord = searchWords.length > 1; + + Map wordPatterns = new LinkedHashMap<>(); + for (String word : searchWords) { + String wordPattern; + if (word.contains("?") || word.contains("*")) { + wordPattern = word.replace("*", ".*").replace("?", "."); + } else { + wordPattern = Pattern.quote(word); + } + Pattern pattern = getSearchPattern(wordPattern, caseInsensitive); + wordPatterns.put(word, pattern); + } for (Object[] result : results) { ItemDomainLogbook logbook = (ItemDomainLogbook) result[0]; @@ -463,14 +476,28 @@ public LinkedList searchLogEntries(String searchString, boolean ca String text = log.getText(); String[] logLines = text.split("\n"); - String matchingLines = ""; + Map keyMatchingLines = new LinkedHashMap<>(); for (String lineText : logLines) { - if (searchPattern.matcher(lineText).find()) { - matchingLines += lineText + "\n"; + List matchedWords = new ArrayList<>(); + for (Map.Entry entry : wordPatterns.entrySet()) { + if (entry.getValue().matcher(lineText).find()) { + matchedWords.add(entry.getKey()); + } } + if (!matchedWords.isEmpty()) { + String key; + if (!multiWord) { + key = "log entry"; + } else { + key = "log entry (" + String.join(" ", matchedWords) + ")"; + } + keyMatchingLines.merge(key, lineText + "\n", String::concat); + } + } + for (Map.Entry entry : keyMatchingLines.entrySet()) { + searchResult.addAttributeMatch(entry.getKey(), entry.getValue()); } - searchResult.addAttributeMatch("log entry", matchingLines); addCommonLogEntryDocumentMatches(searchResult, searchEntityTypeList, searchItemTypeList); diff --git a/src/java/LogrPortal/src/java/gov/anl/aps/logr/portal/model/jsf/beans/LogAttachmentUploadBean.java b/src/java/LogrPortal/src/java/gov/anl/aps/logr/portal/model/jsf/beans/LogAttachmentUploadBean.java index ac84810f8..be060e6f4 100644 --- a/src/java/LogrPortal/src/java/gov/anl/aps/logr/portal/model/jsf/beans/LogAttachmentUploadBean.java +++ b/src/java/LogrPortal/src/java/gov/anl/aps/logr/portal/model/jsf/beans/LogAttachmentUploadBean.java @@ -6,20 +6,10 @@ import gov.anl.aps.logr.portal.model.db.entities.Attachment; import gov.anl.aps.logr.portal.model.db.entities.Log; -import gov.anl.aps.logr.common.utilities.FileUtility; -import gov.anl.aps.logr.portal.utilities.GalleryUtility; +import gov.anl.aps.logr.portal.utilities.LogAttachmentUtility; import gov.anl.aps.logr.portal.utilities.SessionUtility; -import gov.anl.aps.logr.portal.utilities.StorageUtility; -import java.io.File; import java.io.IOException; -import java.io.InputStream; import java.io.Serializable; -import java.nio.file.Files; -import java.nio.file.Path; -import java.nio.file.Paths; -import java.nio.file.StandardCopyOption; -import java.util.ArrayList; -import java.util.List; import javax.enterprise.context.SessionScoped; import javax.inject.Named; import org.apache.logging.log4j.LogManager; @@ -57,50 +47,20 @@ public void upload(UploadedFile uploadedFile) { } public String upload(UploadedFile uploadedFile, boolean attachFileReference) { - Path uploadDirPath; try { if (uploadedFile != null && !uploadedFile.getFileName().isEmpty()) { - String uploadedExtension = FileUtility.getFileExtension(uploadedFile.getFileName()); - - uploadDirPath = Paths.get(StorageUtility.getFileSystemLogAttachmentsDirectory()); - logger.debug("Using log attachments directory: " + uploadDirPath.toString()); - if (Files.notExists(uploadDirPath)) { - Files.createDirectory(uploadDirPath); - } - File uploadDir = uploadDirPath.toFile(); - - String originalExtension = "." + uploadedExtension; - File originalFile = File.createTempFile("attachment.", originalExtension, uploadDir); - InputStream input = uploadedFile.getInputStream(); - Files.copy(input, originalFile.toPath(), StandardCopyOption.REPLACE_EXISTING); - logger.debug("Saved file: " + originalFile.toPath()); - Attachment attachment = new Attachment(); - attachment.setName(originalFile.getName()); - attachment.setOriginalFilename(uploadedFile.getFileName()); - List attachmentList = logEntry.getAttachmentList(); - if (attachmentList == null) { - attachmentList = new ArrayList<>(); - logEntry.setAttachmentList(attachmentList); - } - attachmentList.add(attachment); String fileName = uploadedFile.getFileName(); - String fileReference = "[" + fileName + "](" + attachment.getLogAttachmentPath() + ") "; - if (GalleryUtility.viewableFileName(fileName)) { - fileReference = '!' + fileReference; - // Generate scaled images - GalleryUtility.storeImagePreviews(originalFile, false); - } + Attachment attachment = LogAttachmentUtility.uploadAttachment( + uploadedFile.getInputStream(), fileName, logEntry); + String fileReference = LogAttachmentUtility.buildMarkdownReference(fileName, attachment); if (attachFileReference) { - // TODO make configurable based on domain? String text = logEntry.getText(); - String prefix = "\n\n"; - - text += prefix + fileReference; + text += "\n\n" + fileReference; logEntry.setText(text); } - SessionUtility.addInfoMessage("Success", "Uploaded file " + uploadedFile.getFileName() + "."); + SessionUtility.addInfoMessage("Success", "Uploaded file " + fileName + "."); return fileReference; } } catch (IOException ex) { diff --git a/src/java/LogrPortal/src/java/gov/anl/aps/logr/portal/utilities/LogAttachmentUtility.java b/src/java/LogrPortal/src/java/gov/anl/aps/logr/portal/utilities/LogAttachmentUtility.java new file mode 100644 index 000000000..c7467fbf1 --- /dev/null +++ b/src/java/LogrPortal/src/java/gov/anl/aps/logr/portal/utilities/LogAttachmentUtility.java @@ -0,0 +1,92 @@ +/* + * Copyright (c) UChicago Argonne, LLC. All rights reserved. + * See LICENSE file. + */ +package gov.anl.aps.logr.portal.utilities; + +import gov.anl.aps.logr.common.utilities.FileUtility; +import gov.anl.aps.logr.portal.model.db.entities.Attachment; +import gov.anl.aps.logr.portal.model.db.entities.Log; +import java.io.File; +import java.io.IOException; +import java.io.InputStream; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.Paths; +import java.nio.file.StandardCopyOption; +import java.util.ArrayList; +import java.util.List; +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; + +/** + * Shared utility for uploading log attachments. Used by both the JSF upload + * bean and the REST API. + * + * @author djarosz + */ +public class LogAttachmentUtility { + + private static final Logger logger = LogManager.getLogger(LogAttachmentUtility.class.getName()); + + /** + * Save an uploaded file as a log attachment. Handles: saving to disk, + * creating Attachment entity, linking to log, viewability check, and image + * preview generation. + * + * @param inputStream the file input stream + * @param fileName the original file name + * @param logEntry the log entry to attach to + * @return the created Attachment entity + * @throws IOException if file operations fail + */ + public static Attachment uploadAttachment(InputStream inputStream, String fileName, Log logEntry) throws IOException { + String uploadedExtension = FileUtility.getFileExtension(fileName); + + Path uploadDirPath = Paths.get(StorageUtility.getFileSystemLogAttachmentsDirectory()); + logger.debug("Using log attachments directory: " + uploadDirPath.toString()); + if (Files.notExists(uploadDirPath)) { + Files.createDirectory(uploadDirPath); + } + File uploadDir = uploadDirPath.toFile(); + + String originalExtension = "." + uploadedExtension; + File originalFile = File.createTempFile("attachment.", originalExtension, uploadDir); + Files.copy(inputStream, originalFile.toPath(), StandardCopyOption.REPLACE_EXISTING); + logger.debug("Saved file: " + originalFile.toPath()); + + Attachment attachment = new Attachment(); + attachment.setName(originalFile.getName()); + attachment.setOriginalFilename(fileName); + + List attachmentList = logEntry.getAttachmentList(); + if (attachmentList == null) { + attachmentList = new ArrayList<>(); + logEntry.setAttachmentList(attachmentList); + } + attachmentList.add(attachment); + + if (GalleryUtility.viewableFileName(fileName)) { + GalleryUtility.storeImagePreviews(originalFile, false); + } + + return attachment; + } + + /** + * Build a markdown reference string for an attachment. Returns + * "![filename](path)" for viewable files, "[filename](path)" for others. + * + * @param originalFilename the original file name + * @param attachment the attachment entity + * @return the markdown reference string + */ + public static String buildMarkdownReference(String originalFilename, Attachment attachment) { + String ref = "[" + originalFilename + "](" + attachment.getLogAttachmentPath() + ") "; + if (GalleryUtility.viewableFileName(originalFilename)) { + ref = "!" + ref; + } + return ref; + } + +} diff --git a/src/java/LogrPortal/src/java/gov/anl/aps/logr/rest/entities/AttachmentChecksum.java b/src/java/LogrPortal/src/java/gov/anl/aps/logr/rest/entities/AttachmentChecksum.java new file mode 100644 index 000000000..a10e3d826 --- /dev/null +++ b/src/java/LogrPortal/src/java/gov/anl/aps/logr/rest/entities/AttachmentChecksum.java @@ -0,0 +1,51 @@ +/* + * Copyright (c) UChicago Argonne, LLC. All rights reserved. + * See LICENSE file. + */ +package gov.anl.aps.logr.rest.entities; + +/** + * DTO representing an attachment checksum response for the REST API. + * + * @author djarosz + */ +public class AttachmentChecksum { + + private String name; + private String originalFilename; + private String md5; + + public AttachmentChecksum() { + } + + public AttachmentChecksum(String name, String originalFilename, String md5) { + this.name = name; + this.originalFilename = originalFilename; + this.md5 = md5; + } + + public String getName() { + return name; + } + + public void setName(String name) { + this.name = name; + } + + public String getOriginalFilename() { + return originalFilename; + } + + public void setOriginalFilename(String originalFilename) { + this.originalFilename = originalFilename; + } + + public String getMd5() { + return md5; + } + + public void setMd5(String md5) { + this.md5 = md5; + } + +} diff --git a/src/java/LogrPortal/src/java/gov/anl/aps/logr/rest/entities/FileUploadObject.java b/src/java/LogrPortal/src/java/gov/anl/aps/logr/rest/entities/FileUploadObject.java deleted file mode 100644 index e1d763afb..000000000 --- a/src/java/LogrPortal/src/java/gov/anl/aps/logr/rest/entities/FileUploadObject.java +++ /dev/null @@ -1,32 +0,0 @@ -/* - * Copyright (c) UChicago Argonne, LLC. All rights reserved. - * See LICENSE file. - */ -package gov.anl.aps.logr.rest.entities; - -/** - * - * @author djarosz - */ -public class FileUploadObject { - - private String fileName; - private String base64Binary; - - public String getFileName() { - return fileName; - } - - public void setFileName(String fileName) { - this.fileName = fileName; - } - - public String getBase64Binary() { - return base64Binary; - } - - public void setBase64Binary(String base64Binary) { - this.base64Binary = base64Binary; - } - -} diff --git a/src/java/LogrPortal/src/java/gov/anl/aps/logr/rest/entities/LogEntryAttachment.java b/src/java/LogrPortal/src/java/gov/anl/aps/logr/rest/entities/LogEntryAttachment.java new file mode 100644 index 000000000..31290d8e2 --- /dev/null +++ b/src/java/LogrPortal/src/java/gov/anl/aps/logr/rest/entities/LogEntryAttachment.java @@ -0,0 +1,61 @@ +/* + * Copyright (c) UChicago Argonne, LLC. All rights reserved. + * See LICENSE file. + */ +package gov.anl.aps.logr.rest.entities; + +/** + * DTO representing a log entry attachment for the REST API. + * + * @author djarosz + */ +public class LogEntryAttachment { + + private String markdownReference; + private String downloadPath; + private String originalFilename; + private String storedFilename; + + public LogEntryAttachment() { + } + + public LogEntryAttachment(String markdownReference, String downloadPath, String originalFilename, String storedFilename) { + this.markdownReference = markdownReference; + this.downloadPath = downloadPath; + this.originalFilename = originalFilename; + this.storedFilename = storedFilename; + } + + public String getMarkdownReference() { + return markdownReference; + } + + public void setMarkdownReference(String markdownReference) { + this.markdownReference = markdownReference; + } + + public String getDownloadPath() { + return downloadPath; + } + + public void setDownloadPath(String downloadPath) { + this.downloadPath = downloadPath; + } + + public String getOriginalFilename() { + return originalFilename; + } + + public void setOriginalFilename(String originalFilename) { + this.originalFilename = originalFilename; + } + + public String getStoredFilename() { + return storedFilename; + } + + public void setStoredFilename(String storedFilename) { + this.storedFilename = storedFilename; + } + +} diff --git a/src/java/LogrPortal/src/java/gov/anl/aps/logr/rest/routes/AuthenticationRoute.java b/src/java/LogrPortal/src/java/gov/anl/aps/logr/rest/routes/AuthenticationRoute.java index 06c0febe0..343324b58 100644 --- a/src/java/LogrPortal/src/java/gov/anl/aps/logr/rest/routes/AuthenticationRoute.java +++ b/src/java/LogrPortal/src/java/gov/anl/aps/logr/rest/routes/AuthenticationRoute.java @@ -83,6 +83,7 @@ public Response authenticateUser(@FormParam("username") String username, @POST @Path("/logout") + @Operation(responses = {@ApiResponse(responseCode = "200", description = "OK", useReturnTypeSchema = true)}) @Secured public void logOut() throws AuthenticationError, Exception { Principal userPrincipal = securityContext.getUserPrincipal(); @@ -100,6 +101,7 @@ public void logOut() throws AuthenticationError, Exception { @GET @Path("/Verify") + @Operation(responses = {@ApiResponse(responseCode = "200", description = "OK", useReturnTypeSchema = true)}) @Produces(MediaType.APPLICATION_JSON) @SecurityRequirement(name = "belyAuth") @Secured diff --git a/src/java/LogrPortal/src/java/gov/anl/aps/logr/rest/routes/DomainRoute.java b/src/java/LogrPortal/src/java/gov/anl/aps/logr/rest/routes/DomainRoute.java index d12897d10..cc172a844 100644 --- a/src/java/LogrPortal/src/java/gov/anl/aps/logr/rest/routes/DomainRoute.java +++ b/src/java/LogrPortal/src/java/gov/anl/aps/logr/rest/routes/DomainRoute.java @@ -11,6 +11,8 @@ import gov.anl.aps.logr.portal.model.db.entities.EntityType; import gov.anl.aps.logr.portal.model.db.entities.ItemCategory; import gov.anl.aps.logr.portal.model.db.entities.ItemType; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.responses.ApiResponse; import io.swagger.v3.oas.annotations.tags.Tag; import java.util.List; import javax.ejb.EJB; @@ -43,6 +45,7 @@ public class DomainRoute extends BaseRoute { @GET @Path("/all") + @Operation(responses = {@ApiResponse(responseCode = "200", description = "OK", useReturnTypeSchema = true)}) @Produces(MediaType.APPLICATION_JSON) public List getDomainList() { LOGGER.debug("Fetching domain list"); @@ -51,6 +54,7 @@ public List getDomainList() { @GET @Path("/ById/{id}") + @Operation(responses = {@ApiResponse(responseCode = "200", description = "OK", useReturnTypeSchema = true)}) @Produces(MediaType.APPLICATION_JSON) public Domain getDomainById(@PathParam("id") int id) { LOGGER.debug("Fetching domain with id: " + id); @@ -59,6 +63,7 @@ public Domain getDomainById(@PathParam("id") int id) { @GET @Path("/ByName/{name}") + @Operation(responses = {@ApiResponse(responseCode = "200", description = "OK", useReturnTypeSchema = true)}) @Produces(MediaType.APPLICATION_JSON) public Domain getDomainByName(@PathParam("name") String name) { LOGGER.debug("Fetching domain with name: " + name); @@ -67,6 +72,7 @@ public Domain getDomainByName(@PathParam("name") String name) { @GET @Path("/ById/{id}/Categories") + @Operation(responses = {@ApiResponse(responseCode = "200", description = "OK", useReturnTypeSchema = true)}) @Produces(MediaType.APPLICATION_JSON) public List getDomainCategoryList(@PathParam("id") int id) { Domain domainById = getDomainById(id); @@ -75,6 +81,7 @@ public List getDomainCategoryList(@PathParam("id") int id) { @GET @Path("/ById/{id}/Types") + @Operation(responses = {@ApiResponse(responseCode = "200", description = "OK", useReturnTypeSchema = true)}) @Produces(MediaType.APPLICATION_JSON) public List getDomainTypeList(@PathParam("id") int id) { Domain domainById = getDomainById(id); @@ -83,6 +90,7 @@ public List getDomainTypeList(@PathParam("id") int id) { @GET @Path("/ById/{id}/EntityTypes") + @Operation(responses = {@ApiResponse(responseCode = "200", description = "OK", useReturnTypeSchema = true)}) @Produces(MediaType.APPLICATION_JSON) public List getAllowedEntityTypeList(@PathParam("id") int id) { Domain domainById = getDomainById(id); @@ -91,6 +99,7 @@ public List getAllowedEntityTypeList(@PathParam("id") int id) { @GET @Path("/Category/ById/{id}") + @Operation(responses = {@ApiResponse(responseCode = "200", description = "OK", useReturnTypeSchema = true)}) @Produces(MediaType.APPLICATION_JSON) public ItemCategory getItemCategoryById(@PathParam("id") int id) { return itemCategoryFacade.find(id); @@ -98,6 +107,7 @@ public ItemCategory getItemCategoryById(@PathParam("id") int id) { @GET @Path("/Category/ById/{id}/Types") + @Operation(responses = {@ApiResponse(responseCode = "200", description = "OK", useReturnTypeSchema = true)}) @Produces(MediaType.APPLICATION_JSON) public List getItemCategoryAllowedTypes(@PathParam("id") int id) { ItemCategory itemCategoryById = getItemCategoryById(id); @@ -106,6 +116,7 @@ public List getItemCategoryAllowedTypes(@PathParam("id") int id) { @GET @Path("/Type/ById/{id}") + @Operation(responses = {@ApiResponse(responseCode = "200", description = "OK", useReturnTypeSchema = true)}) @Produces(MediaType.APPLICATION_JSON) public ItemType getItemTypeById(@PathParam("id") int id) { return itemTypeFacade.find(id); @@ -113,6 +124,7 @@ public ItemType getItemTypeById(@PathParam("id") int id) { @GET @Path("/Type/ById/{id}/Categories") + @Operation(responses = {@ApiResponse(responseCode = "200", description = "OK", useReturnTypeSchema = true)}) @Produces(MediaType.APPLICATION_JSON) public List getItemTypeAllowedTypes(@PathParam("id") int id) { ItemType itemTypeById = getItemTypeById(id); diff --git a/src/java/LogrPortal/src/java/gov/anl/aps/logr/rest/routes/DownloadRoute.java b/src/java/LogrPortal/src/java/gov/anl/aps/logr/rest/routes/DownloadRoute.java index 372bf6f6b..12d28f889 100644 --- a/src/java/LogrPortal/src/java/gov/anl/aps/logr/rest/routes/DownloadRoute.java +++ b/src/java/LogrPortal/src/java/gov/anl/aps/logr/rest/routes/DownloadRoute.java @@ -18,16 +18,24 @@ import gov.anl.aps.logr.portal.utilities.StorageUtility; import gov.anl.aps.logr.rest.constants.DownloadRouteMimeType; import io.swagger.v3.oas.annotations.tags.Tag; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.responses.ApiResponse; import java.io.File; +import java.io.FileInputStream; import java.io.FileNotFoundException; +import java.io.IOException; +import java.security.MessageDigest; +import java.security.NoSuchAlgorithmException; import javax.ejb.EJB; import javax.ejb.EJBException; import javax.ws.rs.GET; import javax.ws.rs.Path; import javax.ws.rs.PathParam; import javax.ws.rs.Produces; +import javax.ws.rs.core.MediaType; import javax.ws.rs.core.Response; import javax.ws.rs.core.Response.ResponseBuilder; +import gov.anl.aps.logr.rest.entities.AttachmentChecksum; import org.apache.logging.log4j.LogManager; import org.apache.logging.log4j.Logger; @@ -49,6 +57,7 @@ public class DownloadRoute extends BaseRoute { @GET @Path("/PropertyValue/Image/{imageName}/{scaling}") + @Operation(responses = {@ApiResponse(responseCode = "200", description = "OK", useReturnTypeSchema = true)}) public Response getImage(@PathParam("imageName") String imageName, @PathParam("scaling") String scaling) throws FileNotFoundException { LOGGER.debug("Fetching " + scaling + " image: " + imageName); String fullImageName = imageName + "." + scaling; @@ -59,6 +68,7 @@ public Response getImage(@PathParam("imageName") String imageName, @PathParam("s @GET @Path("/PropertyValue/{propertyValueId}") + @Operation(responses = {@ApiResponse(responseCode = "200", description = "OK", useReturnTypeSchema = true)}) @Produces("image/png") public Response getDownloadByPropertyValueId(@PathParam("propertyValueId") Integer propertyValueId) throws FileNotFoundException, ObjectNotFound, InvalidRequest { PropertyValue result = propertyValueFacade.find(propertyValueId); @@ -110,6 +120,7 @@ public Response getDownloadByPropertyValueId(@PathParam("propertyValueId") Integ @GET @Path("/Attachments/{attachmentName}") + @Operation(responses = {@ApiResponse(responseCode = "200", description = "OK", useReturnTypeSchema = true)}) public Response getAttachment(@PathParam("attachmentName") String attachmentName) throws FileNotFoundException { String originalAttachmentName = attachmentName; Attachment att = null; @@ -128,6 +139,7 @@ public Response getAttachment(@PathParam("attachmentName") String attachmentName @GET @Path("/Attachments/{attachmentName}/{scaling}") + @Operation(responses = {@ApiResponse(responseCode = "200", description = "OK", useReturnTypeSchema = true)}) @Produces("image/png") public Response getAttachment(@PathParam("attachmentName") String attachmentName, @PathParam("scaling") String scaling) throws FileNotFoundException { String fullAttachmentName = attachmentName + "." + scaling; @@ -136,6 +148,51 @@ public Response getAttachment(@PathParam("attachmentName") String attachmentName return getFileResponse("Image: " + fullAttachmentName, fullAttachmentName + ".png", filePath, false); } + @GET + @Path("/Attachments/{attachmentName}/md5") + @Operation(summary = "Calculate MD5 checksum for an attachment.", responses = {@ApiResponse(responseCode = "200", description = "OK", useReturnTypeSchema = true)}) + @Produces(MediaType.APPLICATION_JSON) + public Response getAttachmentChecksum(@PathParam("attachmentName") String attachmentName) throws FileNotFoundException, ObjectNotFound { + Attachment att = null; + try { + att = attachmentFacade.findByName(attachmentName); + } catch (EJBException ex) { } + + if (att == null) { + throw new ObjectNotFound("Could not find an attachment with name: " + attachmentName); + } + + String filePath = StorageUtility.getFileSystemLogAttachmentPath(attachmentName); + File file = new File(filePath); + + if (!file.exists()) { + throw new FileNotFoundException("Attachment file not found on disk: " + attachmentName); + } + + try { + MessageDigest md = MessageDigest.getInstance("MD5"); + try (FileInputStream fis = new FileInputStream(file)) { + byte[] buffer = new byte[8192]; + int bytesRead; + while ((bytesRead = fis.read(buffer)) != -1) { + md.update(buffer, 0, bytesRead); + } + } + byte[] digest = md.digest(); + StringBuilder sb = new StringBuilder(); + for (byte b : digest) { + sb.append(String.format("%02x", b)); + } + String checksum = sb.toString(); + + AttachmentChecksum result = new AttachmentChecksum(att.getName(), att.getOriginalFilename(), checksum); + return Response.ok(result).build(); + } catch (NoSuchAlgorithmException | IOException ex) { + LOGGER.error(ex); + return Response.serverError().entity("{\"error\": \"Failed to calculate checksum.\"}").type(MediaType.APPLICATION_JSON).build(); + } + } + private Response getFileResponse(String errorFileTypeColonName, String fileName, String storageFilePath, boolean isAttachment) throws FileNotFoundException { File file = new File(storageFilePath); diff --git a/src/java/LogrPortal/src/java/gov/anl/aps/logr/rest/routes/LogbookRoute.java b/src/java/LogrPortal/src/java/gov/anl/aps/logr/rest/routes/LogbookRoute.java index 2482242f5..b34ee0949 100644 --- a/src/java/LogrPortal/src/java/gov/anl/aps/logr/rest/routes/LogbookRoute.java +++ b/src/java/LogrPortal/src/java/gov/anl/aps/logr/rest/routes/LogbookRoute.java @@ -15,6 +15,7 @@ import gov.anl.aps.logr.portal.model.db.beans.DomainFacade; import gov.anl.aps.logr.portal.model.db.beans.ItemDomainLogbookFacade; import gov.anl.aps.logr.portal.model.db.beans.LogFacade; +import gov.anl.aps.logr.portal.model.db.entities.Attachment; import gov.anl.aps.logr.portal.model.db.entities.Domain; import gov.anl.aps.logr.portal.model.db.entities.EntityInfo; import gov.anl.aps.logr.portal.model.db.entities.EntityType; @@ -25,20 +26,28 @@ import gov.anl.aps.logr.portal.model.db.entities.Log; import gov.anl.aps.logr.portal.model.db.entities.UserInfo; import gov.anl.aps.logr.portal.model.db.utilities.EntityInfoUtility; +import gov.anl.aps.logr.portal.utilities.LogAttachmentUtility; import gov.anl.aps.logr.rest.authentication.Secured; import gov.anl.aps.logr.rest.entities.LogDocumentOptions; import gov.anl.aps.logr.rest.entities.LogDocumentSection; import gov.anl.aps.logr.rest.entities.LogEntry; +import gov.anl.aps.logr.rest.entities.LogEntryAttachment; import io.swagger.v3.oas.annotations.Operation; import io.swagger.v3.oas.annotations.Parameter; +import io.swagger.v3.oas.annotations.responses.ApiResponse; import io.swagger.v3.oas.annotations.parameters.RequestBody; import io.swagger.v3.oas.annotations.security.SecurityRequirement; +import io.swagger.v3.oas.annotations.media.Content; +import io.swagger.v3.oas.annotations.media.Schema; import io.swagger.v3.oas.annotations.tags.Tag; +import java.io.IOException; +import java.io.InputStream; import java.util.ArrayList; import java.util.List; import java.util.Objects; import javax.ejb.EJB; import javax.ws.rs.GET; +import javax.ws.rs.Consumes; import javax.ws.rs.PUT; import javax.ws.rs.Path; import javax.ws.rs.PathParam; @@ -74,6 +83,8 @@ private Domain getLogbookDomain() { @GET @Path("/LogbookTypes") + @Operation(responses = { + @ApiResponse(responseCode = "200", description = "OK", useReturnTypeSchema = true)}) @Produces(MediaType.APPLICATION_JSON) public List getLogbookTypes() { Domain domain = getLogbookDomain(); @@ -91,6 +102,8 @@ public List getLogbookTypes() { @GET @Path("/LogbookSystems") + @Operation(responses = { + @ApiResponse(responseCode = "200", description = "OK", useReturnTypeSchema = true)}) @Produces(MediaType.APPLICATION_JSON) public List getLogbookSystems() { Domain domain = getLogbookDomain(); @@ -100,6 +113,8 @@ public List getLogbookSystems() { @GET @Path("/LogbookTemplates") + @Operation(responses = { + @ApiResponse(responseCode = "200", description = "OK", useReturnTypeSchema = true)}) @Produces(MediaType.APPLICATION_JSON) public List getLogbookTemplates() { String domainName = ItemDomainName.logbook.getValue(); @@ -109,7 +124,8 @@ public List getLogbookTemplates() { @GET @Path("/LogDocuments/{logbookTypeId}/{limit}") - @Operation(summary = "Fetch last modified log documents for specific logbook type.") + @Operation(summary = "Fetch last modified log documents for specific logbook type.", responses = { + @ApiResponse(responseCode = "200", description = "OK", useReturnTypeSchema = true)}) @Produces(MediaType.APPLICATION_JSON) public List getLogDocuments(@PathParam("logbookTypeId") int logbookTypeId, @PathParam("limit") int rowLimit) throws InvalidArgument { List logbookTypes = getLogbookTypes(); @@ -131,9 +147,26 @@ public List getLogDocuments(@PathParam("logbookTypeId") int l return itemDomainLogbookFacade.findByDomainNameAndEntityTypeOrderByLastModifiedDate(domainName, entityTypeName, rowLimit); } + @GET + @Path("/LogDocumentByName/{name}") + @Operation(summary = "Fetch log documents by name.", responses = { + @ApiResponse(responseCode = "200", description = "OK", useReturnTypeSchema = true)}) + @Produces(MediaType.APPLICATION_JSON) + public ItemDomainLogbook getLogDocumentByName(@PathParam("name") String name) throws ObjectNotFound, InvalidArgument { + List items = itemDomainLogbookFacade.findByName(name); + if (items == null || items.isEmpty()) { + throw new ObjectNotFound("No log document found with name: " + name); + } + if (items.size() > 1) { + throw new InvalidArgument("Multiple log documents found with name: " + name); + } + return items.get(0); + } + @GET @Path("/LogEntries/{logDocumentId}") - @Operation(summary = "Fetch log entry for log document id or section id.") + @Operation(summary = "Fetch log entry for log document id or section id.", responses = { + @ApiResponse(responseCode = "200", description = "OK", useReturnTypeSchema = true)}) @Produces(MediaType.APPLICATION_JSON) public List getLogEntries(@PathParam("logDocumentId") int logDocumentId, @Parameter(description = "boolean to specify if log replies should be included") @QueryParam("loadReplies") boolean loadReplies, @@ -147,6 +180,8 @@ public List getLogEntries(@PathParam("logDocumentId") int logDocumentI @GET @Path("/LogbookSections/{logDocumentId}") + @Operation(responses = { + @ApiResponse(responseCode = "200", description = "OK", useReturnTypeSchema = true)}) @Produces(MediaType.APPLICATION_JSON) public List getLogbookSections(@PathParam("logDocumentId") int logDocumentId) throws ObjectNotFound, InvalidArgument { ItemDomainLogbook logDocument = getLogDocumentById(logDocumentId); @@ -169,7 +204,8 @@ public List getLogbookSections(@PathParam("logDocumentId") i @GET @Path("/LogEntryTemplate/{logDocumentId}") - @Operation(summary = "Fetch new log entry template for log document id or section id.") + @Operation(summary = "Fetch new log entry template for log document id or section id.", responses = { + @ApiResponse(responseCode = "200", description = "OK", useReturnTypeSchema = true)}) @Produces(MediaType.APPLICATION_JSON) @SecurityRequirement(name = "belyAuth") @Secured @@ -187,8 +223,10 @@ public LogEntry getLogEntryTemplate(@PathParam("logDocumentId") int logDocumentI @PUT @Path("/AddUpdateLogEntry") + @Consumes(MediaType.APPLICATION_JSON) @Produces(MediaType.APPLICATION_JSON) - @Operation(summary = "Add/Update a log entry to a log document or section. Will only update the core log entry not related reply/reaction.") + @Operation(summary = "Add/Update a log entry to a log document or section. Will only update the core log entry not related reply/reaction.", responses = { + @ApiResponse(responseCode = "200", description = "OK", useReturnTypeSchema = true)}) @SecurityRequirement(name = "belyAuth") @Secured public LogEntry addUpdateLogEntry(@RequestBody(required = true) LogEntry logEntry) throws CdbException { @@ -205,35 +243,7 @@ public LogEntry addUpdateLogEntry(@RequestBody(required = true) LogEntry logEntr if (logId == null) { logEntity = utility.prepareAddLog(logDocument, user); } else { - List logList = logDocument.getLogList(); - - for (Log log : logList) { - if (Objects.equals(log.getId(), logId)) { - logEntity = log; - break; - } - Log replyMatch = null; - for (Log reply : log.getChildLogList()) { - if (Objects.equals(reply.getId(), logId)) { - replyMatch = reply; - break; - } - } - if (replyMatch != null) { - logEntity = replyMatch; - break; - } - } - - if (logEntity == null) { - throw new ObjectNotFound( - String.format( - "Log id %d does not exist for log document %d.", - logId, - itemId - ) - ); - } + logEntity = findLogInDocument(logDocument, logId); utility.verifySaveLogLockoutsForItem(logDocument, logEntity, user); } @@ -245,7 +255,7 @@ public LogEntry addUpdateLogEntry(@RequestBody(required = true) LogEntry logEntr logEntry.updateLogPerLogEntryObject(logEntity); logEntity = utility.saveLog(logEntity, user, originalLogEntry); - // Update modified date. + // Update modified date. updateModifiedDateForLogDocument(logDocument, user); return new LogEntry(itemId, logEntity, false, false); @@ -253,8 +263,10 @@ public LogEntry addUpdateLogEntry(@RequestBody(required = true) LogEntry logEntr @PUT @Path("/CreateLogDocument") + @Consumes(MediaType.APPLICATION_JSON) @Produces(MediaType.APPLICATION_JSON) - @Operation(summary = "Create logbook document.") + @Operation(summary = "Create logbook document.", responses = { + @ApiResponse(responseCode = "200", description = "OK", useReturnTypeSchema = true)}) @SecurityRequirement(name = "belyAuth") @Secured public ItemDomainLogbook createLogbookDocument(@RequestBody(required = true) LogDocumentOptions newLogDocumentOptions) throws CdbException { @@ -290,7 +302,8 @@ public ItemDomainLogbook createLogbookDocument(@RequestBody(required = true) Log @PUT @Path("/CreateLogDocumentSection/{logDocumentId}/{sectionName}") @Produces(MediaType.APPLICATION_JSON) - @Operation(summary = "Create logbook document.") + @Operation(summary = "Create logbook document section.", responses = { + @ApiResponse(responseCode = "200", description = "OK", useReturnTypeSchema = true)}) @SecurityRequirement(name = "belyAuth") @Secured public LogDocumentSection createLogDocumentSection(@PathParam("logDocumentId") int logDocumentId, @PathParam("sectionName") String sectionName) throws CdbException { @@ -316,6 +329,118 @@ public LogDocumentSection createLogDocumentSection(@PathParam("logDocumentId") i throw new CdbException("Unexpected Exception. Could not find newly added section."); } + @PUT + @Path("/UploadAttachment/{logDocumentId}/{logId}") + @Consumes(MediaType.APPLICATION_OCTET_STREAM) + @Produces(MediaType.APPLICATION_JSON) + @Operation(summary = "Upload an attachment to a log entry.", responses = { + @ApiResponse(responseCode = "200", description = "OK", useReturnTypeSchema = true)}) + @SecurityRequirement(name = "belyAuth") + @Secured + public LogEntryAttachment uploadAttachment( + @PathParam("logDocumentId") int logDocumentId, + @PathParam("logId") int logId, + @QueryParam("appendReference") boolean appendReference, + @QueryParam("fileName") String fileName, + @RequestBody( + required = true, + description = "File content", + content = @Content( + mediaType = MediaType.APPLICATION_OCTET_STREAM, + schema = @Schema(type = "string", format = "binary") + ) + ) InputStream fileInputStream) throws CdbException { + + ItemDomainLogbook logDocument = getLogDocumentById(logDocumentId); + verifyCurrentUserPermissionForItem(logDocument); + + UserInfo user = getCurrentRequestUserInfo(); + Log logEntity = findLogInDocument(logDocument, logId); + + if (fileName == null || fileName.isEmpty()) { + throw new InvalidArgument("fileName query parameter is required."); + } + + if (fileInputStream == null) { + throw new InvalidArgument("Request body with file data is required."); + } + + try { + Attachment attachment = LogAttachmentUtility.uploadAttachment(fileInputStream, fileName, logEntity); + String markdownReference = LogAttachmentUtility.buildMarkdownReference(fileName, attachment); + + if (appendReference) { + String text = logEntity.getText(); + text += "\n\n" + markdownReference; + logEntity.setText(text); + } + + ItemDomainLogbookControllerUtility utility = new ItemDomainLogbookControllerUtility(); + Log originalLogEntry = logFacade.find(logId); + utility.saveLog(logEntity, user, originalLogEntry); + + updateModifiedDateForLogDocument(logDocument, user); + + String downloadPath = "/api/Downloads/Attachments/" + attachment.getName(); + return new LogEntryAttachment(markdownReference, downloadPath, fileName, attachment.getName()); + } catch (IOException ex) { + LOGGER.error(ex); + throw new CdbException("Failed to upload attachment: " + ex.getMessage()); + } + } + + @GET + @Path("/LogEntryAttachments/{logDocumentId}/{logId}") + @Produces(MediaType.APPLICATION_JSON) + @Operation(summary = "Fetch attachments for a log entry.", responses = { + @ApiResponse(responseCode = "200", description = "OK", useReturnTypeSchema = true)}) + public List getLogEntryAttachments( + @PathParam("logDocumentId") int logDocumentId, + @PathParam("logId") int logId) throws CdbException { + + ItemDomainLogbook logDocument = getLogDocumentById(logDocumentId); + Log logEntity = findLogInDocument(logDocument, logId); + + List result = new ArrayList<>(); + List attachmentList = logEntity.getAttachmentList(); + if (attachmentList != null) { + for (Attachment attachment : attachmentList) { + String originalFilename = attachment.getOriginalFilename(); + if (originalFilename == null) { + originalFilename = attachment.getName(); + } + String markdownReference = LogAttachmentUtility.buildMarkdownReference(originalFilename, attachment); + String downloadPath = "/api/Downloads/Attachments/" + attachment.getName(); + result.add(new LogEntryAttachment(markdownReference, downloadPath, originalFilename, attachment.getName())); + } + } + + return result; + } + + private Log findLogInDocument(ItemDomainLogbook logDocument, int logId) throws ObjectNotFound { + List logList = logDocument.getLogList(); + + for (Log log : logList) { + if (Objects.equals(log.getId(), logId)) { + return log; + } + for (Log reply : log.getChildLogList()) { + if (Objects.equals(reply.getId(), logId)) { + return reply; + } + } + } + + throw new ObjectNotFound( + String.format( + "Log id %d does not exist for log document %d.", + logId, + logDocument.getId() + ) + ); + } + private void validateAndGatherLogDocumentOptions(LogDocumentOptions logDocumentOptions) throws CdbException { String name = logDocumentOptions.getName(); if (name == null || name.isEmpty()) { diff --git a/src/java/LogrPortal/src/java/gov/anl/aps/logr/rest/routes/NotificationConfigurationRoute.java b/src/java/LogrPortal/src/java/gov/anl/aps/logr/rest/routes/NotificationConfigurationRoute.java index ce652c147..75896b8e6 100644 --- a/src/java/LogrPortal/src/java/gov/anl/aps/logr/rest/routes/NotificationConfigurationRoute.java +++ b/src/java/LogrPortal/src/java/gov/anl/aps/logr/rest/routes/NotificationConfigurationRoute.java @@ -14,6 +14,8 @@ import gov.anl.aps.logr.portal.model.db.entities.NotificationProviderConfigKey; import gov.anl.aps.logr.portal.model.db.entities.UserInfo; import io.swagger.v3.oas.annotations.tags.Tag; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.responses.ApiResponse; import java.util.List; import javax.ejb.EJB; import javax.ws.rs.GET; @@ -48,6 +50,7 @@ public class NotificationConfigurationRoute extends BaseRoute { @GET @Path("/all") + @Operation(responses = {@ApiResponse(responseCode = "200", description = "OK", useReturnTypeSchema = true)}) @Produces(MediaType.APPLICATION_JSON) public List getAll() { LOGGER.debug("Fetching all notification configurations"); @@ -56,6 +59,7 @@ public List getAll() { @GET @Path("/ById/{id}") + @Operation(responses = {@ApiResponse(responseCode = "200", description = "OK", useReturnTypeSchema = true)}) @Produces(MediaType.APPLICATION_JSON) public NotificationConfiguration getById(@PathParam("id") int id) { LOGGER.debug("Fetching notification configuration with id: " + id); @@ -64,6 +68,7 @@ public NotificationConfiguration getById(@PathParam("id") int id) { @GET @Path("/ByName/{name}") + @Operation(responses = {@ApiResponse(responseCode = "200", description = "OK", useReturnTypeSchema = true)}) @Produces(MediaType.APPLICATION_JSON) public List getByName(@PathParam("name") String name) { LOGGER.debug("Fetching notification configurations with name: " + name); @@ -72,6 +77,7 @@ public List getByName(@PathParam("name") String name) @GET @Path("/ByProviderId/{providerId}") + @Operation(responses = {@ApiResponse(responseCode = "200", description = "OK", useReturnTypeSchema = true)}) @Produces(MediaType.APPLICATION_JSON) public List getByProviderId(@PathParam("providerId") int providerId) { LOGGER.debug("Fetching notification configurations for provider id: " + providerId); @@ -81,6 +87,7 @@ public List getByProviderId(@PathParam("providerId") @GET @Path("/ByUsername/{username}") + @Operation(responses = {@ApiResponse(responseCode = "200", description = "OK", useReturnTypeSchema = true)}) @Produces(MediaType.APPLICATION_JSON) public List getByUsername(@PathParam("username") String username) { LOGGER.debug("Fetching notification configurations for user: " + username); @@ -90,6 +97,7 @@ public List getByUsername(@PathParam("username") Stri @GET @Path("/Providers") + @Operation(responses = {@ApiResponse(responseCode = "200", description = "OK", useReturnTypeSchema = true)}) @Produces(MediaType.APPLICATION_JSON) public List getAllProviders() { LOGGER.debug("Fetching all notification providers"); @@ -98,6 +106,7 @@ public List getAllProviders() { @GET @Path("/ProviderById/{id}") + @Operation(responses = {@ApiResponse(responseCode = "200", description = "OK", useReturnTypeSchema = true)}) @Produces(MediaType.APPLICATION_JSON) public NotificationProvider getProviderById(@PathParam("id") int id) { LOGGER.debug("Fetching notification provider with id: " + id); @@ -106,6 +115,7 @@ public NotificationProvider getProviderById(@PathParam("id") int id) { @GET @Path("/ProviderByName/{name}") + @Operation(responses = {@ApiResponse(responseCode = "200", description = "OK", useReturnTypeSchema = true)}) @Produces(MediaType.APPLICATION_JSON) public NotificationProvider getProviderByName(@PathParam("name") String name) { LOGGER.debug("Fetching notification provider with name: " + name); @@ -114,6 +124,7 @@ public NotificationProvider getProviderByName(@PathParam("name") String name) { @GET @Path("/ProviderConfigKeys/{providerId}") + @Operation(responses = {@ApiResponse(responseCode = "200", description = "OK", useReturnTypeSchema = true)}) @Produces(MediaType.APPLICATION_JSON) public List getProviderConfigKeys(@PathParam("providerId") int providerId) { LOGGER.debug("Fetching config keys for provider id: " + providerId); @@ -123,6 +134,7 @@ public List getProviderConfigKeys(@PathParam("pro @GET @Path("/HandlerConfigKeys") + @Operation(responses = {@ApiResponse(responseCode = "200", description = "OK", useReturnTypeSchema = true)}) @Produces(MediaType.APPLICATION_JSON) public List getHandlerConfigKeys() { LOGGER.debug("Fetching all handler config keys"); diff --git a/src/java/LogrPortal/src/java/gov/anl/aps/logr/rest/routes/PropertyValueRoute.java b/src/java/LogrPortal/src/java/gov/anl/aps/logr/rest/routes/PropertyValueRoute.java index 6a95eaa02..c9ee69037 100644 --- a/src/java/LogrPortal/src/java/gov/anl/aps/logr/rest/routes/PropertyValueRoute.java +++ b/src/java/LogrPortal/src/java/gov/anl/aps/logr/rest/routes/PropertyValueRoute.java @@ -23,6 +23,7 @@ import gov.anl.aps.logr.rest.authentication.Secured; import io.swagger.v3.oas.annotations.Operation; import io.swagger.v3.oas.annotations.parameters.RequestBody; +import io.swagger.v3.oas.annotations.responses.ApiResponse; import io.swagger.v3.oas.annotations.security.SecurityRequirement; import io.swagger.v3.oas.annotations.tags.Tag; import java.util.ArrayList; @@ -62,7 +63,8 @@ public class PropertyValueRoute extends BaseRoute { @GET @Path("/ById/{id}") - @Produces(MediaType.APPLICATION_JSON) + @Operation(responses = {@ApiResponse(responseCode = "200", description = "OK", useReturnTypeSchema = true)}) + @Produces(MediaType.APPLICATION_JSON) public PropertyValue getPropertyValue(@PathParam("id") int id) { LOGGER.debug("Fetching property value with id: " + id); PropertyValue propertyValue = propertyValueFacade.find(id); @@ -73,7 +75,8 @@ public PropertyValue getPropertyValue(@PathParam("id") int id) { @GET @Path("/ByPropertyTypeId/{propertyTypeId}") - @Produces(MediaType.APPLICATION_JSON) + @Operation(responses = {@ApiResponse(responseCode = "200", description = "OK", useReturnTypeSchema = true)}) + @Produces(MediaType.APPLICATION_JSON) public List getPropertyValuesByPropertyTypeId(@PathParam("propertyTypeId") int propertyTypeId) { LOGGER.debug("Fetching property values with type id: " + propertyTypeId); @@ -86,7 +89,8 @@ public List getPropertyValuesByPropertyTypeId(@PathParam("propert @GET @Path("/ByPropertyTypeId/{propertyTypeId}/AndValue/{propertyValue}") - @Produces(MediaType.APPLICATION_JSON) + @Operation(responses = {@ApiResponse(responseCode = "200", description = "OK", useReturnTypeSchema = true)}) + @Produces(MediaType.APPLICATION_JSON) public List getPropertyValuesByPropertyTypeIdAndValue(@PathParam("propertyTypeId") int propertyTypeId, @PathParam("propertyValue") String propertyValue) { LOGGER.debug("Fetching property values with type id: " + propertyTypeId + " and value: " + propertyValue); @@ -97,7 +101,8 @@ public List getPropertyValuesByPropertyTypeIdAndValue(@PathParam( @GET @Path("/ById/{id}/History") - @Produces(MediaType.APPLICATION_JSON) + @Operation(responses = {@ApiResponse(responseCode = "200", description = "OK", useReturnTypeSchema = true)}) + @Produces(MediaType.APPLICATION_JSON) public List getPropertyValueHistory(@PathParam("id") int id) { PropertyValue propertyValue = getPropertyValue(id); return propertyValue.getPropertyValueHistoryList(); @@ -105,7 +110,8 @@ public List getPropertyValueHistory(@PathParam("id") int i @GET @Path("/ById/{id}/Metadata") - @Produces(MediaType.APPLICATION_JSON) + @Operation(responses = {@ApiResponse(responseCode = "200", description = "OK", useReturnTypeSchema = true)}) + @Produces(MediaType.APPLICATION_JSON) public List getPropertyValueMetadata(@PathParam("id") int id) { PropertyValue propertyValue = getPropertyValue(id); return propertyValue.getPropertyMetadataList(); @@ -113,7 +119,7 @@ public List getPropertyValueMetadata(@PathParam("id") int id) @DELETE @Path("/DeleteById/{propertyValueId}") - @Operation(summary = "Delete a property value by its id.") + @Operation(summary = "Delete a property value by its id.", responses = {@ApiResponse(responseCode = "200", description = "OK", useReturnTypeSchema = true)}) @SecurityRequirement(name = "belyAuth") @Secured public void deletePropertyById(@PathParam("propertyValueId") int propertyValueId) throws ObjectNotFound, InvalidArgument, AuthorizationError, CdbException { @@ -150,6 +156,7 @@ public void deletePropertyById(@PathParam("propertyValueId") int propertyValueId @POST @Path("/ById/{id}/AddUpdateMetadata") + @Operation(responses = {@ApiResponse(responseCode = "200", description = "OK", useReturnTypeSchema = true)}) @Produces(MediaType.APPLICATION_JSON) @Consumes(MediaType.APPLICATION_JSON) @SecurityRequirement(name = "belyAuth") diff --git a/src/java/LogrPortal/src/java/gov/anl/aps/logr/rest/routes/SearchRoute.java b/src/java/LogrPortal/src/java/gov/anl/aps/logr/rest/routes/SearchRoute.java index 08f86936d..5437640c4 100644 --- a/src/java/LogrPortal/src/java/gov/anl/aps/logr/rest/routes/SearchRoute.java +++ b/src/java/LogrPortal/src/java/gov/anl/aps/logr/rest/routes/SearchRoute.java @@ -32,6 +32,7 @@ import gov.anl.aps.logr.rest.entities.SearchEntitiesResults; import io.swagger.v3.oas.annotations.Operation; import io.swagger.v3.oas.annotations.Parameter; +import io.swagger.v3.oas.annotations.responses.ApiResponse; import io.swagger.v3.oas.annotations.parameters.RequestBody; import io.swagger.v3.oas.annotations.tags.Tag; import java.util.ArrayList; @@ -71,7 +72,7 @@ public class SearchRoute { @GET @Path("/{searchText}") @Produces(MediaType.APPLICATION_JSON) - @Operation(summary = "Search log documents and log entries.") + @Operation(summary = "Search log documents and log entries.", responses = {@ApiResponse(responseCode = "200", description = "OK", useReturnTypeSchema = true)}) public LogbookSearchResults searchLogbook( @Parameter(description = "Search text with wildcard support (? for single char, * for multiple)", required = true) @PathParam("searchText") String searchText, @@ -141,6 +142,7 @@ public LogbookSearchResults searchLogbook( @Path("/GenericSearch") @Produces(MediaType.APPLICATION_JSON) @Consumes(MediaType.APPLICATION_JSON) + @Operation(responses = {@ApiResponse(responseCode = "200", description = "OK", useReturnTypeSchema = true)}) public SearchEntitiesResults genericSearch(@RequestBody(required = true) SearchEntitiesOptions searchEntitiesOptions) throws InvalidRequest { SearchEntitiesResults results = new SearchEntitiesResults(); String searchText = searchEntitiesOptions.getSearchText(); diff --git a/src/java/LogrPortal/src/java/gov/anl/aps/logr/rest/routes/SystemLogRoute.java b/src/java/LogrPortal/src/java/gov/anl/aps/logr/rest/routes/SystemLogRoute.java index 3dc53ee01..8f33745e7 100644 --- a/src/java/LogrPortal/src/java/gov/anl/aps/logr/rest/routes/SystemLogRoute.java +++ b/src/java/LogrPortal/src/java/gov/anl/aps/logr/rest/routes/SystemLogRoute.java @@ -10,6 +10,8 @@ import gov.anl.aps.logr.portal.model.db.entities.Log; import gov.anl.aps.logr.rest.authentication.Secured; import io.swagger.v3.oas.annotations.security.SecurityRequirement; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.responses.ApiResponse; import io.swagger.v3.oas.annotations.tags.Tag; import java.util.Date; import java.util.List; @@ -36,7 +38,8 @@ public class SystemLogRoute extends BaseRoute { LogFacade logFacade; @GET - @Path("/System/LoginInfo") + @Path("/System/LoginInfo") + @Operation(responses = {@ApiResponse(responseCode = "200", description = "OK", useReturnTypeSchema = true)}) @Produces(MediaType.APPLICATION_JSON) @SecurityRequirement(name = "belyAuth") @Secured @@ -48,7 +51,8 @@ public List getSuccessfulLoginLog() throws InvalidSession { } @GET - @Path("/System/LoginWarning") + @Path("/System/LoginWarning") + @Operation(responses = {@ApiResponse(responseCode = "200", description = "OK", useReturnTypeSchema = true)}) @Produces(MediaType.APPLICATION_JSON) @SecurityRequirement(name = "belyAuth") @Secured @@ -61,6 +65,7 @@ public List getUnsuccessfulLoginLog() throws InvalidSession { @GET @Path("/System/EntityInfo") + @Operation(responses = {@ApiResponse(responseCode = "200", description = "OK", useReturnTypeSchema = true)}) @Produces(MediaType.APPLICATION_JSON) @SecurityRequirement(name = "belyAuth") @Secured @@ -73,6 +78,7 @@ public List getSuccessfulEntityUpdateLog() throws InvalidSession { @GET @Path("/System/EntityInfoSinceEnteredDate/{sinceDate}") + @Operation(responses = {@ApiResponse(responseCode = "200", description = "OK", useReturnTypeSchema = true)}) @Produces(MediaType.APPLICATION_JSON) @SecurityRequirement(name = "belyAuth") @Secured @@ -84,7 +90,8 @@ public List getSuccessfulEntityUpdateLogSinceEnteredDate(@PathParam("sinceD } @GET - @Path("/System/EntityWarning") + @Path("/System/EntityWarning") + @Operation(responses = {@ApiResponse(responseCode = "200", description = "OK", useReturnTypeSchema = true)}) @Produces(MediaType.APPLICATION_JSON) @SecurityRequirement(name = "belyAuth") @Secured @@ -97,6 +104,7 @@ public List getUnsuccessfulEntityUpdateLog() throws InvalidSession { @GET @Path("/System/EntityWarningSinceEnteredDate/{sinceDate}") + @Operation(responses = {@ApiResponse(responseCode = "200", description = "OK", useReturnTypeSchema = true)}) @Produces(MediaType.APPLICATION_JSON) @SecurityRequirement(name = "belyAuth") @Secured diff --git a/src/java/LogrPortal/src/java/gov/anl/aps/logr/rest/routes/TestRoute.java b/src/java/LogrPortal/src/java/gov/anl/aps/logr/rest/routes/TestRoute.java index 44ca55df5..7a260a2e1 100644 --- a/src/java/LogrPortal/src/java/gov/anl/aps/logr/rest/routes/TestRoute.java +++ b/src/java/LogrPortal/src/java/gov/anl/aps/logr/rest/routes/TestRoute.java @@ -6,6 +6,8 @@ import gov.anl.aps.logr.rest.authentication.Secured; import gov.anl.aps.logr.rest.entities.ApiExceptionMessage; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.responses.ApiResponse; import io.swagger.v3.oas.annotations.security.SecurityRequirement; import io.swagger.v3.oas.annotations.tags.Tag; import javax.ws.rs.GET; @@ -27,6 +29,7 @@ public class TestRoute { @GET @Path("/Auth") + @Operation(responses = {@ApiResponse(responseCode = "200", description = "OK", useReturnTypeSchema = true)}) @Produces(MediaType.APPLICATION_JSON) @SecurityRequirement(name = "belyAuth") @Secured @@ -37,6 +40,7 @@ public boolean verifyAuthenticated() { @GET @Path("/NoAuth") + @Operation(responses = {@ApiResponse(responseCode = "200", description = "OK", useReturnTypeSchema = true)}) @Produces(MediaType.APPLICATION_JSON) public boolean verifyConnection() { LOGGER.debug("User is connected."); @@ -45,7 +49,8 @@ public boolean verifyConnection() { @GET @Path("/SampleErrorMessage") - @Produces(MediaType.APPLICATION_JSON) + @Operation(responses = {@ApiResponse(responseCode = "200", description = "OK", useReturnTypeSchema = true)}) + @Produces(MediaType.APPLICATION_JSON) public ApiExceptionMessage getSampleErrorMessage() { Exception exception = new Exception("Sample Exception Message"); ApiExceptionMessage apiExceptionMessage = new ApiExceptionMessage(exception); diff --git a/src/java/LogrPortal/src/java/gov/anl/aps/logr/rest/routes/UsersRoute.java b/src/java/LogrPortal/src/java/gov/anl/aps/logr/rest/routes/UsersRoute.java index bfb134031..879c5e1e4 100644 --- a/src/java/LogrPortal/src/java/gov/anl/aps/logr/rest/routes/UsersRoute.java +++ b/src/java/LogrPortal/src/java/gov/anl/aps/logr/rest/routes/UsersRoute.java @@ -8,6 +8,8 @@ import gov.anl.aps.logr.portal.model.db.beans.UserInfoFacade; import gov.anl.aps.logr.portal.model.db.entities.UserGroup; import gov.anl.aps.logr.portal.model.db.entities.UserInfo; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.responses.ApiResponse; import io.swagger.v3.oas.annotations.tags.Tag; import java.util.List; import javax.ejb.EJB; @@ -37,6 +39,7 @@ public class UsersRoute extends BaseRoute { @GET @Path("/all") + @Operation(responses = {@ApiResponse(responseCode = "200", description = "OK", useReturnTypeSchema = true)}) @Produces(MediaType.APPLICATION_JSON) public List getAll() { LOGGER.debug("Fetching all users."); @@ -45,6 +48,7 @@ public List getAll() { @GET @Path("/ByUsername/{username}") + @Operation(responses = {@ApiResponse(responseCode = "200", description = "OK", useReturnTypeSchema = true)}) @Produces(MediaType.APPLICATION_JSON) public UserInfo getUserByUsername(@PathParam("username") String username) { LOGGER.debug("Fetching user by username: " + username); @@ -53,6 +57,7 @@ public UserInfo getUserByUsername(@PathParam("username") String username) { @GET @Path("/allGroups") + @Operation(responses = {@ApiResponse(responseCode = "200", description = "OK", useReturnTypeSchema = true)}) @Produces(MediaType.APPLICATION_JSON) public List getAllGroups() { LOGGER.debug("Fetching all groups."); @@ -61,6 +66,7 @@ public List getAllGroups() { @GET @Path("/ByGroupName/{groupName}") + @Operation(responses = {@ApiResponse(responseCode = "200", description = "OK", useReturnTypeSchema = true)}) @Produces(MediaType.APPLICATION_JSON) public UserGroup getGroupByName(@PathParam("groupName") String groupName) { LOGGER.debug("Fetching user by username: " + groupName); diff --git a/src/java/LogrPortal/web/resources/css/logbook.css b/src/java/LogrPortal/web/resources/css/logbook.css index fde8820a6..8f7311a63 100644 --- a/src/java/LogrPortal/web/resources/css/logbook.css +++ b/src/java/LogrPortal/web/resources/css/logbook.css @@ -43,7 +43,7 @@ html, body { } .logEntryDialogEntryValue { - width: 500px; + width: 600px; height: 400px; } diff --git a/src/java/LogrPortal/web/views/itemDomainLogbook/private/addLogbookEntryDialog.xhtml b/src/java/LogrPortal/web/views/itemDomainLogbook/private/addLogbookEntryDialog.xhtml index df4a86e90..842f0ae26 100644 --- a/src/java/LogrPortal/web/views/itemDomainLogbook/private/addLogbookEntryDialog.xhtml +++ b/src/java/LogrPortal/web/views/itemDomainLogbook/private/addLogbookEntryDialog.xhtml @@ -28,49 +28,61 @@ See LICENSE file. - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/java/LogrPortal/web/views/log/private/logAttachmentFileUpload.xhtml b/src/java/LogrPortal/web/views/log/private/logAttachmentFileUpload.xhtml index e0b535dd0..16339748a 100644 --- a/src/java/LogrPortal/web/views/log/private/logAttachmentFileUpload.xhtml +++ b/src/java/LogrPortal/web/views/log/private/logAttachmentFileUpload.xhtml @@ -9,8 +9,9 @@ See LICENSE file. xmlns:p="http://primefaces.org/ui"> - diff --git a/src/java/LogrPortal/web/views/notificationConfiguration/private/notificationConfigurationDeleteDialog.xhtml b/src/java/LogrPortal/web/views/notificationConfiguration/private/notificationConfigurationDeleteDialog.xhtml index 522b3724f..53dd75eae 100644 --- a/src/java/LogrPortal/web/views/notificationConfiguration/private/notificationConfigurationDeleteDialog.xhtml +++ b/src/java/LogrPortal/web/views/notificationConfiguration/private/notificationConfigurationDeleteDialog.xhtml @@ -12,8 +12,10 @@ diff --git a/src/java/LogrPortal/web/views/notificationConfiguration/private/notificationConfigurationEditDialog.xhtml b/src/java/LogrPortal/web/views/notificationConfiguration/private/notificationConfigurationEditDialog.xhtml index 44549b6c0..1f8f49564 100644 --- a/src/java/LogrPortal/web/views/notificationConfiguration/private/notificationConfigurationEditDialog.xhtml +++ b/src/java/LogrPortal/web/views/notificationConfiguration/private/notificationConfigurationEditDialog.xhtml @@ -18,43 +18,22 @@ - - - - - - - - - - - - + + + + - - - - - - + + - + + @@ -86,7 +65,14 @@ styleClass="fullWidthInput" /> - + + + @@ -112,14 +98,14 @@ - + @@ -137,6 +123,35 @@ + + + + + + + + + + + + + + + + + + diff --git a/src/java/LogrPortal/web/views/notificationConfiguration/private/notificationConfigurationListDataTable.xhtml b/src/java/LogrPortal/web/views/notificationConfiguration/private/notificationConfigurationListDataTable.xhtml index 45ead4b00..7d8ef4317 100644 --- a/src/java/LogrPortal/web/views/notificationConfiguration/private/notificationConfigurationListDataTable.xhtml +++ b/src/java/LogrPortal/web/views/notificationConfiguration/private/notificationConfigurationListDataTable.xhtml @@ -17,10 +17,6 @@ - - - - diff --git a/src/java/LogrPortal/web/views/notificationConfiguration/private/notificationConfigurationListView.xhtml b/src/java/LogrPortal/web/views/notificationConfiguration/private/notificationConfigurationListView.xhtml index 7fbf8e864..98d5db38d 100644 --- a/src/java/LogrPortal/web/views/notificationConfiguration/private/notificationConfigurationListView.xhtml +++ b/src/java/LogrPortal/web/views/notificationConfiguration/private/notificationConfigurationListView.xhtml @@ -2,19 +2,32 @@ -
- -
+ +
+ + + +
+
diff --git a/src/java/LogrPortal/web/views/userInfo/edit.xhtml b/src/java/LogrPortal/web/views/userInfo/edit.xhtml index f0678a8d1..a3222034d 100644 --- a/src/java/LogrPortal/web/views/userInfo/edit.xhtml +++ b/src/java/LogrPortal/web/views/userInfo/edit.xhtml @@ -31,18 +31,7 @@ See LICENSE file.
- - - - - - - - - - - - + + + + + + + + + + + + + +

Settings are managed by the system. You can reset all your settings to the default values.

+ +
+ +
+ diff --git a/support/bin/install_glassfish.sh b/support/bin/install_glassfish.sh index 8a498817a..4122a1156 100755 --- a/support/bin/install_glassfish.sh +++ b/support/bin/install_glassfish.sh @@ -7,7 +7,7 @@ CDB_HOST_ARCH=$(uname -sm | tr -s '[:upper:][:blank:]' '[:lower:][\-]') CDB_HOSTNAME=`hostname -f` -PAYARA_VERSION=5.192 +PAYARA_VERSION=5.2022.5 PAYARA_ZIP_FILE=payara-$PAYARA_VERSION.zip PAYARA_DOWNLOAD_URL=https://search.maven.org/remotecontent?filepath=fish/payara/distributions/payara/$PAYARA_VERSION/$PAYARA_ZIP_FILE PAYARA_DN_NAME="CN=${CDB_HOSTNAME}" @@ -43,7 +43,7 @@ mkdir -p $srcDir cd $srcDir if [ ! -f $PAYARA_ZIP_FILE ]; then echo "Retrieving $PAYARA_DOWNLOAD_URL" - curl -L -O $PAYARA_DOWNLOAD_URL + curl -L -o $PAYARA_ZIP_FILE $PAYARA_DOWNLOAD_URL fi if [ ! -f $PAYARA_ZIP_FILE ]; then @@ -74,6 +74,10 @@ chmod -R ug+rwx $payaraInstallDir/glassfish/bin chmod -R o-rwx $payaraInstallDir/bin chmod -R o-rwx $payaraInstallDir/glassfish/bin +# create production domain +echo "Creating production domain" +$ASADMIN_CMD create-domain --nopassword production + export PAYARA_DOMAIN_NAME=production # backup passwords diff --git a/tools/developer_tools/bely-mqtt-message-broker/conda-recipe/README.md b/tools/developer_tools/bely-mqtt-message-broker/conda-recipe/README.md deleted file mode 100644 index 5af3c8b03..000000000 --- a/tools/developer_tools/bely-mqtt-message-broker/conda-recipe/README.md +++ /dev/null @@ -1,134 +0,0 @@ -# Conda Recipe for BELY MQTT Framework - -This directory contains the conda recipe for building the BELY MQTT Framework package. - -## Prerequisites - -- Conda or Miniconda installed -- conda-build package: `conda install conda-build` -- anaconda-client (for uploading): `conda install anaconda-client` - -## Building the Package - -### Quick Build - -```bash -# Run the build script -./conda-recipe/build_conda_package.sh -``` - -### Manual Build - -```bash -# Set build output directory (important if running from conda env in project) -export CONDA_BLD_PATH="${HOME}/conda-bld" - -# Build for current platform -conda build conda-recipe/ --output-folder "${CONDA_BLD_PATH}" - -# Build with specific Python version -conda build conda-recipe/ --python 3.11 --output-folder "${CONDA_BLD_PATH}" - -# Build for all Python versions defined in conda_build_config.yaml -conda build conda-recipe/ --variants --output-folder "${CONDA_BLD_PATH}" -``` - -## Testing the Package - -The package includes automated tests that run during the build process: -- Import tests for all modules -- CLI command tests -- Basic functionality tests - -## Uploading to Private Repository - -### To Anaconda Cloud (Private Channel) - -```bash -# Login to Anaconda Cloud -anaconda login - -# Upload the package -anaconda upload --user YOUR_ORG --channel YOUR_CHANNEL /path/to/package.tar.bz2 - -# Upload all built packages -anaconda upload --user YOUR_ORG --channel YOUR_CHANNEL ~/conda-bld/**/*.tar.bz2 -``` - -### To Private Conda Server - -```bash -# Example for Artifactory -curl -u username:password -T /path/to/package.tar.bz2 \ - "https://your-artifactory.com/artifactory/conda-local/linux-64/package.tar.bz2" -``` - -## Installing from Private Repository - -### From Anaconda Cloud - -```bash -# Add your private channel -conda config --add channels https://conda.anaconda.org/YOUR_ORG/YOUR_CHANNEL - -# Install the package -conda install bely-mqtt-framework -``` - -### From Private Server - -```bash -# Add your private repository -conda config --add channels https://your-server.com/conda/channel - -# Install the package -conda install bely-mqtt-framework -``` - -## Package Variants - -The recipe builds packages for multiple Python versions: -- Python 3.9 -- Python 3.10 -- Python 3.11 -- Python 3.12 - -All packages are noarch (platform-independent). - -## Optional Dependencies - -To include optional dependencies (like apprise for notifications): - -```bash -# Install with apprise support -conda install bely-mqtt-framework apprise -``` - -## Troubleshooting - -### Build Failures - -1. Check conda-build is up to date: `conda update conda-build` -2. Clear conda cache: `conda clean --all` -3. Check build logs in `~/conda-bld/work/` - -### "Can't merge/copy source into subdirectory of itself" Error - -This occurs when trying to build conda packages with the build directory inside the source tree. The build script automatically handles this by: - -1. Using a temporary directory for the build process -2. Copying the final artifacts to `./conda-bld` in your project -3. Cleaning up the temporary directory - -This allows you to keep build artifacts locally while avoiding the circular reference issue. - -### Import Errors - -1. Ensure all dependencies are available in your conda channels -2. Check for conflicting packages: `conda list | grep pydantic` - -### Upload Issues - -1. Verify anaconda-client is installed: `conda install anaconda-client` -2. Check authentication: `anaconda whoami` -3. Verify channel permissions \ No newline at end of file diff --git a/tools/developer_tools/bely-mqtt-message-broker/conda-recipe/build.sh b/tools/developer_tools/bely-mqtt-message-broker/conda-recipe/build.sh new file mode 100755 index 000000000..b7583ebaf --- /dev/null +++ b/tools/developer_tools/bely-mqtt-message-broker/conda-recipe/build.sh @@ -0,0 +1,3 @@ +#!/usr/bin/env bash + +$PYTHON -m pip install . --no-deps --no-build-isolation diff --git a/tools/developer_tools/bely-mqtt-message-broker/conda-recipe/build_conda_package.sh b/tools/developer_tools/bely-mqtt-message-broker/conda-recipe/build_conda_package.sh deleted file mode 100755 index 96ee31437..000000000 --- a/tools/developer_tools/bely-mqtt-message-broker/conda-recipe/build_conda_package.sh +++ /dev/null @@ -1,91 +0,0 @@ -#!/bin/bash -# Build script for creating conda packages - -set -e - -# Colors for output -GREEN='\033[0;32m' -YELLOW='\033[1;33m' -RED='\033[0;31m' -NC='\033[0m' # No Color - -echo -e "${GREEN}Building BELY MQTT Framework conda package...${NC}" - -# Check if conda-build is installed -if ! command -v conda-build &> /dev/null; then - echo -e "${RED}conda-build is not installed. Installing...${NC}" - conda install -y conda-build -fi - -# Clean previous builds -echo -e "${YELLOW}Cleaning previous builds...${NC}" -rm -rf build/ dist/ *.egg-info/ -conda build purge - -# Set up local directory for final artifacts -SCRIPT_DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )" -PROJECT_ROOT="$( cd "${SCRIPT_DIR}/.." && pwd )" -LOCAL_CONDA_BLD="${PROJECT_ROOT}/conda-bld" - -# Remove existing conda-bld directory to ensure clean build -if [ -d "${LOCAL_CONDA_BLD}" ]; then - echo -e "${YELLOW}Removing existing conda-bld directory...${NC}" - rm -rf "${LOCAL_CONDA_BLD}" -fi -mkdir -p "${LOCAL_CONDA_BLD}" - -# Use a temporary build directory outside the project -TEMP_BUILD_DIR=$(mktemp -d "${TMPDIR:-/tmp}/conda-build.XXXXXX") -export CONDA_BLD_PATH="${TEMP_BUILD_DIR}" - -echo -e "${YELLOW}Using temporary build directory: ${CONDA_BLD_PATH}${NC}" -echo -e "${YELLOW}Final packages will be copied to: ${LOCAL_CONDA_BLD}${NC}" - -# Build the package -echo -e "${YELLOW}Building conda package...${NC}" -conda build conda-recipe/ --output-folder "${CONDA_BLD_PATH}" - -# Get the package path -PACKAGE_PATH=$(conda build conda-recipe/ --output-folder "${CONDA_BLD_PATH}" --output) - -# Copy build artifacts to local directory -echo -e "${YELLOW}Copying build artifacts to project directory...${NC}" -cp -r "${CONDA_BLD_PATH}"/noarch "${LOCAL_CONDA_BLD}/" 2>/dev/null || true -cp -r "${CONDA_BLD_PATH}"/osx-arm64 "${LOCAL_CONDA_BLD}/" 2>/dev/null || true -cp -r "${CONDA_BLD_PATH}"/linux-64 "${LOCAL_CONDA_BLD}/" 2>/dev/null || true -cp -r "${CONDA_BLD_PATH}"/win-64 "${LOCAL_CONDA_BLD}/" 2>/dev/null || true - -# Update package path to local directory -PACKAGE_NAME=$(basename "${PACKAGE_PATH}") -PACKAGE_PATH="${LOCAL_CONDA_BLD}/noarch/${PACKAGE_NAME}" - -# Clean up temporary directory -rm -rf "${TEMP_BUILD_DIR}" - -echo -e "${GREEN}Package built successfully!${NC}" -echo -e "${GREEN}Package location: ${PACKAGE_PATH}${NC}" - -# Optional: Convert to other platforms (skip for noarch packages) -if [[ ! ${PACKAGE_PATH} == *"noarch"* ]]; then - echo -e "${YELLOW}Converting package for other platforms...${NC}" - # Use temp directory for conversion, then copy results - TEMP_CONVERT_DIR=$(mktemp -d "${TMPDIR:-/tmp}/conda-convert.XXXXXX") - conda convert -p all ${PACKAGE_PATH} -o "${TEMP_CONVERT_DIR}" - cp -r "${TEMP_CONVERT_DIR}"/* "${LOCAL_CONDA_BLD}/" 2>/dev/null || true - rm -rf "${TEMP_CONVERT_DIR}" -else - echo -e "${YELLOW}Package is noarch - no platform conversion needed${NC}" -fi - -echo -e "${GREEN}Build complete!${NC}" -echo "" -echo "To upload to your private conda channel:" -echo " anaconda upload ${PACKAGE_PATH}" -echo "" -echo "To install locally:" -echo " conda install -c local bely-mqtt-framework" -echo "" -echo "Or install from the local build directory:" -echo " conda install -c file://${LOCAL_CONDA_BLD} bely-mqtt-framework" -echo "" -echo "Package files are located in: ${LOCAL_CONDA_BLD}" \ No newline at end of file diff --git a/tools/developer_tools/bely-mqtt-message-broker/conda-recipe/conda-build.sh b/tools/developer_tools/bely-mqtt-message-broker/conda-recipe/conda-build.sh new file mode 100755 index 000000000..b2e9c4717 --- /dev/null +++ b/tools/developer_tools/bely-mqtt-message-broker/conda-recipe/conda-build.sh @@ -0,0 +1,51 @@ +#!/bin/bash + +MY_DIR=`dirname $0` && cd $MY_DIR && MY_DIR=`pwd` +ROOT_DIR=$MY_DIR + +ENV_NAME=bely-mqtt-env +CONDA_DIR=$CONDA_PREFIX_1 +echo $CONDA_DIR + +if [ -z $CONDA_DIR ] +then + CONDA_DIR=$CONDA_PREFIX +fi + +if [ -z $CONDA_DIR ] +then + echo '$CONDA_PREFIX must be defined.' + exit 1 +fi + +source $CONDA_DIR/etc/profile.d/conda.sh || exit 1 + +trap 'rm -rf src' EXIT + +# Clean +rm -rvf ./build +rm -rvf src + +# Prepare build source +mkdir -p src +cp -Rv ../src src/ +cp -Rv ../tests src/ +cp -v ../pyproject.toml ../setup.py ../README.md ../LICENSE ../CHANGELOG.md ../pytest.ini src/ + +# Build +conda build . --output-folder ./build || exit 1 + +# Install build into a new env +conda create -n $ENV_NAME -y || exit 1 +conda activate $ENV_NAME || exit 1 +conda install bely-mqtt-message-broker -c ./build -y || exit 1 + + +# Export +conda list -n $ENV_NAME --explicit > $ENV_NAME.txt + +echo "Please use the c2 tool to upload the $ENV_NAME.txt" + +# Clean up +conda activate +conda env remove -n $ENV_NAME diff --git a/tools/developer_tools/bely-mqtt-message-broker/conda-recipe/conda_build_config.yaml b/tools/developer_tools/bely-mqtt-message-broker/conda-recipe/conda_build_config.yaml deleted file mode 100644 index d54e73aad..000000000 --- a/tools/developer_tools/bely-mqtt-message-broker/conda-recipe/conda_build_config.yaml +++ /dev/null @@ -1,14 +0,0 @@ -# Conda build configuration -# This file defines build variants and pinnings - -python: - - 3.9 - - 3.10 - - 3.11 - - 3.12 - -# Pin run dependencies to be compatible with the build dependencies -pin_run_as_build: - python: - min_pin: x.x - max_pin: x.x \ No newline at end of file diff --git a/tools/developer_tools/bely-mqtt-message-broker/conda-recipe/meta.yaml b/tools/developer_tools/bely-mqtt-message-broker/conda-recipe/meta.yaml index d98e74294..5a75b9a20 100644 --- a/tools/developer_tools/bely-mqtt-message-broker/conda-recipe/meta.yaml +++ b/tools/developer_tools/bely-mqtt-message-broker/conda-recipe/meta.yaml @@ -1,65 +1,47 @@ -{% set name = "bely-mqtt-framework" %} -{% set version = "0.1.0" %} +{% set name = "bely-mqtt-message-broker" %} +{% set version = "2026.3.0" %} package: - name: {{ name|lower }} - version: {{ version }} + name: "{{ name|lower }}" + version: "{{ version }}" source: - path: .. + path: ./src build: number: 0 noarch: python - script: {{ PYTHON }} -m pip install . -vv - entry_points: - - bely-mqtt = bely_mqtt.cli:cli requirements: - host: - - python >=3.9 + build: - pip - - setuptools >=65.0 - - wheel + - python>3.10 + - pytest-asyncio + - python-dotenv run: - - python >=3.9 - - click >=8.1.0 + - python>3.10 + - click - paho-mqtt >=2.0.0 - - pydantic >=2.0.0 - - python-dotenv >=1.0.0 - - pluggy >=1.3.0 + - pydantic + - python-dotenv + - pluggy + - pyyaml + - apprise + - bely-api test: imports: - bely_mqtt - - bely_mqtt.models - - bely_mqtt.events - - bely_mqtt.plugin - - bely_mqtt.mqtt_client - commands: - - bely-mqtt --help - - bely-mqtt --version + source_files: + - tests + - pytest.ini requires: - - pytest >=7.0.0 - - pytest-asyncio >=0.21.0 + - pytest + - pytest-asyncio + commands: + - pytest tests -v about: - home: https://github.com/bely-org/bely-mqtt-framework - license: MIT - license_family: MIT - license_file: LICENSE - summary: Pluggable Python framework for handling BELY MQTT events - description: | - BELY MQTT Framework is a pluggable Python framework for handling MQTT events - from BELY (Best Electronic Logbook Yet). It provides: - - Pluggable handler system for MQTT topics - - Type-safe models for BELY events - - Integration with BELY API for additional data - - CLI for easy configuration and management - - Async support for high performance - doc_url: https://github.com/bely-org/bely-mqtt-framework/tree/main/docs - dev_url: https://github.com/bely-org/bely-mqtt-framework - -extra: - recipe-maintainers: - - your-github-username \ No newline at end of file + home: "https://git.aps.anl.gov/controls/hla/bely" + license: "Copyright (c) UChicago Argonne, LLC. All rights reserved." + summary: "Pluggable Python framework for handling BELY MQTT events" diff --git a/tools/developer_tools/bely-mqtt-message-broker/conda-recipe/post-link.sh b/tools/developer_tools/bely-mqtt-message-broker/conda-recipe/post-link.sh deleted file mode 100644 index 36f02e49d..000000000 --- a/tools/developer_tools/bely-mqtt-message-broker/conda-recipe/post-link.sh +++ /dev/null @@ -1,15 +0,0 @@ -#!/bin/bash -# Post-link script that runs after package installation - -echo "" -echo "BELY MQTT Framework has been successfully installed!" -echo "" -echo "To get started:" -echo " 1. Create a handlers directory: mkdir -p handlers" -echo " 2. Create your first handler (see examples)" -echo " 3. Run: bely-mqtt start --handlers-dir ./handlers" -echo "" -echo "For more information:" -echo " - Documentation: https://github.com/bely-org/bely-mqtt-framework/tree/main/docs" -echo " - Examples: https://github.com/bely-org/bely-mqtt-framework/tree/main/examples" -echo "" \ No newline at end of file diff --git a/tools/developer_tools/bely-mqtt-message-broker/conda-recipe/pre-unlink.sh b/tools/developer_tools/bely-mqtt-message-broker/conda-recipe/pre-unlink.sh deleted file mode 100644 index 8c96312f8..000000000 --- a/tools/developer_tools/bely-mqtt-message-broker/conda-recipe/pre-unlink.sh +++ /dev/null @@ -1,10 +0,0 @@ -#!/bin/bash -# Pre-unlink script that runs before package removal - -echo "Removing BELY MQTT Framework..." - -# Clean up any cached files -if [ -d "$HOME/.cache/bely-mqtt" ]; then - echo "Cleaning cache directory..." - rm -rf "$HOME/.cache/bely-mqtt" -fi \ No newline at end of file diff --git a/tools/developer_tools/bely-mqtt-message-broker/conda-recipe/run_test.py b/tools/developer_tools/bely-mqtt-message-broker/conda-recipe/run_test.py deleted file mode 100644 index 639b97f18..000000000 --- a/tools/developer_tools/bely-mqtt-message-broker/conda-recipe/run_test.py +++ /dev/null @@ -1,81 +0,0 @@ -"""Test script for conda package.""" - -import sys -import subprocess - -def test_imports(): - """Test that all modules can be imported.""" - print("Testing imports...") - - try: - import bely_mqtt - print(f"✓ bely_mqtt version: {bely_mqtt.__version__}") - - from bely_mqtt import MQTTHandler, BelyMQTTClient, PluginManager - print("✓ Core classes imported") - - from bely_mqtt.models import ( - LogEntryAddEvent, - LogEntryUpdateEvent, - MQTTMessage - ) - print("✓ Event models imported") - - from bely_mqtt.events import EventType - print("✓ Event types imported") - - except ImportError as e: - print(f"✗ Import failed: {e}") - return False - - return True - -def test_cli(): - """Test CLI commands.""" - print("\nTesting CLI...") - - # Test help command - result = subprocess.run( - [sys.executable, "-m", "bely_mqtt.cli", "--help"], - capture_output=True, - text=True - ) - if result.returncode != 0: - print(f"✗ CLI help failed: {result.stderr}") - return False - print("✓ CLI help works") - - # Test version command - result = subprocess.run( - [sys.executable, "-m", "bely_mqtt.cli", "--version"], - capture_output=True, - text=True - ) - if result.returncode != 0: - print(f"✗ CLI version failed: {result.stderr}") - return False - print(f"✓ CLI version: {result.stdout.strip()}") - - return True - -def main(): - """Run all tests.""" - print("Running conda package tests...\n") - - tests_passed = True - - if not test_imports(): - tests_passed = False - - if not test_cli(): - tests_passed = False - - if tests_passed: - print("\n✓ All tests passed!") - return 0 - else: - print("\n✗ Some tests failed!") - return 1 - -if __name__ == "__main__": - sys.exit(main()) \ No newline at end of file diff --git a/tools/developer_tools/bely-mqtt-message-broker/conda-recipe/variants.yaml b/tools/developer_tools/bely-mqtt-message-broker/conda-recipe/variants.yaml deleted file mode 100644 index bfb3b0865..000000000 --- a/tools/developer_tools/bely-mqtt-message-broker/conda-recipe/variants.yaml +++ /dev/null @@ -1,19 +0,0 @@ -# Build variants for different configurations -# This allows building multiple package variants in one go - -# Python versions to build for -python: - - 3.9 - - 3.10 - - 3.11 - - 3.12 - -# Optional: Build with different dependency versions -pydantic: - - 2.0 - - 2.5 - -# Optional: Build with/without optional dependencies -include_apprise: - - true - - false \ No newline at end of file diff --git a/tools/developer_tools/bely-mqtt-message-broker/examples/handlers/logging_handler_advanced.py b/tools/developer_tools/bely-mqtt-message-broker/examples/handlers/logging_handler_advanced.py index 9e95cfb28..2981d7c77 100644 --- a/tools/developer_tools/bely-mqtt-message-broker/examples/handlers/logging_handler_advanced.py +++ b/tools/developer_tools/bely-mqtt-message-broker/examples/handlers/logging_handler_advanced.py @@ -62,16 +62,16 @@ class AdvancedLoggingHandler(MQTTHandler): def __init__( self, logging_dir: Optional[str] = None, - api_client: Optional[object] = None, + api_factory: Optional[object] = None, ): """ Initialize the advanced logging handler. - + Args: logging_dir: Directory to store log files. If None, uses BELY_LOG_DIR environment variable or current directory. Directory is created if it doesn't exist. - api_client: Optional BELY API client (for compatibility with handler system). + api_factory: Optional BELY API factory (for compatibility with handler system). Examples: # Use environment variable @@ -94,7 +94,7 @@ def __init__( # Use default (current directory) handler = AdvancedLoggingHandler() """ - super().__init__(api_client=api_client) + super().__init__(api_factory=api_factory) # Set up logging directory # Priority: parameter > environment variable > current directory diff --git a/tools/developer_tools/bely-mqtt-message-broker/pyproject.toml b/tools/developer_tools/bely-mqtt-message-broker/pyproject.toml index 4bbbf1345..010639af3 100644 --- a/tools/developer_tools/bely-mqtt-message-broker/pyproject.toml +++ b/tools/developer_tools/bely-mqtt-message-broker/pyproject.toml @@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta" [project] name = "bely-mqtt-framework" -version = "0.1.0" +version = "2026.3.dev0" description = "Pluggable Python framework for handling BELY MQTT events" readme = "README.md" requires-python = ">=3.10" @@ -28,7 +28,7 @@ classifiers = [ dependencies = [ "click>=8.1.0", - "paho-mqtt>=1.6.1", + "paho-mqtt>=2.0.0", "pydantic>=2.0.0", "python-dotenv>=1.0.0", "pluggy>=1.3.0", diff --git a/tools/developer_tools/bely-mqtt-message-broker/setup.py b/tools/developer_tools/bely-mqtt-message-broker/setup.py index 3e2166507..1c648319a 100644 --- a/tools/developer_tools/bely-mqtt-message-broker/setup.py +++ b/tools/developer_tools/bely-mqtt-message-broker/setup.py @@ -4,7 +4,7 @@ setup( name="bely-mqtt-framework", - version="0.1.0", + version="2026.3.dev0", description="Pluggable Python framework for handling BELY MQTT events", long_description=open("README.md").read(), long_description_content_type="text/markdown", @@ -17,7 +17,7 @@ python_requires=">=3.9", install_requires=[ "click>=8.1.0", - "paho-mqtt>=1.6.1", + "paho-mqtt>=2.0.0", "pydantic>=2.0.0", "python-dotenv>=1.0.0", "pluggy>=1.3.0", diff --git a/tools/developer_tools/bely-mqtt-message-broker/src/bely_mqtt/__init__.py b/tools/developer_tools/bely-mqtt-message-broker/src/bely_mqtt/__init__.py index 20a1bb1a5..8cd1fb683 100644 --- a/tools/developer_tools/bely-mqtt-message-broker/src/bely_mqtt/__init__.py +++ b/tools/developer_tools/bely-mqtt-message-broker/src/bely_mqtt/__init__.py @@ -35,7 +35,7 @@ from bely_mqtt.mqtt_client import BelyMQTTClient from bely_mqtt.plugin import MQTTHandler, PluginManager -__version__ = "0.1.0" +__version__ = "2026.3.dev0" __all__ = [ "BelyMQTTClient", diff --git a/tools/developer_tools/python-client/BelyApiFactory.py b/tools/developer_tools/python-client/BelyApiFactory.py index 6dda4da04..906b3d3b5 100755 --- a/tools/developer_tools/python-client/BelyApiFactory.py +++ b/tools/developer_tools/python-client/BelyApiFactory.py @@ -4,6 +4,7 @@ # See LICENSE file. import base64 import os +import warnings from belyApi import Configuration, ApiClient from belyApi import ( @@ -37,53 +38,70 @@ def __init__(self, bely_url): api_client=self.api_client ) - self.downloadsApi = DownloadsApi(api_client=self.api_client) - self.propertyValueApi = PropertyValueApi(api_client=self.api_client) - self.usersApi = UsersApi(api_client=self.api_client) - self.domainApi = DomainApi(api_client=self.api_client) + self.downloads_api = DownloadsApi(api_client=self.api_client) + self.property_value_api = PropertyValueApi(api_client=self.api_client) + self.users_api = UsersApi(api_client=self.api_client) + self.domain_api = DomainApi(api_client=self.api_client) - self.systemlogApi = SystemLogApi(api_client=self.api_client) - self.searchApi = SearchApi(api_client=self.api_client) + self.systemlog_api = SystemLogApi(api_client=self.api_client) + self.search_api = SearchApi(api_client=self.api_client) + + # Deprecated camelCase aliases + self.downloadsApi = self.downloads_api + self.propertyValueApi = self.property_value_api + self.usersApi = self.users_api + self.domainApi = self.domain_api + self.systemlogApi = self.systemlog_api + self.searchApi = self.search_api self.auth_api = AuthenticationApi(api_client=self.api_client) - def get_lobook_api(self) -> LogbookApi: + def _deprecated(self, old_name, new_name): + warnings.warn( + f"{old_name} is deprecated, use {new_name} instead", + DeprecationWarning, + stacklevel=3, + ) + + # -- snake_case methods (primary) -- + + def get_logbook_api(self) -> LogbookApi: return self.logbook_api - def getDomainApi(self) -> DomainApi: - return self.domainApi + def get_domain_api(self) -> DomainApi: + return self.domain_api - def getDownloadApi(self) -> DownloadsApi: - return self.downloadsApi + def get_download_api(self) -> DownloadsApi: + return self.downloads_api - def getPropertyValueApi(self) -> PropertyValueApi: - return self.propertyValueApi + def get_property_value_api(self) -> PropertyValueApi: + return self.property_value_api - def getUsersApi(self) -> UsersApi: - return self.usersApi + def get_users_api(self) -> UsersApi: + return self.users_api - def getSearchApi(self) -> SearchApi: - return self.searchApi + def get_search_api(self) -> SearchApi: + return self.search_api - def getNotificationConfigurationApi(self): + def get_notification_configuration_api(self): return self.notification_configuration_api - def generateCDBUrlForItemId(self, itemId): - return self.URL_FORMAT % (self.cdbUrl, str(itemId)) + def generate_cdb_url_for_item_id(self, item_id): + return self.URL_FORMAT % (self.bely_url, str(item_id)) def authenticate_user(self, username, password): response = self.auth_api.authenticate_user_with_http_info( username=username, password=password ) - token = response[-1][self.HEADER_TOKEN_KEY] + token = response.headers[self.HEADER_TOKEN_KEY] self.__set_authenticate_token(token) def __set_authenticate_token(self, token): self.api_client.set_default_header(self.HEADER_TOKEN_KEY, token) - def getAuthenticateToken(self): - return self.apiClient.default_headers[self.HEADER_TOKEN_KEY] + def get_authenticate_token(self): + return self.api_client.default_headers[self.HEADER_TOKEN_KEY] def test_authenticated(self): self.auth_api.verify_authenticated() @@ -91,6 +109,51 @@ def test_authenticated(self): def logout_user(self): self.auth_api.log_out() + def parse_api_exception(self, open_api_exception): + response_type = ApiExceptionMessage.__name__ + open_api_exception.data = open_api_exception.body + ex_obj = self.api_client.deserialize(open_api_exception, response_type) + ex_obj.status = open_api_exception.status + return ex_obj + + # -- Deprecated camelCase wrappers -- + + def getDomainApi(self) -> DomainApi: + self._deprecated("getDomainApi", "get_domain_api") + return self.get_domain_api() + + def getDownloadApi(self) -> DownloadsApi: + self._deprecated("getDownloadApi", "get_download_api") + return self.get_download_api() + + def getPropertyValueApi(self) -> PropertyValueApi: + self._deprecated("getPropertyValueApi", "get_property_value_api") + return self.get_property_value_api() + + def getUsersApi(self) -> UsersApi: + self._deprecated("getUsersApi", "get_users_api") + return self.get_users_api() + + def getSearchApi(self) -> SearchApi: + self._deprecated("getSearchApi", "get_search_api") + return self.get_search_api() + + def getNotificationConfigurationApi(self): + self._deprecated("getNotificationConfigurationApi", "get_notification_configuration_api") + return self.get_notification_configuration_api() + + def generateCDBUrlForItemId(self, itemId): + self._deprecated("generateCDBUrlForItemId", "generate_cdb_url_for_item_id") + return self.generate_cdb_url_for_item_id(itemId) + + def getAuthenticateToken(self): + self._deprecated("getAuthenticateToken", "get_authenticate_token") + return self.get_authenticate_token() + + def parseApiException(self, openApiException): + self._deprecated("parseApiException", "parse_api_exception") + return self.parse_api_exception(openApiException) + # Restore later # @classmethod # def createFileUploadObject(cls, filePath): @@ -100,13 +163,6 @@ def logout_user(self): # fileName = os.path.basename(filePath) # return FileUploadObject(file_name=fileName, base64_binary=b64String) - def parseApiException(self, openApiException): - responseType = ApiExceptionMessage.__name__ - openApiException.data = openApiException.body - exObj = self.apiClient.deserialize(openApiException, responseType) - exObj.status = openApiException.status - return exObj - # def run_command(): # Example diff --git a/tools/developer_tools/python-client/conda-recipe/API/meta.yaml b/tools/developer_tools/python-client/conda-recipe/API/meta.yaml index ca02bc5e6..cf0bdb9e9 100644 --- a/tools/developer_tools/python-client/conda-recipe/API/meta.yaml +++ b/tools/developer_tools/python-client/conda-recipe/API/meta.yaml @@ -1,5 +1,5 @@ {% set name = "BELY-API" %} -{% set version = "2024.6" %} +{% set version = "2026.3.0" %} package: name: "{{ name|lower }}" @@ -8,14 +8,20 @@ package: source: path: ./src -requirements: +build: + number: 0 + noarch: python + + +requirements: build: - pip - python>3.10 + - setuptools run: - python - python-dateutil - - six + - pydantic - urllib3 - certifi diff --git a/tools/developer_tools/python-client/conda-recipe/CLI/meta.yaml b/tools/developer_tools/python-client/conda-recipe/CLI/meta.yaml index 150337778..84645f803 100644 --- a/tools/developer_tools/python-client/conda-recipe/CLI/meta.yaml +++ b/tools/developer_tools/python-client/conda-recipe/CLI/meta.yaml @@ -21,7 +21,7 @@ requirements: - python - python-dateutil - urllib3 - - six + - pydantic - click - pandas - rich diff --git a/tools/developer_tools/python-client/generatePyClient.sh b/tools/developer_tools/python-client/generatePyClient.sh index cf3aec770..1571df2e3 100755 --- a/tools/developer_tools/python-client/generatePyClient.sh +++ b/tools/developer_tools/python-client/generatePyClient.sh @@ -14,7 +14,7 @@ MY_DIR=`dirname $0` && cd $MY_DIR && MY_DIR=`pwd` ROOT_DIR=$MY_DIR -OPEN_API_VERSION="4.3.1" +OPEN_API_VERSION="7.20.0" OPEN_API_GENERATOR_JAR="openapi-generator-cli-$OPEN_API_VERSION.jar" OPEN_API_GENERATOR_JAR_URL="https://repo1.maven.org/maven2/org/openapitools/openapi-generator-cli/$OPEN_API_VERSION/$OPEN_API_GENERATOR_JAR" @@ -36,7 +36,7 @@ curl -O $OPEN_API_GENERATOR_JAR_URL java -jar $OPEN_API_GENERATOR_JAR generate -i "$CDB_OPENAPI_YML_URL" -g python -o $GEN_OUT_DIR -c $GEN_CONFIG_FILE_PATH || exit 1 # Clean up -rm belyApi -rv +rm -rv belyApi rm $OPEN_API_GENERATOR_JAR # Fetch the generated Api diff --git a/tools/developer_tools/python-client/setup-api.py b/tools/developer_tools/python-client/setup-api.py index 9dbfea59a..413f9626c 100644 --- a/tools/developer_tools/python-client/setup-api.py +++ b/tools/developer_tools/python-client/setup-api.py @@ -8,15 +8,15 @@ from setuptools import setup setup(name='bely_api', - version='2025.3.dev0', + version='2026.3.0', packages=["belyApi", "belyApi.api", "belyApi.models"], py_modules=["BelyApiFactory"], - install_requires=['python-dateutil', + install_requires=['python-dateutil', 'urllib3', 'certifi', - 'six'], + 'pydantic>=1.10'], license='Copyright (c) UChicago Argonne, LLC. All rights reserved.', description='Python client API library used to communicate with BELY API.', maintainer='Dariusz Jarosz', diff --git a/tools/developer_tools/python-client/test/attachment_test.py b/tools/developer_tools/python-client/test/attachment_test.py new file mode 100644 index 000000000..4bfb16ee2 --- /dev/null +++ b/tools/developer_tools/python-client/test/attachment_test.py @@ -0,0 +1,132 @@ +import os +import unittest + +from belyApi import OpenApiException +from test.bely_test_base import BelyTestBase + +TEST_DATA_DIR = os.path.join(os.path.dirname(__file__), "..", "data") +TEST_IMAGE = os.path.join(TEST_DATA_DIR, "AnlLogo.png") + + +class AttachmentUploadTests(BelyTestBase): + + def _create_log_entry(self): + """Log in as admin, create a log entry on DOC_SAMPLE_ID, return (doc_id, log_id).""" + self.login_as_admin() + entry_template = self.logbook_api.get_log_entry_template(self.DOC_SAMPLE_ID) + entry_template.log_entry = "attachment test %s" % self._gen_unique_name() + new_entry = self.logbook_api.add_update_log_entry(entry_template) + return self.DOC_SAMPLE_ID, new_entry.log_id + + def test_upload_attachment_with_file_path(self): + doc_id, log_id = self._create_log_entry() + + result = self.logbook_api.upload_attachment( + log_document_id=doc_id, + log_id=log_id, + body=TEST_IMAGE, + file_name="AnlLogo.png", + ) + + self.assertIsNotNone(result.markdown_reference) + self.assertIsNotNone(result.download_path) + self.assertIsNotNone(result.original_filename) + self.assertIsNotNone(result.stored_filename) + self.assertEqual("AnlLogo.png", result.original_filename) + + def test_upload_attachment_with_bytes(self): + doc_id, log_id = self._create_log_entry() + + with open(TEST_IMAGE, "rb") as f: + file_bytes = f.read() + + result = self.logbook_api.upload_attachment( + log_document_id=doc_id, + log_id=log_id, + body=file_bytes, + file_name="AnlLogo.png", + ) + + self.assertIsNotNone(result.markdown_reference) + self.assertIsNotNone(result.download_path) + self.assertIsNotNone(result.original_filename) + self.assertIsNotNone(result.stored_filename) + self.assertEqual("AnlLogo.png", result.original_filename) + + def test_upload_attachment_requires_auth(self): + with self.assertRaises(OpenApiException): + self.logbook_api.upload_attachment( + log_document_id=self.DOC_SAMPLE_ID, + log_id=1, + body=b"dummy", + file_name="test.txt", + ) + + def test_upload_attachment_requires_filename(self): + doc_id, log_id = self._create_log_entry() + + with self.assertRaises(OpenApiException): + self.logbook_api.upload_attachment( + log_document_id=doc_id, + log_id=log_id, + body=b"dummy", + ) + + def test_get_log_entry_attachments(self): + doc_id, log_id = self._create_log_entry() + + self.logbook_api.upload_attachment( + log_document_id=doc_id, + log_id=log_id, + body=TEST_IMAGE, + file_name="AnlLogo.png", + ) + + attachments = self.logbook_api.get_log_entry_attachments( + log_document_id=doc_id, + log_id=log_id, + ) + + filenames = [a.original_filename for a in attachments] + self.assertIn("AnlLogo.png", filenames) + + def test_download_attachment(self): + doc_id, log_id = self._create_log_entry() + + with open(TEST_IMAGE, "rb") as f: + original_bytes = f.read() + + result = self.logbook_api.upload_attachment( + log_document_id=doc_id, + log_id=log_id, + body=original_bytes, + file_name="AnlLogo.png", + ) + + download_api = self.factory.get_download_api() + response = download_api.get_attachment_without_preload_content( + result.stored_filename + ) + downloaded_bytes = response.data + + self.assertEqual(original_bytes, downloaded_bytes) + + def test_upload_attachment_append_reference(self): + doc_id, log_id = self._create_log_entry() + + result = self.logbook_api.upload_attachment( + log_document_id=doc_id, + log_id=log_id, + body=TEST_IMAGE, + file_name="AnlLogo.png", + append_reference=True, + ) + + log_entries = self.logbook_api.get_log_entries(doc_id) + entry = next(e for e in log_entries if e.log_id == log_id) + + self.assertIn(result.markdown_reference, entry.log_entry) + + +if __name__ == "__main__": + unittest.main() diff --git a/tools/developer_tools/python-client/test/auth_test.py b/tools/developer_tools/python-client/test/auth_test.py index 00ea3e578..89ac4e59a 100644 --- a/tools/developer_tools/python-client/test/auth_test.py +++ b/tools/developer_tools/python-client/test/auth_test.py @@ -1,4 +1,3 @@ -from tkinter.messagebox import NO import unittest from BelyApiFactory import BelyApiFactory diff --git a/tools/developer_tools/python-client/test/bely_test_base.py b/tools/developer_tools/python-client/test/bely_test_base.py index 4d9f3018f..25f432c55 100644 --- a/tools/developer_tools/python-client/test/bely_test_base.py +++ b/tools/developer_tools/python-client/test/bely_test_base.py @@ -24,7 +24,7 @@ class BelyTestBase(unittest.TestCase): def setUp(self): self.factory = BelyApiFactory("http://127.0.0.1:8080/bely") - self.logbook_api = self.factory.get_lobook_api() + self.logbook_api = self.factory.get_logbook_api() self.loggedIn = False diff --git a/tools/developer_tools/python-client/test/log_document_test.py b/tools/developer_tools/python-client/test/log_document_test.py index 27f87b1ad..775799c53 100644 --- a/tools/developer_tools/python-client/test/log_document_test.py +++ b/tools/developer_tools/python-client/test/log_document_test.py @@ -1,8 +1,7 @@ -from tkinter.messagebox import NO import unittest from BelyApiFactory import BelyApiFactory -from belyApi import LogDocumentOptions, OpenApiException +from belyApi import ItemDomainLogbook, LogDocumentOptions, OpenApiException from test.bely_test_base import BelyTestBase @@ -41,6 +40,24 @@ def test_fetch_logbook_document_with_more_info(self): self.assertIsNotNone(more_info.last_modified_on_date_time) + def test_fetch_log_document_by_name(self): + doc_name = self._gen_unique_name() + " ByName" + options = LogDocumentOptions(name=doc_name, logbook_type_id=self.CTL_LOGBOOK_ID) + + self.login_as_user() + created_doc = self.logbook_api.create_logbook_document(log_document_options=options) + + result = self.logbook_api.get_log_document_by_name(name=doc_name) + + self.assertIsInstance(result, ItemDomainLogbook) + self.assertEqual(result.name, doc_name) + self.assertEqual(result.id, created_doc.id) + + def test_fetch_log_document_by_name_not_found(self): + with self.assertRaises(OpenApiException): + self.logbook_api.get_log_document_by_name(name="NonExistentDoc_12345") + + class LogDocumentEditTests(BelyTestBase): def test_create_logbook_document(self): @@ -195,7 +212,7 @@ def test_create_log_document_with_sections(self): self.login_as_user() - options = LogDocumentOptions(doc_name, logbook_type_id=self.CTL_LOGBOOK_ID) + options = LogDocumentOptions(name=doc_name, logbook_type_id=self.CTL_LOGBOOK_ID) new_doc = self.logbook_api.create_logbook_document(options) diff --git a/tools/developer_tools/python-client/test/log_entry_test.py b/tools/developer_tools/python-client/test/log_entry_test.py index d9af65d69..127bc51a5 100644 --- a/tools/developer_tools/python-client/test/log_entry_test.py +++ b/tools/developer_tools/python-client/test/log_entry_test.py @@ -1,4 +1,3 @@ -from tkinter.messagebox import NO import unittest from belyApi import OpenApiException from test.bely_test_base import BelyTestBase diff --git a/tools/developer_tools/python-client/test/requirements.txt b/tools/developer_tools/python-client/test/requirements.txt index 933025c2d..d31910ae2 100644 --- a/tools/developer_tools/python-client/test/requirements.txt +++ b/tools/developer_tools/python-client/test/requirements.txt @@ -1,5 +1,5 @@ certifi +pydantic pytest python-dateutil -six urllib3 diff --git a/tools/developer_tools/python-client/test/search_test.py b/tools/developer_tools/python-client/test/search_test.py new file mode 100644 index 000000000..28f204930 --- /dev/null +++ b/tools/developer_tools/python-client/test/search_test.py @@ -0,0 +1,146 @@ +import unittest +import uuid + +from belyApi import SearchEntitiesOptions +from test.bely_test_base import BelyTestBase + + +class SearchTests(BelyTestBase): + + def setUp(self): + super().setUp() + self.search_api = self.factory.get_search_api() + + # -- search_logbook (GET /api/Search/{searchText}) -- + + def test_search_documents_by_name(self): + results = self.search_api.search_logbook("Sample*") + + self.assertIsNotNone(results.document_results) + self.assertGreater(len(results.document_results), 0) + + names = [r.object_name for r in results.document_results] + self.assertTrue( + any("Sample" in n for n in names), + f"Expected a document name containing 'Sample', got: {names}", + ) + + def test_search_log_entries(self): + results = self.search_api.search_logbook("Top level*") + + self.assertIsNotNone(results.log_entry_results) + self.assertGreater(len(results.log_entry_results), 0) + + doc_ids = [r.log_document_id for r in results.log_entry_results] + self.assertIn( + self.DOC_WITH_ENTRIES, + doc_ids, + f"Expected log document ID {self.DOC_WITH_ENTRIES} in results, got: {doc_ids}", + ) + + def test_search_no_results(self): + nonsense = uuid.uuid4().hex + results = self.search_api.search_logbook(nonsense) + + doc_results = results.document_results or [] + entry_results = results.log_entry_results or [] + + self.assertEqual(len(doc_results), 0) + self.assertEqual(len(entry_results), 0) + + def test_search_with_logbook_type_filter(self): + results = self.search_api.search_logbook( + "test_doc*", logbook_type_id=[self.OPS_LOGBOOK_ID] + ) + + all_results = (results.document_results or []) + ( + results.log_entry_results or [] + ) + for r in all_results: + if r.logbook_type is not None: + # Verify each result belongs to the filtered logbook type + self.assertIsNotNone(r.logbook_type) + + def test_search_case_insensitive(self): + # Case-insensitive search should find "Sample log document" + ci_results = self.search_api.search_logbook( + "sample*", case_insensitive=True + ) + ci_docs = ci_results.document_results or [] + self.assertGreater( + len(ci_docs), 0, "Case-insensitive search for 'sample*' should find results" + ) + + # Case-sensitive search for lowercase should not match "Sample..." + cs_results = self.search_api.search_logbook( + "sample*", case_insensitive=False + ) + cs_docs = cs_results.document_results or [] + self.assertEqual( + len(cs_docs), + 0, + f"Case-sensitive search for 'sample*' should find no documents, got: " + f"{[r.object_name for r in cs_docs]}", + ) + + def test_search_wildcard(self): + results = self.search_api.search_logbook("?ection*") + + all_results = (results.document_results or []) + ( + results.log_entry_results or [] + ) + self.assertGreater( + len(all_results), + 0, + "Expected results matching '?ection*' wildcard pattern", + ) + + names = [r.object_name for r in all_results] + self.assertTrue( + any("ection" in (n or "") for n in names), + f"Expected a result containing 'ection', got: {names}", + ) + + # -- generic_search (POST /api/Search/GenericSearch) -- + + def test_generic_search_users(self): + options = SearchEntitiesOptions( + search_text="logr", include_user=True + ) + results = self.search_api.generic_search(options) + + self.assertIsNotNone(results.user_results) + self.assertGreater(len(results.user_results), 0) + + def test_generic_search_no_flags(self): + options = SearchEntitiesOptions(search_text="logr") + results = self.search_api.generic_search(options) + + # With no include flags, all result lists should be empty or None + for field_name in [ + "item_domain_catalog_results", + "item_domain_inventory_results", + "item_domain_machine_design_results", + "item_domain_cable_catalog_results", + "item_domain_cable_inventory_results", + "item_domain_cable_design_results", + "item_domain_location_results", + "item_domain_maarc_results", + "item_element_results", + "item_type_results", + "item_category_results", + "property_type_results", + "property_type_category_results", + "source_results", + "user_results", + "user_group_results", + ]: + value = getattr(results, field_name) + self.assertTrue( + value is None or len(value) == 0, + f"Expected {field_name} to be empty/None, got: {value}", + ) + + +if __name__ == "__main__": + unittest.main()