diff --git a/node/code/dev/border_router/start_network.sh b/node/code/dev/border_router/start_network.sh new file mode 100755 index 00000000..cf72f07b --- /dev/null +++ b/node/code/dev/border_router/start_network.sh @@ -0,0 +1,123 @@ +#!/usr/bin/env bash + +USB_CDC_ECM_DIR="$(dirname "$(readlink -f "$0")")" + +INTERFACE_CHECK_COUNTER=5 # 5 attempts to find usb interface + +find_interface() { + INTERFACE=$(ls -A /sys/bus/usb/drivers/cdc_ether/*/net/ 2>/dev/null) + INTERFACE_CHECK=$(echo -n "${INTERFACE}" | head -c1 | wc -c) + if [ "${INTERFACE_CHECK}" -eq 0 ] && [ ${INTERFACE_CHECK_COUNTER} != 0 ]; then + # We want to have multiple opportunities to find the USB interface + # as sometimes it can take a few seconds for it to enumerate after + # the device has been flashed. + sleep 1 + ((INTERFACE_CHECK_COUNTER=INTERFACE_CHECK_COUNTER-1)) + find_interface + fi + INTERFACE=${INTERFACE%/} +} + +echo "Waiting for network interface." +find_interface + +if [ "${INTERFACE_CHECK}" -eq 0 ]; then + echo "Unable to find network interface" + exit 1 +else + echo "Found interface: ${INTERFACE}" +fi + +setup_interface() { + sysctl -w net.ipv6.conf."${INTERFACE}".forwarding=1 + sysctl -w net.ipv6.conf."${INTERFACE}".accept_ra=0 + ip link set "${INTERFACE}" up + ip a a fe80::1/64 dev "${INTERFACE}" + ip a a fd00:dead:beef::1/128 dev lo +} + +cleanup_interface() { + ip a d fe80::1/64 dev "${INTERFACE}" + ip a d fd00:dead:beef::1/128 dev lo + ip route del "${PREFIX}" via fe80::2 dev "${INTERFACE}" +} + +cleanup() { + echo "Cleaning up..." + cleanup_interface + if [ -n "${UHCPD_PID}" ]; then + kill "${UHCPD_PID}" + fi + if [ -n "${DHCPD_PIDFILE}" ]; then + kill "$(cat "${DHCPD_PIDFILE}")" + rm "${DHCPD_PIDFILE}" + fi + trap "" INT QUIT TERM EXIT +} + +start_uhcpd() { + ip route add "${PREFIX}" via fe80::2 dev "${INTERFACE}" + ${UHCPD} "${INTERFACE}" "${PREFIX}" > /dev/null & + UHCPD_PID=$! +} + +start_dhcpd() { + ip route add "${PREFIX}" via fe80::2 dev "${INTERFACE}" + DHCPD_PIDFILE=$(mktemp) + ${DHCPD} -d -p "${DHCPD_PIDFILE}" "${INTERFACE}" "${PREFIX}" 2> /dev/null +} + +start_radvd() { + ADDR=$(echo "${PREFIX}" | sed -e 's/::\//::1\//') + ip a a "${ADDR}" dev "${INTERFACE}" + sysctl net.ipv6.conf."${INTERFACE}".accept_ra=2 + sysctl net.ipv6.conf."${INTERFACE}".accept_ra_rt_info_max_plen=64 + ${RADVD} -c "${INTERFACE}" "${PREFIX}" +} + +if [ "$1" = "-d" ] || [ "$1" = "--use-dhcpv6" ]; then + USE_DHCPV6=1 + shift 1 +else + USE_DHCPV6=0 +fi + +if [ "$1" = "-r" ] || [ "$1" = "--use-radvd" ]; then + USE_RADVD=1 + shift 1 +else + USE_RADVD=0 +fi + +PREFIX=$1 +[ -z "${PREFIX}" ] && { + echo "usage: $0 [-d|--use-dhcpv6] [-r|--use-radvd ] []" + exit 1 +} + +if [ -n "$2" ]; then + PORT=$2 +fi + +trap "cleanup" INT QUIT TERM EXIT + +setup_interface + +if [ ${USE_DHCPV6} -eq 1 ]; then + DHCPD="$(readlink -f "${USB_CDC_ECM_DIR}/../dhcpv6-pd_ia/")/dhcpv6-pd_ia.py" + start_dhcpd +elif [ ${USE_RADVD} -eq 1 ]; then + RADVD="$(readlink -f "${USB_CDC_ECM_DIR}/../radvd/")/radvd.sh" + start_radvd +else + UHCPD="$(readlink -f "${USB_CDC_ECM_DIR}/../uhcpd/bin")/uhcpd" + start_uhcpd +fi + +if [ -z "${PORT}" ]; then + echo "Network enabled over CDC-ECM" + echo "Press Return to stop" + read -r +else + "${USB_CDC_ECM_DIR}/../pyterm/pyterm" -p "${PORT}" +fi diff --git a/platform/compose.common.yml b/platform/compose.common.yml index 11c35405..690fc206 100644 --- a/platform/compose.common.yml +++ b/platform/compose.common.yml @@ -13,13 +13,16 @@ x-common-healthcheck: &common-healthcheck services: proxy: image: nginx:1.25-alpine - ports: - - "127.0.0.1:${PROXY_PUBLIC_PORT}:80" - - "[::]:${PROXY_PUBLIC_PORT}:80" + #ports: + #- "127.0.0.1:${PROXY_PUBLIC_PORT}:80" + #- "[::1]:${PROXY_PUBLIC_PORT}:80" + #- "127.0.0.1:443:443" + #- "[::1]:443:443" volumes: - "./proxy/proxy_params:/etc/nginx/proxy_params:ro" - "./proxy/nginx.conf:/etc/nginx/nginx.conf:ro" - "./proxy/templates/default.conf.template:/etc/nginx/templates/default.conf.template:ro" + - "./data/ssl/certs:/etc/nginx/ssl" environment: PROXY_PUBLIC_PORT: ${PROXY_PUBLIC_PORT} # NOTE: picky about trailing slash @@ -104,7 +107,7 @@ services: POSTGRES_BACKEND_DB: backend POSTGRES_BACKEND_PASSWORD: ${POSTGRES_BACKEND_PASSWORD} healthcheck: - test: [ "CMD", "pg_isready", "-q", "-d", "postgres", "-U", "postgres" ] + test: [ "CMD", "pg_isready", "-q", "-h", "postgres", "-d", "postgres", "-U", "postgres" ] <<: *common-healthcheck volumes: - postgres:/var/lib/postgresql/data diff --git a/platform/compose.raspi.yml b/platform/compose.raspi.yml index 4dcfafef..fd4095c0 100644 --- a/platform/compose.raspi.yml +++ b/platform/compose.raspi.yml @@ -4,10 +4,31 @@ # services: + proxy: + environment: + PROXY_PUBLIC_PORT: 80 + ports: + - "0.0.0.0:80:80" + - "0.0.0.0:443:443" + - "[::]:80:80" + - "[::]:443:443" + volumes: + - "./data/htdocs:/usr/share/nginx/html" + devdash: + build: + args: + VITE_AUTHORITY: https://teamagochi/kc/realms/teamagochi + VITE_CLIENT_ID: teamagochi-webapp teashan: ports: - - "[fd00:dead:beef::1]:5683:5683/udp" # [coap://] CoAP over UDP (with experimental OSCORE) - - "[fd00:dead:beef::1]:5683:5683/tcp" # [coap+tcp://] CoAP over TCP (experimental) - - "[fd00:dead:beef::1]:5684:5684/udp" # [coaps://] CoAP over DTLS - - "[fd00:dead:beef::1]:5684:5684/tcp" # [coaps+tcp://] CoAP over TLS (experimental) - - "[fd00:dead:beef::1]:5685:5685/udp" # [coap://] CoAP over UDP[::] + - "[fd00:dead:beef::1]:5683:5683/udp" # [coap://] + - "[fd00:dead:beef::1]:5683:5683/tcp" # [coap+tcp://] + - "[fd00:dead:beef::1]:5684:5684/udp" # [coaps://] + - "[fd00:dead:beef::1]:5684:5684/tcp" # [coaps+tcp://] + - "[fd00:dead:beef::1]:5685:5685/udp" # [coap://] + keycloak: + environment: + KC_HOSTNAME_URL: https://teamagochi/kc + KC_HOSTNAME_ADMIN_URL: https://teamagochi/kc + backend: + pull_policy: never diff --git a/platform/create-certs.sh b/platform/create-certs.sh new file mode 100755 index 00000000..0a56776a --- /dev/null +++ b/platform/create-certs.sh @@ -0,0 +1,6 @@ +#!/bin/bash +openssl genrsa -out ./data/ssl/certs/server.key 2048 +openssl req -new -out ./data/ssl/certs/server.csr -key ./data/ssl/certs/server.key -config ./data/ssl/openssl.cnf +openssl x509 -req -days 3650 -in ./data/ssl/certs/server.csr -signkey ./data/ssl/certs/server.key -out ./data/ssl/certs/server.crt -extensions v3_req -extfile ./data/ssl/openssl.cnf + +#openssl req -x509 -nodes -days 365 -newkey rsa:2048 -keyout ./data/nginx.key -out ./data/ssl/nginx.crt diff --git a/platform/data/htdocs/.gitignore b/platform/data/htdocs/.gitignore new file mode 100644 index 00000000..377ccd3f --- /dev/null +++ b/platform/data/htdocs/.gitignore @@ -0,0 +1,2 @@ +* +!.gitkeep diff --git a/platform/data/keycloak/import/teamagochi-realm.json b/platform/data/keycloak/import/teamagochi-realm.json index 69ba48cc..33a998d7 100644 --- a/platform/data/keycloak/import/teamagochi-realm.json +++ b/platform/data/keycloak/import/teamagochi-realm.json @@ -728,15 +728,15 @@ "clientId" : "teamagochi-webapp", "name" : "Teamagochi Web-Application", "description" : "", - "rootUrl" : "http://localhost:4000", + "rootUrl" : "https://teamagochi", "adminUrl" : "", "baseUrl" : "/", "surrogateAuthRequired" : false, "enabled" : true, "alwaysDisplayInConsole" : false, "clientAuthenticatorType" : "client-secret", - "redirectUris" : [ "http://localhost:3030/*", "http://localhost:5173/*", "http://localhost:4000/*" ], - "webOrigins" : [ "http://localhost:3030", "+", "http://localhost:4000" ], + "redirectUris" : [ "http://teamagochi:3030/*", "http://localhost:5173/*", "http://teamagochi/*", "https://teamagochi/*" ], + "webOrigins" : [ "http://teamagochi:3030", "+", "http://teamagochi", "https://teamagochi", "https://teamagochi:5713" ], "notBefore" : 0, "bearerOnly" : false, "consentRequired" : false, @@ -748,7 +748,7 @@ "frontchannelLogout" : true, "protocol" : "openid-connect", "attributes" : { - "post.logout.redirect.uris" : "/*##http://localhost:3030##http://localhost:5173/*##http://localhost:4000/*", + "post.logout.redirect.uris" : "/*##http://teamagochi:3030##http://localhost:5173/*##http://teamagochi/*##https://teamagochi/*", "oauth2.device.authorization.grant.enabled" : "false", "backchannel.logout.revoke.offline.tokens" : "false", "use.refresh.tokens" : "true", diff --git a/platform/data/ssl/certs/server.crt b/platform/data/ssl/certs/server.crt new file mode 100644 index 00000000..72d70d52 --- /dev/null +++ b/platform/data/ssl/certs/server.crt @@ -0,0 +1,22 @@ +-----BEGIN CERTIFICATE----- +MIIDqDCCApCgAwIBAgIUR0OdADZcrVfEHXFeElxOzUNy8REwDQYJKoZIhvcNAQEL +BQAwXzELMAkGA1UEBhMCREUxEDAOBgNVBAgMB0hhbWJ1cmcxEDAOBgNVBAcMB0hh +bWJ1cmcxFzAVBgNVBAoMDlRlYW1hZ29jaGlASEFXMRMwEQYDVQQDDAp0ZWFtYWdv +Y2hpMB4XDTI0MDcwMTA4NTAxNFoXDTM0MDYyOTA4NTAxNFowXzELMAkGA1UEBhMC +REUxEDAOBgNVBAgMB0hhbWJ1cmcxEDAOBgNVBAcMB0hhbWJ1cmcxFzAVBgNVBAoM +DlRlYW1hZ29jaGlASEFXMRMwEQYDVQQDDAp0ZWFtYWdvY2hpMIIBIjANBgkqhkiG +9w0BAQEFAAOCAQ8AMIIBCgKCAQEAtfEFZYnTIue1nhW4fhLkKDi8HON9yfrzqFPy +iDGQK04laAo/1VZoTvjtRJB0WwbH8jkG9pTz/07QCiLhQRoqHluJKr0vL8/qlNEB +i7Ct0mNgnnFjQjjKN06j9wi5lM9LuHzBHDdou+UwGT0bAWdyxED6HS4t31ncviKq +8P83TH+6u/3/RnEmQmHXRmNnfZwdZ3gsqJFvGUpTJlDS63THm1RcGI3WSBQt53fF +sg7F+MIEH7i1dzWKYJqROU1HiasgP6yZaEicMn6UbQWMjijdHxiZqTSWfRU6Z8CO +JB0ggigAXjdGzJRILkHPtjR7pSDqPRm49s3q6yfUZB43KhU81wIDAQABo1wwWjA5 +BgNVHREEMjAwggwqLnRlYW1hZ29jaGmCDnRlYW1hZ29jaGkuZGV2ghAqLnRlYW1h +Z29jaGkuZGV2MB0GA1UdDgQWBBT3+Qex8n7ofXOnKBWw9O9JNzt7zTANBgkqhkiG +9w0BAQsFAAOCAQEAJNZ6A5cHqd4grUL/OZCE42vLpCP5WS3z4mvFTdJWx7lXk5Zu +QnKHl2yPOb9AZL9FI4xLXTwUpNoSqAxv7cv4OJjdarHGAdNMDxDMkZgpbf70KxH7 +fHoleM0NATLHwW+U4hM8gqcszzlXVbJqxw98wEXwTeClnxPmNSG/7/Hihaxdrdf8 +L0nJN5ELRX6oLz9Lh8ZKIqiJ8bm0ZZcPcRC6wFXPr2MuNHIPnXaWSQPxaeE6wJ0D +SIEHxYUtXbF52/cNUeVVeP8z61sefJX12+xNUC+0ftf6brRwFq5kvdkHRwDD6mcg +BGSHIVaQinlzsdS+SjpGAykR9+j7JfRZKqiu0g== +-----END CERTIFICATE----- diff --git a/platform/data/ssl/certs/server.csr b/platform/data/ssl/certs/server.csr new file mode 100644 index 00000000..cfc28d9a --- /dev/null +++ b/platform/data/ssl/certs/server.csr @@ -0,0 +1,18 @@ +-----BEGIN CERTIFICATE REQUEST----- +MIIC8DCCAdgCAQAwXzELMAkGA1UEBhMCREUxEDAOBgNVBAgMB0hhbWJ1cmcxEDAO +BgNVBAcMB0hhbWJ1cmcxFzAVBgNVBAoMDlRlYW1hZ29jaGlASEFXMRMwEQYDVQQD +DAp0ZWFtYWdvY2hpMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAtfEF +ZYnTIue1nhW4fhLkKDi8HON9yfrzqFPyiDGQK04laAo/1VZoTvjtRJB0WwbH8jkG +9pTz/07QCiLhQRoqHluJKr0vL8/qlNEBi7Ct0mNgnnFjQjjKN06j9wi5lM9LuHzB +HDdou+UwGT0bAWdyxED6HS4t31ncviKq8P83TH+6u/3/RnEmQmHXRmNnfZwdZ3gs +qJFvGUpTJlDS63THm1RcGI3WSBQt53fFsg7F+MIEH7i1dzWKYJqROU1HiasgP6yZ +aEicMn6UbQWMjijdHxiZqTSWfRU6Z8COJB0ggigAXjdGzJRILkHPtjR7pSDqPRm4 +9s3q6yfUZB43KhU81wIDAQABoEwwSgYJKoZIhvcNAQkOMT0wOzA5BgNVHREEMjAw +ggwqLnRlYW1hZ29jaGmCDnRlYW1hZ29jaGkuZGV2ghAqLnRlYW1hZ29jaGkuZGV2 +MA0GCSqGSIb3DQEBCwUAA4IBAQANgs+qxKQ1zTcM+sEV90kYE1e1fyH1lAKuk9Bm +2/7YTLxGAryQSMfWsZgCKCT8FbGBGRTP6cC6O33Yk+7xYWDft23+sBJcZ350jeqC +pBo3ChigkP5vutl5uyyp/n0T7BvoNqQazgQovBA/iWybP1RLg+PdLB2u8F0x7aDq +otbI8odrlbkEAkDrADwvNdUI34HwXI+adNgLQdIQvDOKoablEAMUMxdmBnbKbfwt +38YOmJnHddn/1Oj9g9xh/3/SxcwY1OsxIVbEu97fk8KUJDVdjTnWmk+vhCv0idhp +ySF+rRQ6McyGLI6tzQvkPTOf9HyzVPABWjJx7AExWdijax4w +-----END CERTIFICATE REQUEST----- diff --git a/platform/data/ssl/certs/server.key b/platform/data/ssl/certs/server.key new file mode 100644 index 00000000..657fc9c7 --- /dev/null +++ b/platform/data/ssl/certs/server.key @@ -0,0 +1,28 @@ +-----BEGIN PRIVATE KEY----- +MIIEvgIBADANBgkqhkiG9w0BAQEFAASCBKgwggSkAgEAAoIBAQC18QVlidMi57We +Fbh+EuQoOLwc433J+vOoU/KIMZArTiVoCj/VVmhO+O1EkHRbBsfyOQb2lPP/TtAK +IuFBGioeW4kqvS8vz+qU0QGLsK3SY2CecWNCOMo3TqP3CLmUz0u4fMEcN2i75TAZ +PRsBZ3LEQPodLi3fWdy+Iqrw/zdMf7q7/f9GcSZCYddGY2d9nB1neCyokW8ZSlMm +UNLrdMebVFwYjdZIFC3nd8WyDsX4wgQfuLV3NYpgmpE5TUeJqyA/rJloSJwyfpRt +BYyOKN0fGJmpNJZ9FTpnwI4kHSCCKABeN0bMlEguQc+2NHulIOo9Gbj2zerrJ9Rk +HjcqFTzXAgMBAAECggEAIbWm0NHQ7zosIb6XgLBiWntsixOxNi+LXogzOv2n3wYr +ExQ3EIFicSNY3qe/DzB58kyTmDMv5AFHtWESCBxfamF+UA9eQ0cMCQeSeD3DbuQf +vit2/wmDmey1n2sb61DfEC75Ho+7lXiYyUxTiRaQIoGBdc+dCzFcn1PQBTfcGI2P +rHzDE+TxOIjELLH/AUmJgqcG4AQO8wxg1uEHLtsLDmGorqwzGN1ht63u7fpH7CYs +6r/1+ovhkhOCg+/sjOORQ5Vkyrju1es1AfLp95BRCGKlh459TbjrAb9DhmkXtO9n +Qsdok9QxTirNGKRDwHBV4pt7uN+giZzH2XZXQ7WwiQKBgQDoS9isarRU9YRcPUw3 +TMd7WGUAtXksbVHx3GCRIcBLtHTyvOlQAnncnhSZPFkBxDEMuWMDc/TivxNThtnX +1IJgBdLZL+d+1cFUmZ7+dR6WrVScN7fJVDGVJ3C2UKblwST89xQgikB2F305Me5P +FLr7Miw7U0Qhdvh0NMzKoyw9bwKBgQDIgch2W9kLlao5NxxxuKAJCOAuktIePPYj +FrBHhmHfSd+x4FzLZs8WIxj4SATPAFTF62m9jjPtw65WYuf5BmZiPCKAEqWJy4zr +DECjnQuR2puAgFcOJ3C9DOfoqDfaYhKkZi9cCWiCwbTYDegdWTDJjD4tzPV7Co16 ++eZ8fwwTGQKBgQC4h8/Wn2kjeGmt0G/kGhT5Me4CUyawGSOYawU3JUWZnf+s+E6q +5Vyi2dzpIZxfH9gLEJXMH2gwW7NhjeUtY74xw06Mg1Z1Vh6fMu2vm6Ax9/0Xn9da +koxUvSD3YzhBmV3lqe0OMGUmqmAqeDSkE4a5l1C1y956awTRY0Qv0NGK9wKBgGnn +GrFhK8+RmlokALMUF/sNpBMVW0O7YSoBVtRAZztfsnYqEfkg8So5GXwx1dBb1WrA +P5ZuIIFpxJA+J1YBilxCdNp+fs68I08WpqGEVlMQSufhYZnJPSOtSGQ7TVzcRgpt +KoCLO5csps8i8UFnFXyuxTy9r77wQO/+RD3ngMIpAoGBAMBmzNVEPj+9XY1Zd5k0 +80PEBjZ+E9qsXTuyNorKPHeELKTQ6Ms6VUIsa9sU0COFO/y0XgYrbqyy/09N8hVs +Fu/TtyiTiPtNCs43sBgjN8CRjsC+k+7HNA3AdCSGjx1APJC+woAeHngGnmgEplxj +zYRxxM0+UwjyS3MDig8LneVl +-----END PRIVATE KEY----- diff --git a/platform/data/ssl/openssl.cnf b/platform/data/ssl/openssl.cnf new file mode 100644 index 00000000..6d78b6dc --- /dev/null +++ b/platform/data/ssl/openssl.cnf @@ -0,0 +1,17 @@ +[req] +default_bits = 2048 +distinguished_name = req_distinguished_name +req_extensions = v3_req +prompt = no +[req_distinguished_name] +countryName = DE +stateOrProvinceName = Hamburg +localityName = Hamburg +organizationName = Teamagochi@HAW +commonName = teamagochi +[v3_req] +subjectAltName = @alt_names +[alt_names] +DNS.1 = *.teamagochi +DNS.2 = teamagochi.dev +DNS.3 = *.teamagochi.dev diff --git a/platform/devdash/Dockerfile b/platform/devdash/Dockerfile index 005f7e3e..8508f077 100644 --- a/platform/devdash/Dockerfile +++ b/platform/devdash/Dockerfile @@ -5,6 +5,12 @@ FROM node:20.12-bookworm as base # FROM base as builder +ARG VITE_AUTHORITY +ARG VITE_CLIENT_ID + +ENV VITE_AUTHORITY=$VITE_AUTHORITY +ENV VITE_CLIENT_ID=$VITE_CLIENT_ID + WORKDIR /build COPY package*.json ./ diff --git a/platform/devdash/src/main.tsx b/platform/devdash/src/main.tsx index 37045df9..c5212564 100644 --- a/platform/devdash/src/main.tsx +++ b/platform/devdash/src/main.tsx @@ -17,7 +17,7 @@ const theme = createTheme(themeOptions); // eslint-disable-next-line @typescript-eslint/no-non-null-assertion ReactDOM.createRoot(document.getElementById('root')!).render( - + diff --git a/platform/devdash/vite.config.ts b/platform/devdash/vite.config.ts index 22b338d8..eebc9f59 100644 --- a/platform/devdash/vite.config.ts +++ b/platform/devdash/vite.config.ts @@ -7,7 +7,7 @@ export default defineConfig(({ command, mode }) => { const commonConfig: UserConfigExport = { plugins: [react()], - base: '/' + base: '/dev' }; if (command === 'serve') { diff --git a/platform/proxy/templates/default.conf.template b/platform/proxy/templates/default.conf.template index 3c6913d2..b1c52d75 100644 --- a/platform/proxy/templates/default.conf.template +++ b/platform/proxy/templates/default.conf.template @@ -1,15 +1,33 @@ server { listen 80 default_server; listen [::]:80 default_server; + listen 443 default_server ssl; + listen [::]:443 default_server ssl; server_name _; + ssl_certificate /etc/nginx/ssl/server.crt; + ssl_certificate_key /etc/nginx/ssl/server.key; + include /etc/nginx/conf.d/*.include; # - # Development Dashboard + # Frontend # location / { + root /usr/share/nginx/html; + index index.html; + try_files $uri $uri/ /index.html; + } + + # + # Development Dashboard + # + location = /dev { + rewrite ^ $scheme://$http_host/dev/ permanent; + } + + location /dev/ { proxy_pass ${DEVDASH_WEB_URL}; include proxy_params; } diff --git a/platform/teashan/client/client-wrapper.sh b/platform/teashan/client/client-wrapper.sh index e4e327f3..c0f052e1 100755 --- a/platform/teashan/client/client-wrapper.sh +++ b/platform/teashan/client/client-wrapper.sh @@ -34,6 +34,7 @@ shift if [ "$USE_BOOTSTRAP" = true ]; then # With bootstrap server SERVER_URL=coap://localhost:5683 + #SERVER_URL=coap://[fd00:dead:beef::1]:5683 #SERVER_URL=coap://teamagochi:5683 { sleep 5; echo -e "create 32769\n create 32770\n delete 6\n delete 3303\n delete 3442\n"; } \ | java -jar ./leshan-client-demo.jar \ diff --git a/web_backend/build-and-push.sh b/web_backend/build-and-push.sh new file mode 100755 index 00000000..f933d3e2 --- /dev/null +++ b/web_backend/build-and-push.sh @@ -0,0 +1,3 @@ +#!/bin/bash +echo $TEAMAGOCHI_TOKEN | docker login ghcr.io -u ozfox --password-stdin +./mvnw install -Dquarkus.profile=prod,publish diff --git a/web_backend/src/main/java/haw/teamagochi/backend/device/dataaccess/model/DeviceEntity.java b/web_backend/src/main/java/haw/teamagochi/backend/device/dataaccess/model/DeviceEntity.java index 84ed6f3b..b8645dbd 100644 --- a/web_backend/src/main/java/haw/teamagochi/backend/device/dataaccess/model/DeviceEntity.java +++ b/web_backend/src/main/java/haw/teamagochi/backend/device/dataaccess/model/DeviceEntity.java @@ -1,33 +1,41 @@ package haw.teamagochi.backend.device.dataaccess.model; + import haw.teamagochi.backend.pet.dataaccess.model.PetEntity; import haw.teamagochi.backend.user.dataaccess.model.UserEntity; -import io.quarkus.hibernate.orm.panache.PanacheEntity; import jakarta.annotation.Nullable; -import jakarta.persistence.*; +import jakarta.persistence.Column; +import jakarta.persistence.Entity; +import jakarta.persistence.EnumType; +import jakarta.persistence.Enumerated; +import jakarta.persistence.GeneratedValue; +import jakarta.persistence.Id; +import jakarta.persistence.ManyToOne; +import jakarta.persistence.OneToOne; import jakarta.validation.constraints.Size; -import java.util.ArrayList; -import java.util.List; import java.util.Objects; import lombok.Getter; +import lombok.NoArgsConstructor; import lombok.NonNull; import lombok.RequiredArgsConstructor; import lombok.Setter; +/** + * Persist-able device representation. + */ @Getter @Setter @RequiredArgsConstructor +@NoArgsConstructor @Entity public class DeviceEntity { - public DeviceEntity() {} - @Id @GeneratedValue private long id; @Nullable @Column(unique = true) - String identifier; // for device endpoint. Would be encrypted with DTLS? + private String identifier; @NonNull @Size(max = 255) @@ -37,7 +45,6 @@ public DeviceEntity() {} @Enumerated(EnumType.STRING) // save enum as string private DeviceType deviceType; - @ManyToOne @Nullable private UserEntity owner; @@ -46,7 +53,6 @@ public DeviceEntity() {} @Nullable private PetEntity pet; - @Override public boolean equals(Object o) { if (this == o) { @@ -63,4 +69,16 @@ public boolean equals(Object o) { public int hashCode() { return Objects.hash(id, name, deviceType); } + + @Override + public String toString() { + return "DeviceEntity{" + + "id=" + id + + ", identifier='" + identifier + '\'' + + ", name='" + name + '\'' + + ", deviceType=" + deviceType + + ", owner=" + owner + + ", pet=" + pet + + '}'; + } } diff --git a/web_backend/src/main/java/haw/teamagochi/backend/device/dataaccess/model/DeviceType.java b/web_backend/src/main/java/haw/teamagochi/backend/device/dataaccess/model/DeviceType.java index 94a9d4c5..d3eac9d2 100644 --- a/web_backend/src/main/java/haw/teamagochi/backend/device/dataaccess/model/DeviceType.java +++ b/web_backend/src/main/java/haw/teamagochi/backend/device/dataaccess/model/DeviceType.java @@ -1,9 +1,9 @@ package haw.teamagochi.backend.device.dataaccess.model; +/** + * Hardware device types. + */ public enum DeviceType { FROG, UNDEFINED; - - DeviceType() { - } } diff --git a/web_backend/src/main/java/haw/teamagochi/backend/device/logic/UcChangePet.java b/web_backend/src/main/java/haw/teamagochi/backend/device/logic/UcChangePet.java deleted file mode 100644 index 18996ab0..00000000 --- a/web_backend/src/main/java/haw/teamagochi/backend/device/logic/UcChangePet.java +++ /dev/null @@ -1,15 +0,0 @@ -package haw.teamagochi.backend.device.logic; - -import haw.teamagochi.backend.device.dataaccess.model.DeviceEntity; - -public interface UcChangePet { - - /** - * - * @param deviceId id of the device - * @param petId id of pet to be put on the device - * @return the Device Entity - */ - DeviceEntity changePet(long deviceId, long petId); - -} diff --git a/web_backend/src/main/java/haw/teamagochi/backend/device/logic/UcChangePetImpl.java b/web_backend/src/main/java/haw/teamagochi/backend/device/logic/UcChangePetImpl.java deleted file mode 100644 index 4ba876f5..00000000 --- a/web_backend/src/main/java/haw/teamagochi/backend/device/logic/UcChangePetImpl.java +++ /dev/null @@ -1,35 +0,0 @@ -package haw.teamagochi.backend.device.logic; - -import haw.teamagochi.backend.device.dataaccess.model.DeviceEntity; -import haw.teamagochi.backend.device.dataaccess.repository.DeviceRepository; -import haw.teamagochi.backend.pet.dataaccess.model.PetEntity; -import haw.teamagochi.backend.pet.logic.UcFindPet; -import jakarta.enterprise.context.ApplicationScoped; -import jakarta.inject.Inject; -import jakarta.transaction.Transactional; -import jakarta.ws.rs.NotFoundException; - -@ApplicationScoped -public class UcChangePetImpl implements UcChangePet{ - - @Inject - DeviceRepository deviceRepository; - - @Inject - UcFindDevice ucFindDevice; - @Inject - UcFindPet findPet; - - @Override - @Transactional - public DeviceEntity changePet(long deviceId, long petId) { - DeviceEntity device = ucFindDevice.find(deviceId); - PetEntity pet = findPet.find(petId); - if(device == null || pet == null){ - return null; - } - device.setPet(pet); - //deviceRepository.persist(device); --> nicht da als @Transactional gekennzeichnet - return device; - } -} diff --git a/web_backend/src/main/java/haw/teamagochi/backend/device/logic/UcDeviceResourceOperations.java b/web_backend/src/main/java/haw/teamagochi/backend/device/logic/UcDeviceResourceOperations.java index 77afaae5..27f491ad 100644 --- a/web_backend/src/main/java/haw/teamagochi/backend/device/logic/UcDeviceResourceOperations.java +++ b/web_backend/src/main/java/haw/teamagochi/backend/device/logic/UcDeviceResourceOperations.java @@ -1,5 +1,7 @@ package haw.teamagochi.backend.device.logic; +import haw.teamagochi.backend.device.logic.clients.rest.DeviceStatus; + /** * Operations on a teamagochi device object (32770). * @@ -8,11 +10,21 @@ */ public interface UcDeviceResourceOperations { + /** + * Write a registration code to the device. + * + * @param endpoint to write to + * @param status which should be written + * @return true if successful, otherwise false + */ + boolean writeStatus(String endpoint, DeviceStatus status); + /** * Write a registration code to the device. * * @param endpoint to write to * @param registrationCode which should be written + * @return true if successful, otherwise false */ boolean writeRegistrationCode(String endpoint, String registrationCode); } diff --git a/web_backend/src/main/java/haw/teamagochi/backend/device/logic/UcDeviceResourceOperationsImpl.java b/web_backend/src/main/java/haw/teamagochi/backend/device/logic/UcDeviceResourceOperationsImpl.java index 0a859f6e..4eaadd5c 100644 --- a/web_backend/src/main/java/haw/teamagochi/backend/device/logic/UcDeviceResourceOperationsImpl.java +++ b/web_backend/src/main/java/haw/teamagochi/backend/device/logic/UcDeviceResourceOperationsImpl.java @@ -1,7 +1,8 @@ package haw.teamagochi.backend.device.logic; +import haw.teamagochi.backend.device.logic.clients.rest.DeviceStatus; import haw.teamagochi.backend.device.logic.clients.rest.LeshanClientRestclient; -import haw.teamagochi.backend.leshanclient.datatypes.rest.ResourceDto; +import haw.teamagochi.backend.leshanclient.datatypes.common.ResourceDto; import haw.teamagochi.backend.leshanclient.datatypes.rest.ResourceResponseDto; import jakarta.enterprise.context.ApplicationScoped; import org.eclipse.microprofile.rest.client.inject.RestClient; @@ -18,11 +19,23 @@ public class UcDeviceResourceOperationsImpl implements UcDeviceResourceOperation private static final int DEVICE_OBJECT_ID = 32770; - private static final int PET_OBJECT_ID = 32769; - @RestClient private LeshanClientRestclient restClient; + /** + * {@inheritDoc} + */ + public boolean writeStatus(String endpoint, DeviceStatus status) { + ResourceDto resourceDto = createStatusResourceDto(status); + + ResourceResponseDto response = restClient.writeClientResource( + endpoint, DEVICE_OBJECT_ID, 0, resourceDto.id, + DEFAULT_COAP_TIMEOUT, DEFAULT_COAP_FORMAT, resourceDto + ); + + return !response.failure; + } + /** * {@inheritDoc} */ @@ -37,6 +50,10 @@ public boolean writeRegistrationCode(String endpoint, String registrationCode) { return !response.failure; } + private ResourceDto createStatusResourceDto(DeviceStatus status) { + return createSingleStringResource(0, status.name().toUpperCase()); + } + private ResourceDto createRegistrationCodeResourceDto(String registrationCode) { return createSingleStringResource(1, registrationCode); } diff --git a/web_backend/src/main/java/haw/teamagochi/backend/device/logic/UcFindLeshanClient.java b/web_backend/src/main/java/haw/teamagochi/backend/device/logic/UcFindLeshanClient.java new file mode 100644 index 00000000..412cec5a --- /dev/null +++ b/web_backend/src/main/java/haw/teamagochi/backend/device/logic/UcFindLeshanClient.java @@ -0,0 +1,17 @@ +package haw.teamagochi.backend.device.logic; + +import haw.teamagochi.backend.leshanclient.datatypes.rest.ClientDto; +import java.util.List; + +/** + * Operations to find clients. + */ +public interface UcFindLeshanClient { + + /** + * Get registered clients. + * + * @return a list of currently registered clients. + */ + List getClients(); +} diff --git a/web_backend/src/main/java/haw/teamagochi/backend/device/logic/UcFindLeshanClientImpl.java b/web_backend/src/main/java/haw/teamagochi/backend/device/logic/UcFindLeshanClientImpl.java new file mode 100644 index 00000000..836c719c --- /dev/null +++ b/web_backend/src/main/java/haw/teamagochi/backend/device/logic/UcFindLeshanClientImpl.java @@ -0,0 +1,25 @@ +package haw.teamagochi.backend.device.logic; + +import haw.teamagochi.backend.device.logic.clients.rest.LeshanClientRestclient; +import haw.teamagochi.backend.leshanclient.datatypes.rest.ClientDto; +import jakarta.enterprise.context.ApplicationScoped; +import java.util.List; +import org.eclipse.microprofile.rest.client.inject.RestClient; + +/** + * Default implementation for {@link UcFindLeshanClient}. + */ +@ApplicationScoped +public class UcFindLeshanClientImpl implements UcFindLeshanClient { + + @RestClient + private LeshanClientRestclient restClient; + + /** + * {@inheritDoc} + */ + @Override + public List getClients() { + return restClient.getClients().stream().toList(); + } +} diff --git a/web_backend/src/main/java/haw/teamagochi/backend/device/logic/UcHandleLeshanEvents.java b/web_backend/src/main/java/haw/teamagochi/backend/device/logic/UcHandleLeshanEvents.java index bf7eb945..eb90f50c 100644 --- a/web_backend/src/main/java/haw/teamagochi/backend/device/logic/UcHandleLeshanEvents.java +++ b/web_backend/src/main/java/haw/teamagochi/backend/device/logic/UcHandleLeshanEvents.java @@ -2,6 +2,7 @@ import haw.teamagochi.backend.leshanclient.datatypes.events.AwakeDto; import haw.teamagochi.backend.leshanclient.datatypes.events.CoaplogDto; +import haw.teamagochi.backend.leshanclient.datatypes.events.NotificationDto; import haw.teamagochi.backend.leshanclient.datatypes.events.RegistrationDto; import haw.teamagochi.backend.leshanclient.datatypes.events.UpdatedDto; @@ -15,6 +16,13 @@ public interface UcHandleLeshanEvents { void handleUpdate(UpdatedDto dto); + /** + * Process notifications containing changes on observed values. + * + * @param dto as sent by Leshan + */ + void handleNotification(NotificationDto dto); + void handleSleeping(AwakeDto dto); void handleAwake(AwakeDto dto); diff --git a/web_backend/src/main/java/haw/teamagochi/backend/device/logic/UcHandleLeshanEventsImpl.java b/web_backend/src/main/java/haw/teamagochi/backend/device/logic/UcHandleLeshanEventsImpl.java index 167143de..261c6ce1 100644 --- a/web_backend/src/main/java/haw/teamagochi/backend/device/logic/UcHandleLeshanEventsImpl.java +++ b/web_backend/src/main/java/haw/teamagochi/backend/device/logic/UcHandleLeshanEventsImpl.java @@ -1,53 +1,124 @@ package haw.teamagochi.backend.device.logic; +import haw.teamagochi.backend.device.logic.devicemanager.DeviceManager; import haw.teamagochi.backend.device.logic.registrationmanager.RegistrationManager; +import haw.teamagochi.backend.leshanclient.datatypes.common.ResourceDto; import haw.teamagochi.backend.leshanclient.datatypes.events.AwakeDto; import haw.teamagochi.backend.leshanclient.datatypes.events.CoaplogDto; +import haw.teamagochi.backend.leshanclient.datatypes.events.NotificationDto; import haw.teamagochi.backend.leshanclient.datatypes.events.RegistrationDto; import haw.teamagochi.backend.leshanclient.datatypes.events.UpdatedDto; +import haw.teamagochi.backend.pet.logic.petmanager.InteractionRecord; +import haw.teamagochi.backend.pet.logic.petmanager.PetManager; import jakarta.enterprise.context.ApplicationScoped; import jakarta.inject.Inject; import org.jboss.logging.Logger; -/** Default implementation of {@link UcHandleLeshanEvents}. */ +/** + * Default implementation of {@link UcHandleLeshanEvents}. + */ @ApplicationScoped public class UcHandleLeshanEventsImpl implements UcHandleLeshanEvents { private static final Logger LOGGER = Logger.getLogger(UcHandleLeshanEventsImpl.class); - @Inject RegistrationManager registrationManager; + @Inject + RegistrationManager registrationManager; + + @Inject + DeviceManager deviceManager; + + @Inject + PetManager petManager; @Override public void handleRegistration(RegistrationDto dto) { - LOGGER.debug("Received registration event: " + dto.endpoint + " (" + dto.registrationId + ")"); + LOGGER.info("Received registration event: " + dto.endpoint + " (" + dto.registrationId + ")"); - registrationManager.addClient(dto.endpoint); + if (deviceManager.contains(dto.endpoint)) { + deviceManager.enableDevice(dto.endpoint); + } else { + registrationManager.addClient(dto.endpoint); + } } @Override public void handleDeregistration(RegistrationDto dto) { - LOGGER.debug( - "Received deregistration event: " + dto.endpoint + " (" + dto.registrationId + ")"); + LOGGER.info("Received deregistration event: " + dto.endpoint + " (" + dto.registrationId + ")"); + + if (deviceManager.contains(dto.endpoint)) { + deviceManager.disableDevice(dto.endpoint); + } } @Override public void handleUpdate(UpdatedDto dto) { - LOGGER.debug( - "Received update event: " - + dto.registration.endpoint - + "(" - + dto.update.registrationId - + ")"); + LOGGER.debug("Received update event: " + dto.registration.endpoint + + " (registrationId: " + dto.update.registrationId + ")"); + + if (deviceManager.contains(dto.registration.endpoint)) { + deviceManager.enableDevice(dto.registration.endpoint); + } else { + registrationManager.addClient(dto.registration.endpoint); + } + } + + /** + * {@inheritDoc} + */ + @Override + public void handleNotification(NotificationDto dto) { + LOGGER.info("Received notification event: " + dto.ep + " (kind: " + dto.kind + ")"); + + if (!deviceManager.contains(dto.ep)) { + LOGGER.warn("Received notification event for inactive device: " + dto.ep); + return; + } + + Long petId = deviceManager.getActivePetByClientEndpointName(dto.ep); + + if (petId == null) { + LOGGER.warn("Received notification event for inactive pet: " + dto.ep); + return; + } + + InteractionRecord interactionRecord = petManager.getCurrentInteraction(petId); + + if (interactionRecord == null || interactionRecord.isEvaluated()) { + petManager.addInteraction(petId, new InteractionRecord()); + interactionRecord = petManager.getCurrentInteraction(petId); + } + + ResourceDto resource = dto.val; + switch (resource.id) { + case 40 -> // feed + interactionRecord.setFeed(Integer.valueOf((String) resource.value)); + case 41 -> // medicate + interactionRecord.setMedicate(Integer.valueOf((String) resource.value)); + case 42 -> // play + interactionRecord.setPlay(Integer.valueOf((String) resource.value)); + case 43 -> // clean + interactionRecord.setClean(Integer.valueOf((String) resource.value)); + default -> { /* noop */ } + } } @Override public void handleSleeping(AwakeDto dto) { LOGGER.debug("Received sleeping event: " + dto.ep); + + if (deviceManager.contains(dto.ep)) { + deviceManager.disableDevice(dto.ep); + } } @Override public void handleAwake(AwakeDto dto) { LOGGER.debug("Received awake event: " + dto.ep); + + if (deviceManager.contains(dto.ep)) { + deviceManager.enableDevice(dto.ep); + } } @Override diff --git a/web_backend/src/main/java/haw/teamagochi/backend/device/logic/UcManageDevice.java b/web_backend/src/main/java/haw/teamagochi/backend/device/logic/UcManageDevice.java index aaf67340..7b89d30e 100644 --- a/web_backend/src/main/java/haw/teamagochi/backend/device/logic/UcManageDevice.java +++ b/web_backend/src/main/java/haw/teamagochi/backend/device/logic/UcManageDevice.java @@ -26,6 +26,14 @@ public interface UcManageDevice { */ DeviceEntity create(DeviceEntity entity); + /** + * Update a device. + * + * @param entity to update + * @return the updated entity + */ + DeviceEntity update(DeviceEntity entity); + /** * Delete a device. * @@ -43,9 +51,10 @@ public interface UcManageDevice { * Register a device. * * @param registrationCode displayed on the device - * @param deviceName for the created device + * @param name for the created device + * @param type for the created device * @param uuid of the owner * @return the created entity */ - public DeviceEntity registerDevice(String registrationCode, String deviceName, String uuid); + DeviceEntity registerDevice(String registrationCode, String name, String type, String uuid); } diff --git a/web_backend/src/main/java/haw/teamagochi/backend/device/logic/UcManageDeviceImpl.java b/web_backend/src/main/java/haw/teamagochi/backend/device/logic/UcManageDeviceImpl.java index 204d88d3..60ad2d92 100644 --- a/web_backend/src/main/java/haw/teamagochi/backend/device/logic/UcManageDeviceImpl.java +++ b/web_backend/src/main/java/haw/teamagochi/backend/device/logic/UcManageDeviceImpl.java @@ -3,7 +3,10 @@ import haw.teamagochi.backend.device.dataaccess.model.DeviceEntity; import haw.teamagochi.backend.device.dataaccess.model.DeviceType; import haw.teamagochi.backend.device.dataaccess.repository.DeviceRepository; +import haw.teamagochi.backend.device.logic.devicemanager.DeviceManager; import haw.teamagochi.backend.device.logic.registrationmanager.RegistrationManager; +import haw.teamagochi.backend.pet.dataaccess.model.PetEntity; +import haw.teamagochi.backend.pet.logic.UcFindPet; import haw.teamagochi.backend.user.dataaccess.model.UserEntity; import haw.teamagochi.backend.user.logic.UcFindUser; import haw.teamagochi.backend.user.logic.UcManageUser; @@ -26,9 +29,15 @@ public class UcManageDeviceImpl implements UcManageDevice { @Inject UcManageUser ucManageUser; + @Inject + UcFindPet ucFindPet; + @Inject RegistrationManager registrationManager; + @Inject + DeviceManager deviceManager; + /** * {@inheritDoc} */ @@ -36,8 +45,7 @@ public class UcManageDeviceImpl implements UcManageDevice { @Transactional public DeviceEntity create(String name, DeviceType deviceType) { DeviceEntity device = new DeviceEntity(name, deviceType); - deviceRepository.persist(device); - return device; + return create(device); } /** @@ -47,6 +55,29 @@ public DeviceEntity create(String name, DeviceType deviceType) { @Transactional public DeviceEntity create(DeviceEntity entity) { deviceRepository.persist(entity); + deviceManager.reloadDevice(entity.getIdentifier()); + return entity; + } + + /** + * {@inheritDoc} + */ + @Override + @Transactional + public DeviceEntity update(DeviceEntity entity) { + if (entity.getPet() != null) { + PetEntity petEntity = ucFindPet.find(entity.getPet().getId()); + if (petEntity == null) { + throw new RuntimeException("No pet exists for given id."); + } + if (!petEntity.getOwner().equals(entity.getOwner())) { + throw new RuntimeException("User is not owner of the pet."); + } + } + + deviceRepository.getEntityManager().merge(entity); + deviceManager.reloadDevice(entity.getIdentifier()); + return entity; } @@ -56,8 +87,14 @@ public DeviceEntity create(DeviceEntity entity) { @Override @Transactional public boolean deleteById(long deviceId) { - registrationManager.clearCache(); - return deviceRepository.deleteById(deviceId); + try { + DeviceEntity entity = deviceRepository.findById(deviceId); + deviceManager.remove(entity.getIdentifier()); + deviceRepository.delete(entity); + } catch (Exception e) { + return false; + } + return true; } /** @@ -66,31 +103,40 @@ public boolean deleteById(long deviceId) { @Override @Transactional public void deleteAll() { - registrationManager.clearCache(); + deviceManager.removeAll(); deviceRepository.deleteAll(); } /** * {@inheritDoc} - * TODO whe have no way to determine the device type here. Lets default to "FROG". */ @Override @Transactional - public DeviceEntity registerDevice(String registrationCode, String deviceName, String uuid) { - String endpoint = registrationManager.registerClient(registrationCode); + public DeviceEntity registerDevice(String registrationCode, String deviceName, String deviceType, String uuid) { + String endpoint = registrationManager.getClientByCode(registrationCode); if (endpoint == null) { return null; } UserEntity owner = ucFindUser.find(uuid); if (owner == null) { - owner = ucManageUser.create(uuid); // create userId in database + owner = ucManageUser.create(uuid); } - DeviceEntity device = new DeviceEntity(deviceName, DeviceType.FROG); + DeviceType type; + try { + type = DeviceType.valueOf(deviceType.toUpperCase()); + } catch (IllegalArgumentException e) { + type = DeviceType.FROG; + } + + DeviceEntity device = new DeviceEntity(deviceName, type); device.setOwner(owner); - device.setIdentifier(endpoint); // Jessica + device.setIdentifier(endpoint); + create(device); - return create(device); + registrationManager.updateClient(endpoint, device.getId()); + + return device; } } diff --git a/web_backend/src/main/java/haw/teamagochi/backend/device/logic/UcPetResourceOperations.java b/web_backend/src/main/java/haw/teamagochi/backend/device/logic/UcPetResourceOperations.java new file mode 100644 index 00000000..74d82a57 --- /dev/null +++ b/web_backend/src/main/java/haw/teamagochi/backend/device/logic/UcPetResourceOperations.java @@ -0,0 +1,27 @@ +package haw.teamagochi.backend.device.logic; + +import haw.teamagochi.backend.pet.dataaccess.model.PetEntity; + +/** + * Operations on a teamagochi pet object (32769). + * + *

See + * Object model specifications + */ +public interface UcPetResourceOperations { + + /** + * Write a pet to the device. + * + * @param entity is the pet to write + * @return true if successful, otherwise false + */ + boolean writePet(String endpoint, PetEntity entity); + + /** + * Observe interactions. + * + * @param endpoint name of the client device + */ + void observePetInteractions(String endpoint); +} diff --git a/web_backend/src/main/java/haw/teamagochi/backend/device/logic/UcPetResourceOperationsImpl.java b/web_backend/src/main/java/haw/teamagochi/backend/device/logic/UcPetResourceOperationsImpl.java new file mode 100644 index 00000000..f6369d7d --- /dev/null +++ b/web_backend/src/main/java/haw/teamagochi/backend/device/logic/UcPetResourceOperationsImpl.java @@ -0,0 +1,114 @@ +package haw.teamagochi.backend.device.logic; + +import haw.teamagochi.backend.device.logic.clients.rest.LeshanClientRestclient; +import haw.teamagochi.backend.leshanclient.datatypes.rest.ObjectInstanceDto; +import haw.teamagochi.backend.leshanclient.datatypes.common.ResourceDto; +import haw.teamagochi.backend.leshanclient.datatypes.rest.ResourceResponseDto; +import haw.teamagochi.backend.pet.dataaccess.model.PetEntity; +import jakarta.enterprise.context.ApplicationScoped; +import java.util.ArrayList; +import java.util.List; +import org.eclipse.microprofile.rest.client.inject.RestClient; + +/** + * Default implementation for {@link UcPetResourceOperations}. + */ +@ApplicationScoped +public class UcPetResourceOperationsImpl implements UcPetResourceOperations { + + private static final int DEFAULT_COAP_TIMEOUT = 30; // seconds + + private static final String DEFAULT_COAP_FORMAT = "TLV"; + + private static final int PET_OBJECT_ID = 32769; + + @RestClient + private LeshanClientRestclient restClient; + + @Override + public boolean writePet(String endpoint, PetEntity entity) { + List resources = new ArrayList<>(); + + resources.add(createSingleIntegerResource(0, entity.getId())); + resources.add(createSingleStringResource(1, entity.getName())); + resources.add(createSingleIntegerResource(2, 11206400)); + + resources.add(createSingleIntegerResource(3, entity.getHappiness())); + resources.add(createSingleIntegerResource(4, entity.getWellbeing())); + resources.add(createSingleIntegerResource(5, entity.getHealth())); + resources.add(createSingleIntegerResource(6, entity.getXp())); + resources.add(createSingleIntegerResource(7, entity.getHunger())); + resources.add(createSingleIntegerResource(8, entity.getCleanliness())); + resources.add(createSingleIntegerResource(9, entity.getFun())); + + resources.add(createSingleBooleanResource(20, false)); // Hungry + resources.add(createSingleBooleanResource(21, false)); // Ill + resources.add(createSingleBooleanResource(22, false)); // Bored + resources.add(createSingleBooleanResource(23, false)); // Dirty + + resources.add(createSingleIntegerResource(40, 0)); // feed + resources.add(createSingleIntegerResource(41, 0)); // medicate + resources.add(createSingleIntegerResource(42, 0)); // play + resources.add(createSingleIntegerResource(43, 0)); // clean + + ObjectInstanceDto instanceDto = new ObjectInstanceDto(); + instanceDto.kind = "instance"; + instanceDto.id = 0; + instanceDto.resources = resources; + + ResourceResponseDto response = restClient.writeClientObjectInstance( + endpoint, PET_OBJECT_ID, instanceDto.id, + DEFAULT_COAP_TIMEOUT, DEFAULT_COAP_FORMAT, true, instanceDto); + + return !response.failure; + } + + /** + * {@inheritDoc} + */ + @Override + public void observePetInteractions(String endpoint) { + restClient.observeClientResource(endpoint, PET_OBJECT_ID, 0, 40, + DEFAULT_COAP_TIMEOUT, DEFAULT_COAP_FORMAT); + restClient.observeClientResource(endpoint, PET_OBJECT_ID, 0, 41, + DEFAULT_COAP_TIMEOUT, DEFAULT_COAP_FORMAT); + restClient.observeClientResource(endpoint, PET_OBJECT_ID, 0, 42, + DEFAULT_COAP_TIMEOUT, DEFAULT_COAP_FORMAT); + restClient.observeClientResource(endpoint, PET_OBJECT_ID, 0, 43, + DEFAULT_COAP_TIMEOUT, DEFAULT_COAP_FORMAT); + } + + private ResourceDto createSingleStringResource(int id, String value) { + ResourceDto resourceDto = new ResourceDto(); + resourceDto.id = id; + resourceDto.kind = "singleResource"; + resourceDto.type = "string"; + resourceDto.value = value; + + return resourceDto; + } + + private ResourceDto createSingleIntegerResource(int id, Long value) { + return createSingleIntegerResource(id, value.intValue()); + } + + private ResourceDto createSingleIntegerResource(int id, Integer value) { + ResourceDto resourceDto = new ResourceDto(); + resourceDto.id = id; + resourceDto.kind = "singleResource"; + resourceDto.type = "integer"; + resourceDto.value = value; + + return resourceDto; + } + + private ResourceDto createSingleBooleanResource(int id, boolean value) { + ResourceDto resourceDto = new ResourceDto(); + resourceDto.id = id; + resourceDto.kind = "singleResource"; + resourceDto.type = "boolean"; + resourceDto.value = value; + + return resourceDto; + } +} diff --git a/web_backend/src/main/java/haw/teamagochi/backend/device/logic/clients/LeshanEventListener.java b/web_backend/src/main/java/haw/teamagochi/backend/device/logic/clients/LeshanEventListener.java index a55fbdf2..a562ca4b 100644 --- a/web_backend/src/main/java/haw/teamagochi/backend/device/logic/clients/LeshanEventListener.java +++ b/web_backend/src/main/java/haw/teamagochi/backend/device/logic/clients/LeshanEventListener.java @@ -6,6 +6,7 @@ import haw.teamagochi.backend.device.logic.clients.sse.LeshanEventClient; import haw.teamagochi.backend.leshanclient.datatypes.events.AwakeDto; import haw.teamagochi.backend.leshanclient.datatypes.events.CoaplogDto; +import haw.teamagochi.backend.leshanclient.datatypes.events.NotificationDto; import haw.teamagochi.backend.leshanclient.datatypes.events.RegistrationDto; import haw.teamagochi.backend.leshanclient.datatypes.events.UpdatedDto; import io.quarkus.arc.profile.UnlessBuildProfile; @@ -13,6 +14,7 @@ import io.quarkus.runtime.Startup; import io.smallrye.mutiny.infrastructure.Infrastructure; import jakarta.enterprise.context.ApplicationScoped; +import org.eclipse.microprofile.config.inject.ConfigProperty; import org.eclipse.microprofile.rest.client.inject.RestClient; import org.jboss.logging.Logger; import org.jboss.resteasy.reactive.client.SseEvent; @@ -33,6 +35,12 @@ public class LeshanEventListener { @RestClient private LeshanEventClient sseClient; + @ConfigProperty(name = "leshan.client-endpoint-name.prefix") + protected String clientEndpointNamePrefix; + + @ConfigProperty(name = "leshan.client-endpoint-name.filter-mode") + protected String clientEndpointNameFilterMode; + LeshanEventListener(UcHandleLeshanEvents ucHandleLeshanEvents) { this.ucHandleLeshanEvents = ucHandleLeshanEvents; this.objectMapper = new ObjectMapper(); @@ -44,9 +52,12 @@ void receiveRegistrationEvents() { .registration() .emitOn(Infrastructure.getDefaultWorkerPool()) .onFailure().invoke(failure -> LOGGER.error(failure.getMessage())) - .subscribe().with(sseEvent -> handleEventConsumer(sseEvent, (event) -> { + .subscribe().with( + sseEvent -> handleEventConsumer(sseEvent, (event) -> { RegistrationDto dto = objectMapper.readValue(event.data(), RegistrationDto.class); - ucHandleLeshanEvents.handleRegistration(dto); + if (isAcceptableClientEndpointName(dto.endpoint)) { + ucHandleLeshanEvents.handleRegistration(dto); + } } ), failure -> LOGGER.error(failure.getMessage())); } @@ -60,7 +71,9 @@ void receiveDeregistrationEvents() { .subscribe().with( sseEvent -> handleEventConsumer(sseEvent, (event) -> { RegistrationDto dto = objectMapper.readValue(event.data(), RegistrationDto.class); - ucHandleLeshanEvents.handleDeregistration(dto); + if (isAcceptableClientEndpointName(dto.endpoint)) { + ucHandleLeshanEvents.handleDeregistration(dto); + } } ), failure -> LOGGER.error(failure.getMessage())); } @@ -74,11 +87,29 @@ void receiveUpdatedEvents() { .subscribe().with( sseEvent -> handleEventConsumer(sseEvent, (event) -> { UpdatedDto dto = objectMapper.readValue(event.data(), UpdatedDto.class); - ucHandleLeshanEvents.handleUpdate(dto); + if (isAcceptableClientEndpointName(dto.registration.endpoint)) { + ucHandleLeshanEvents.handleUpdate(dto); + } } ), failure -> LOGGER.error(failure.getMessage())); } + @Startup + void receiveNotificationEvents() { + sseClient + .notification() + .emitOn(Infrastructure.getDefaultWorkerPool()) + .onFailure().invoke(failure -> LOGGER.error(failure.getMessage())) + .subscribe().with( + sseEvent -> handleEventConsumer(sseEvent, (event) -> { + NotificationDto dto = objectMapper.readValue(event.data(), NotificationDto.class); + if (isAcceptableClientEndpointName(dto.ep)) { + ucHandleLeshanEvents.handleNotification(dto); + } + } + ), failure -> LOGGER.error(failure.getMessage())); + } + @Startup void receiveSleepingEvents() { sseClient @@ -88,7 +119,9 @@ void receiveSleepingEvents() { .subscribe().with( sseEvent -> handleEventConsumer(sseEvent, (event) -> { AwakeDto dto = objectMapper.readValue(event.data(), AwakeDto.class); - ucHandleLeshanEvents.handleSleeping(dto); + if (isAcceptableClientEndpointName(dto.ep)) { + ucHandleLeshanEvents.handleSleeping(dto); + } } ), failure -> LOGGER.error(failure.getMessage())); } @@ -102,7 +135,9 @@ void receiveAwakeEvents() { .subscribe().with( sseEvent -> handleEventConsumer(sseEvent, (event) -> { AwakeDto dto = objectMapper.readValue(event.data(), AwakeDto.class); - ucHandleLeshanEvents.handleAwake(dto); + if (isAcceptableClientEndpointName(dto.ep)) { + ucHandleLeshanEvents.handleAwake(dto); + } } ), failure -> LOGGER.error(failure.getMessage())); } @@ -116,11 +151,31 @@ void receiveCoapLogEvents() { .subscribe().with( sseEvent -> handleEventConsumer(sseEvent, (event) -> { CoaplogDto dto = objectMapper.readValue(event.data(), CoaplogDto.class); - ucHandleLeshanEvents.handleCoapLog(dto); + if (isAcceptableClientEndpointName(dto.ep)) { + ucHandleLeshanEvents.handleCoapLog(dto); + } } ), failure -> LOGGER.error(failure.getMessage())); } + /** + * Check if the event is about a Teamagochi device. + * + *

Note that Section "7.4.1. Endpoint Client Name" of the LwM2M spec says: + * "[...] the Endpoint Client Name is unauthenticated and can be set to arbitrary + * value by a misconfigured or malicious client and hence MUST NOT be used alone for + * any decision making without prior matching the Endpoint Client Name against the + * identifier used with the security protocol protecting LwM2M communication". + * + * @param value to evaluate + */ + private boolean isAcceptableClientEndpointName(String value) { + if (clientEndpointNameFilterMode.equals("write")) { + return true; + } + return clientEndpointNamePrefix != null && value.startsWith(clientEndpointNamePrefix); + } + /** * Wrapper for {@link CheckedEventConsumer} functions for logging and error handling. * @@ -129,8 +184,8 @@ void receiveCoapLogEvents() { */ private void handleEventConsumer( SseEvent event, CheckedEventConsumer> onItem) { - Log.debug("Handle: " + event.name()); try { + Log.debug("Handle: " + event.name()); onItem.accept(event); } catch (JsonProcessingException exception) { Log.error("Error processing JSON: " + event.name(), exception); diff --git a/web_backend/src/main/java/haw/teamagochi/backend/device/logic/clients/rest/DeviceStatus.java b/web_backend/src/main/java/haw/teamagochi/backend/device/logic/clients/rest/DeviceStatus.java new file mode 100644 index 00000000..917b4fd6 --- /dev/null +++ b/web_backend/src/main/java/haw/teamagochi/backend/device/logic/clients/rest/DeviceStatus.java @@ -0,0 +1,7 @@ +package haw.teamagochi.backend.device.logic.clients.rest; + +public enum DeviceStatus { + REGISTER, + REGISTERED, + READY +} diff --git a/web_backend/src/main/java/haw/teamagochi/backend/device/logic/clients/rest/LeshanClientResource.java b/web_backend/src/main/java/haw/teamagochi/backend/device/logic/clients/rest/LeshanClientResource.java index de806442..1fca45ce 100644 --- a/web_backend/src/main/java/haw/teamagochi/backend/device/logic/clients/rest/LeshanClientResource.java +++ b/web_backend/src/main/java/haw/teamagochi/backend/device/logic/clients/rest/LeshanClientResource.java @@ -2,6 +2,7 @@ import haw.teamagochi.backend.leshanclient.datatypes.rest.*; import jakarta.ws.rs.PathParam; +import jakarta.ws.rs.QueryParam; import java.util.Set; import org.eclipse.microprofile.rest.client.inject.RestClient; @@ -32,6 +33,17 @@ public ObjectInstanceResponseDto getClientObjectInstance( return clientRestclient.getClientObjectInstance(endpoint, object, instance); } + public ResourceResponseDto observeClientResource( + @PathParam("endpoint") String endpoint, + @PathParam("object") Integer object, + @PathParam("instance") Integer instance, + @PathParam("resource") Integer resource, + @QueryParam("timeout") Integer timeout, + @QueryParam("format") String format) { + return clientRestclient.observeClientResource( + endpoint, object, instance, resource, timeout, format); + } + public ResourceResponseDto getClientResource( @PathParam("endpoint") String endpoint, @PathParam("object") Integer object, diff --git a/web_backend/src/main/java/haw/teamagochi/backend/device/logic/clients/rest/LeshanClientRestclient.java b/web_backend/src/main/java/haw/teamagochi/backend/device/logic/clients/rest/LeshanClientRestclient.java index 22e710f5..3c230f2f 100644 --- a/web_backend/src/main/java/haw/teamagochi/backend/device/logic/clients/rest/LeshanClientRestclient.java +++ b/web_backend/src/main/java/haw/teamagochi/backend/device/logic/clients/rest/LeshanClientRestclient.java @@ -1,5 +1,6 @@ package haw.teamagochi.backend.device.logic.clients.rest; +import haw.teamagochi.backend.leshanclient.datatypes.common.ResourceDto; import haw.teamagochi.backend.leshanclient.datatypes.rest.*; import jakarta.inject.Singleton; import jakarta.ws.rs.GET; @@ -9,7 +10,6 @@ import jakarta.ws.rs.PathParam; import jakarta.ws.rs.QueryParam; import java.util.Set; -import org.eclipse.microprofile.openapi.annotations.parameters.RequestBody; import org.eclipse.microprofile.rest.client.inject.RegisterRestClient; /** @@ -68,6 +68,46 @@ ResourceResponseDto writeClientResource( @QueryParam("format") String format, ResourceDto resourceDto); + /** + * Observe a Teamagochi Device Object Instance. + * + *

Example: + *

+   *   http//example.com/clients/my-endpoint/32769/0/41/observe?timeout=5&format=TLV
+   *   {"status":"CONTENT(205)","valid":true,"success":true,"failure":false,"content":{
+   *     "kind":"singleResource","id":41,"type":"INTEGER","value":"0"}
+   *   }
+   * 
+ */ + @POST + @Path("/clients/{endpoint}/{object}/{instance}/{resource}/observe") + ResourceResponseDto observeClientResource( + @PathParam("endpoint") String endpoint, + @PathParam("object") Integer object, + @PathParam("instance") Integer instance, + @PathParam("resource") Integer resource, + @QueryParam("timeout") Integer timeout, + @QueryParam("format") String format); + + /** + * Write to a Teamagochi Device Object Instance. + * + *

Example: + *

+   *   http//example.com/clients/my-endpoint/3201/0?timeout=300&format=TLV
+   * 
+ */ + @PUT + @Path("/clients/{endpoint}/{object}/{instance}") + ResourceResponseDto writeClientObjectInstance( + @PathParam("endpoint") String endpoint, + @PathParam("object") Integer object, + @PathParam("instance") Integer instance, + @QueryParam("timeout") Integer timeout, + @QueryParam("format") String format, + @QueryParam("replace") boolean replace, + ObjectInstanceDto objectInstanceDto); + @GET @Path("/objectspecs/{endpoint}") Set getClientObjectSpecifications(@PathParam("endpoint") String endpoint); diff --git a/web_backend/src/main/java/haw/teamagochi/backend/device/logic/clients/sse/EventType.java b/web_backend/src/main/java/haw/teamagochi/backend/device/logic/clients/sse/EventType.java index f446a168..043e15fe 100644 --- a/web_backend/src/main/java/haw/teamagochi/backend/device/logic/clients/sse/EventType.java +++ b/web_backend/src/main/java/haw/teamagochi/backend/device/logic/clients/sse/EventType.java @@ -9,6 +9,7 @@ public enum EventType { Registration, Deregistration, Updated, + Notification, Sleeping, Awake, Coaplog; diff --git a/web_backend/src/main/java/haw/teamagochi/backend/device/logic/clients/sse/LeshanEventClient.java b/web_backend/src/main/java/haw/teamagochi/backend/device/logic/clients/sse/LeshanEventClient.java index e67deb6a..aca10381 100644 --- a/web_backend/src/main/java/haw/teamagochi/backend/device/logic/clients/sse/LeshanEventClient.java +++ b/web_backend/src/main/java/haw/teamagochi/backend/device/logic/clients/sse/LeshanEventClient.java @@ -3,6 +3,7 @@ import haw.teamagochi.backend.device.logic.clients.sse.filter.AwakeEventFilter; import haw.teamagochi.backend.device.logic.clients.sse.filter.CoaplogEventFilter; import haw.teamagochi.backend.device.logic.clients.sse.filter.DeregistrationEventFilter; +import haw.teamagochi.backend.device.logic.clients.sse.filter.NotificationEventFilter; import haw.teamagochi.backend.device.logic.clients.sse.filter.RegistrationEventFilter; import haw.teamagochi.backend.device.logic.clients.sse.filter.SleepingEventFilter; import haw.teamagochi.backend.device.logic.clients.sse.filter.UpdatedEventFilter; @@ -42,6 +43,12 @@ public interface LeshanEventClient { @SseEventFilter(UpdatedEventFilter.class) Multi> updated(); + @GET + @Path("/") + @Produces(MediaType.SERVER_SENT_EVENTS) + @SseEventFilter(NotificationEventFilter.class) + Multi> notification(); + @GET @Path("/") @Produces(MediaType.SERVER_SENT_EVENTS) diff --git a/web_backend/src/main/java/haw/teamagochi/backend/device/logic/clients/sse/filter/NotificationEventFilter.java b/web_backend/src/main/java/haw/teamagochi/backend/device/logic/clients/sse/filter/NotificationEventFilter.java new file mode 100644 index 00000000..fe672af7 --- /dev/null +++ b/web_backend/src/main/java/haw/teamagochi/backend/device/logic/clients/sse/filter/NotificationEventFilter.java @@ -0,0 +1,14 @@ +package haw.teamagochi.backend.device.logic.clients.sse.filter; + +import haw.teamagochi.backend.device.logic.clients.sse.EventType; +import org.jboss.resteasy.reactive.client.SseEvent; + +/** + * Filter Awake events. + */ +public class NotificationEventFilter extends AbstractEventFilter { + @Override + public boolean test(SseEvent event) { + return matchesName(event, EventType.Notification); + } +} \ No newline at end of file diff --git a/web_backend/src/main/java/haw/teamagochi/backend/device/logic/devicemanager/DeviceManager.java b/web_backend/src/main/java/haw/teamagochi/backend/device/logic/devicemanager/DeviceManager.java new file mode 100644 index 00000000..47b5aee0 --- /dev/null +++ b/web_backend/src/main/java/haw/teamagochi/backend/device/logic/devicemanager/DeviceManager.java @@ -0,0 +1,77 @@ +package haw.teamagochi.backend.device.logic.devicemanager; + +import java.util.List; + +/** + * Defines a device manager. + * + *

Caches devices which are currently active and registered with the LwM2m Server. + */ +public interface DeviceManager { + + /** Initialize. */ + void init(); + + /** + * Check if a device is known to the manager. + * + * @param endpoint name of the client device + */ + boolean contains(String endpoint); + + /** + * Add a device. + * + * @param endpoint name of the client device + * @param deviceId for the device entity + */ + void add(String endpoint, Long deviceId); + + /** + * Remove a device. + * + * @param endpoint name of the client device + */ + void remove(String endpoint); + + /** + * Remove all devices. + */ + void removeAll(); + + /** + * Disable a device. + * + * @param endpoint name of the client device + */ + void disableDevice(String endpoint); + + /** + * Enable a device. + * + * @param endpoint name of the client device + */ + void enableDevice(String endpoint); + + /** + * Reload a device, more precisely disable and enable immediately. + * + * @param endpoint name of the client device + */ + void reloadDevice(String endpoint); + + /** + * Get all active devices. + * + * @return all active devices + */ + List getActiveDevices(); + + /** + * Get pet of a client device. + * + * @param endpoint name of the client device + * @return the id of the pet, or null if not pet is active on the device + */ + Long getActivePetByClientEndpointName(String endpoint); +} diff --git a/web_backend/src/main/java/haw/teamagochi/backend/device/logic/devicemanager/MemoryDeviceManager.java b/web_backend/src/main/java/haw/teamagochi/backend/device/logic/devicemanager/MemoryDeviceManager.java new file mode 100644 index 00000000..aa901551 --- /dev/null +++ b/web_backend/src/main/java/haw/teamagochi/backend/device/logic/devicemanager/MemoryDeviceManager.java @@ -0,0 +1,208 @@ +package haw.teamagochi.backend.device.logic.devicemanager; + +import haw.teamagochi.backend.device.dataaccess.model.DeviceEntity; +import haw.teamagochi.backend.device.logic.UcDeviceResourceOperations; +import haw.teamagochi.backend.device.logic.UcFindDevice; +import haw.teamagochi.backend.device.logic.UcFindLeshanClient; +import haw.teamagochi.backend.device.logic.UcPetResourceOperations; +import haw.teamagochi.backend.device.logic.clients.rest.DeviceStatus; +import haw.teamagochi.backend.leshanclient.datatypes.rest.ClientDto; +import haw.teamagochi.backend.pet.logic.petmanager.PetManager; +import io.quarkus.runtime.Startup; +import jakarta.annotation.PostConstruct; +import jakarta.enterprise.context.ApplicationScoped; +import jakarta.inject.Inject; +import jakarta.transaction.Transactional; +import java.util.HashMap; +import java.util.HashSet; +import java.util.List; +import java.util.Map; +import java.util.Set; +import org.jboss.logging.Logger; + +/** + * In-memory implementation of a {@link DeviceManager}. + */ +@ApplicationScoped +@Startup +public class MemoryDeviceManager implements DeviceManager { + + private static final Logger LOGGER = Logger.getLogger(MemoryDeviceManager.class); + + private boolean initialized; + + @Inject + UcFindDevice ucFindDevice; + + @Inject + UcFindLeshanClient ucFindLeshanClient; + + @Inject + UcPetResourceOperations ucPetResourceOperations; + + @Inject + UcDeviceResourceOperations ucDeviceResourceOperations; + + @Inject + PetManager petManager; + + Set activeDevices; + + Map devices; + + Map pets; + + public MemoryDeviceManager() { + initialized = false; + activeDevices = new HashSet<>(); + devices = new HashMap<>(); + pets = new HashMap<>(); + } + + /** + * Initialize the manager. + */ + @PostConstruct + public void init() { + if (initialized) { + return; + } + + List clientDtos = ucFindLeshanClient.getClients(); + for (ClientDto client : clientDtos) { + DeviceEntity entity = ucFindDevice.findByIdentifier(client.endpoint); + if (entity != null) { + add(entity.getIdentifier(), entity.getId()); + } + } + + initialized = true; + + LOGGER.debug("Initialized MemoryDeviceManager instance."); + } + + /** + * {@inheritDoc} + */ + @Override + public boolean contains(String endpoint) { + return devices.containsKey(endpoint); + } + + /** + * {@inheritDoc} + * + *

A device is added as soon as the registration was successful. + */ + @Override + public void add(String endpoint, Long deviceId) { + devices.put(endpoint, deviceId); + enableDevice(endpoint); + } + + /** + * {@inheritDoc} + */ + @Override + public void remove(String endpoint) { + devices.remove(endpoint); + activeDevices.remove(endpoint); + pets.remove(endpoint); + } + + @Override + public void removeAll() { + devices.clear(); + activeDevices.clear(); + pets.clear(); + } + + /** + * {@inheritDoc} + */ + @Override + public void disableDevice(String endpoint) { + activeDevices.remove(endpoint); + } + + /** + * {@inheritDoc} + */ + @Override + @Transactional + public void enableDevice(String endpoint) { + if (activeDevices.contains(endpoint)) { + // Should be idempotent, may be called multiple times + return; + } + + DeviceEntity entity = ucFindDevice.findByIdentifier(endpoint); + + if (writePetToDevice(entity)) { + activeDevices.add(endpoint); + + assert entity.getPet() != null; + LOGGER.info("Enabled device " + endpoint + " with pet " + entity.getPet().getName()); + } + } + + /** + * {@inheritDoc} + */ + @Override + @Transactional + public void reloadDevice(String endpoint) { + activeDevices.remove(endpoint); + + enableDevice(endpoint); + } + + /** + * Write pet object of a given entity to the registered client device. + * + * @param entity the device which should be written + * @return true on success, otherwise false + */ + private boolean writePetToDevice(DeviceEntity entity) { + try { + if (entity != null && entity.getPet() != null) { + ucPetResourceOperations.writePet(entity.getIdentifier(), entity.getPet()); + + // TODO not always needed, right? + ucDeviceResourceOperations.writeStatus(entity.getIdentifier(), DeviceStatus.READY); + ucPetResourceOperations.observePetInteractions(entity.getIdentifier()); + + if (!petManager.contains(entity.getPet().getId())) { + petManager.add(entity.getPet()); + pets.put(entity.getIdentifier(), entity.getPet().getId()); + } + + return true; + } + } catch (Exception exception) { + LOGGER.error("Could not write pet to device", exception); + } + + return false; + } + + /** + * {@inheritDoc} + */ + @Override + public List getActiveDevices() { + return activeDevices.stream().map(endpoint -> devices.get(endpoint)).toList(); + } + + /** + * {@inheritDoc} + */ + @Override + public Long getActivePetByClientEndpointName(String endpoint) { + if (endpoint == null) { + throw new IllegalArgumentException("Endpoint must not be null"); + } + + return pets.get(endpoint); + } +} diff --git a/web_backend/src/main/java/haw/teamagochi/backend/device/logic/registrationmanager/MemoryRegistrationManager.java b/web_backend/src/main/java/haw/teamagochi/backend/device/logic/registrationmanager/MemoryRegistrationManager.java index cad5bca0..1d399b5b 100644 --- a/web_backend/src/main/java/haw/teamagochi/backend/device/logic/registrationmanager/MemoryRegistrationManager.java +++ b/web_backend/src/main/java/haw/teamagochi/backend/device/logic/registrationmanager/MemoryRegistrationManager.java @@ -1,7 +1,9 @@ package haw.teamagochi.backend.device.logic.registrationmanager; import haw.teamagochi.backend.device.logic.UcDeviceResourceOperations; -import haw.teamagochi.backend.device.logic.UcFindDevice; +import haw.teamagochi.backend.device.logic.clients.rest.DeviceStatus; +import haw.teamagochi.backend.device.logic.devicemanager.DeviceManager; +import io.quarkus.runtime.Startup; import io.quarkus.scheduler.Scheduled; import jakarta.annotation.PostConstruct; import jakarta.enterprise.context.ApplicationScoped; @@ -9,16 +11,17 @@ import jakarta.transaction.Transactional; import java.util.Date; import java.util.HashMap; -import java.util.HashSet; import java.util.List; import java.util.Map; import java.util.Random; import java.util.Set; import lombok.Setter; +import org.eclipse.microprofile.config.inject.ConfigProperty; import org.jboss.logging.Logger; /** In-memory implementation of a {@link RegistrationManager}. */ @ApplicationScoped +@Startup public class MemoryRegistrationManager implements RegistrationManager { private static final Logger LOGGER = Logger.getLogger(MemoryRegistrationManager.class); @@ -31,14 +34,17 @@ public class MemoryRegistrationManager implements RegistrationManager { /** Maps registration codes to endpoint client names. */ private final HashMap registrationCodeMap; - /** Blocklist: a cache for already registered devices. */ - private final HashSet blocklist; - @Inject - UcFindDevice ucFindDevice; + UcDeviceResourceOperations ucDeviceResourceOperations; @Inject - UcDeviceResourceOperations ucDeviceResourceOperations; + DeviceManager deviceManager; + + @ConfigProperty(name = "leshan.client-endpoint-name.prefix") + protected String clientEndpointNamePrefix; + + @ConfigProperty(name = "leshan.client-endpoint-name.filter-mode") + protected String clientEndpointNameFilterMode; @Setter private Integer registrationLifetime; @@ -46,11 +52,11 @@ public class MemoryRegistrationManager implements RegistrationManager { public MemoryRegistrationManager() { clientMap = new HashMap<>(); registrationCodeMap = new HashMap<>(); - blocklist = new HashSet<>(); } @PostConstruct void init() { + deviceManager.init(); LOGGER.debug("Initialized MemoryRegistrationManager instance."); } @@ -58,21 +64,29 @@ void init() { * {@inheritDoc} */ @Override - public String registerClient(String registrationCode) { - String endpoint = registrationCodeMap.get(registrationCode); + public String getClientByCode(String registrationCode) { + return registrationCodeMap.get(registrationCode); + } - if (endpoint != null) { - // TODO write new state to the device + /** + * {@inheritDoc} + */ + @Override + public boolean updateClient(String endpoint, Long deviceId) { + if (!clientMap.containsKey(endpoint)) { + throw new IllegalStateException("Client is not available for registration."); + } - registrationCodeMap.remove(registrationCode); - clientMap.remove(endpoint); - blocklist.add(endpoint); + boolean result = ucDeviceResourceOperations.writeStatus(endpoint, DeviceStatus.REGISTERED); - LOGGER.info( - "The client '" + endpoint + "' was registered with code '" + registrationCode + "'."); + if (result) { + removeClient(endpoint); + deviceManager.add(endpoint, deviceId); + + LOGGER.info("The client '" + endpoint + "' was registered."); } - return endpoint; + return result; } /** @@ -98,7 +112,9 @@ public void addClient(String endpoint) { */ try { String registrationCode = generateUniqueRegistrationCode(registrationCodeMap.keySet()); - writeRegistrationCodeToDevice(endpoint, registrationCode); + if (isAcceptableForWriting(endpoint)) { + writeRegistrationCodeToDevice(endpoint, registrationCode); + } registrationCodeMap.put(registrationCode, endpoint); LOGGER.info( @@ -128,10 +144,25 @@ public void addClient(String endpoint) { * @return true if it should be added, otherwise false. */ private boolean isClientAllowedForRegistration(String endpoint) { - if (blocklist.isEmpty()) { - ucFindDevice.findAll().forEach(device -> blocklist.add(device.getIdentifier())); + return !deviceManager.contains(endpoint); + } + + /** + * Check if we want to write to the device. + * + *

Note that Section "7.4.1. Endpoint Client Name" of the LwM2M spec says: + * "[...] the Endpoint Client Name is unauthenticated and can be set to arbitrary + * value by a misconfigured or malicious client and hence MUST NOT be used alone for + * any decision making without prior matching the Endpoint Client Name against the + * identifier used with the security protocol protecting LwM2M communication". + * + * @param endpointName of a device + */ + private boolean isAcceptableForWriting(String endpointName) { + if (clientEndpointNameFilterMode.equals("receive")) { + return true; } - return !blocklist.contains(endpoint); + return clientEndpointNamePrefix != null && endpointName.startsWith(clientEndpointNamePrefix); } /** @@ -211,9 +242,7 @@ public int size() { } @Override - public void clearCache() { - blocklist.clear(); - } + public void clearCache() {} /** * Find outdated clients. diff --git a/web_backend/src/main/java/haw/teamagochi/backend/device/logic/registrationmanager/RegistrationManager.java b/web_backend/src/main/java/haw/teamagochi/backend/device/logic/registrationmanager/RegistrationManager.java index 1caf2539..bdfd612a 100644 --- a/web_backend/src/main/java/haw/teamagochi/backend/device/logic/registrationmanager/RegistrationManager.java +++ b/web_backend/src/main/java/haw/teamagochi/backend/device/logic/registrationmanager/RegistrationManager.java @@ -9,12 +9,21 @@ public interface RegistrationManager { /** - * Register a client. + * Get client endpoint name for a given registration code. * - * @param registrationCode the key associated with the client - * @return the "Endpoint Client Name" of a LwM2M client + * @param registrationCode associated with the client + * @return the client endpoint name if found, otherwise null */ - String registerClient(String registrationCode); + String getClientByCode(String registrationCode); + + /** + * Update client after successful registration. + * + * @param endpoint is the "Endpoint Client Name" of a LwM2M client + * @param deviceId of the related device entity + * @return true if client was registered and new state was written to device, otherwise false + */ + boolean updateClient(String endpoint, Long deviceId); /** * Add a device. diff --git a/web_backend/src/main/java/haw/teamagochi/backend/device/service/rest/v1/DeviceRestSelfService.java b/web_backend/src/main/java/haw/teamagochi/backend/device/service/rest/v1/DeviceRestSelfService.java index c38e4266..1b8673e3 100644 --- a/web_backend/src/main/java/haw/teamagochi/backend/device/service/rest/v1/DeviceRestSelfService.java +++ b/web_backend/src/main/java/haw/teamagochi/backend/device/service/rest/v1/DeviceRestSelfService.java @@ -1,17 +1,21 @@ package haw.teamagochi.backend.device.service.rest.v1; import haw.teamagochi.backend.device.dataaccess.model.DeviceEntity; -import haw.teamagochi.backend.device.logic.UcChangePet; import haw.teamagochi.backend.device.logic.UcFindDevice; import haw.teamagochi.backend.device.logic.UcManageDevice; import haw.teamagochi.backend.device.service.rest.v1.mapper.DeviceMapper; import haw.teamagochi.backend.device.service.rest.v1.model.DeviceDTO; import haw.teamagochi.backend.general.security.SecurityUtil; +import haw.teamagochi.backend.pet.logic.UcFindPet; +import haw.teamagochi.backend.user.logic.UcFindUser; import io.quarkus.security.identity.SecurityIdentity; import jakarta.inject.Inject; +import jakarta.transaction.Transactional; +import jakarta.ws.rs.DELETE; import jakarta.ws.rs.GET; import jakarta.ws.rs.NotFoundException; import jakarta.ws.rs.POST; +import jakarta.ws.rs.PUT; import jakarta.ws.rs.Path; import jakarta.ws.rs.PathParam; import java.util.List; @@ -37,13 +41,17 @@ public class DeviceRestSelfService { @Inject protected DeviceMapper deviceMapper; + @Inject + protected UcFindUser ucFindUser; + + @Inject + protected UcFindPet ucFindPet; + @Inject protected UcFindDevice ucFindDevice; @Inject protected UcManageDevice ucManageDevice; - @Inject - UcChangePet ucChangePet; /** * Get all devices. @@ -82,6 +90,55 @@ public DeviceDTO getDeviceById(@PathParam("deviceId") long deviceId) { throw new NotFoundException(); } + /** + * Update a device. + * + * @param deviceId of the device to update + * @return the {@link DeviceDTO} if deleted + * @throws NotFoundException if no device was found + */ + @PUT + @Path("/{deviceId}") + @Operation(summary = "Update a device") + @APIResponse(responseCode = "200") + @APIResponse(responseCode = "404", description = "Not Found") + @Transactional + public DeviceDTO updateDevice(@PathParam("deviceId") long deviceId, DeviceDTO dto) { + String uuid = SecurityUtil.getExternalUserId(identity); + if (dto.getOwnerId() != null + && Objects.equals(dto.getOwnerId(), uuid) + && Objects.equals(dto.getId(), deviceId)) { + DeviceEntity entity = deviceMapper.mapTransferObjectToEntity( + dto, ucFindUser, ucFindPet, ucFindDevice); + ucManageDevice.update(entity); + return deviceMapper.mapEntityToTransferObject(entity); + } + throw new NotFoundException(); + } + + /** + * Delete a device by its id. + * + * @param deviceId of the device to delete + * @return the {@link DeviceDTO} if deleted + * @throws NotFoundException if no device was found + */ + @DELETE + @Path("/{deviceId}") + @Operation(summary = "Delete a device by its id") + @APIResponse(responseCode = "200") + @APIResponse(responseCode = "404", description = "Not Found") + public DeviceDTO deleteDeviceById(@PathParam("deviceId") long deviceId) { + String uuid = SecurityUtil.getExternalUserId(identity); + DeviceEntity entity = ucFindDevice.find(deviceId); + if (entity.getOwner() != null + && Objects.equals(uuid, entity.getOwner().getExternalID().toString()) + && ucManageDevice.deleteById(deviceId)) { + return deviceMapper.mapEntityToTransferObject(entity); + } + throw new NotFoundException(); + } + /** * Register a device using a registration code. * @@ -90,38 +147,18 @@ public DeviceDTO getDeviceById(@PathParam("deviceId") long deviceId) { * @throws NotFoundException if the registration code is not found */ @POST - @Path("/register/{registrationCode}/{deviceName}") + @Path("/register/{registrationCode}") @Operation(summary = "Register a device using a registration code") @APIResponse(responseCode = "200") @APIResponse(responseCode = "404", description = "Not Found") public DeviceDTO registerDevice( - @PathParam("registrationCode") String registrationCode, - @PathParam("deviceName") String deviceName) { + @PathParam("registrationCode") String registrationCode, DeviceDTO dto) { String uuid = SecurityUtil.getExternalUserId(identity); - DeviceEntity entity = ucManageDevice.registerDevice(registrationCode, deviceName, uuid); + DeviceEntity entity = + ucManageDevice.registerDevice(registrationCode, dto.getName(), dto.getType(), uuid); if (entity != null) { return deviceMapper.mapEntityToTransferObject(entity); } throw new NotFoundException(); } - - @POST - @Path("/changepet/{deviceID}/{PetID}") - @Operation(summary = "Set Pet for the Device") - @APIResponse(responseCode = "200") - @APIResponse(responseCode = "404", description = "Not Found") - public DeviceDTO changePet(@PathParam("deviceID") long deviceId, @PathParam("PetID") long petId){ - String uuid = SecurityUtil.getExternalUserId(identity); - DeviceEntity device = ucFindDevice.find(deviceId); - if(device != null - && device.getOwner() != null - && Objects.equals(device.getOwner().getExternalID().toString(), uuid)){//request authorised - device = ucChangePet.changePet(deviceId, petId); - if (device != null) { - return deviceMapper.mapEntityToTransferObject(device); - }//if - }//if - throw new NotFoundException(); - }//method - } diff --git a/web_backend/src/main/java/haw/teamagochi/backend/device/service/rest/v1/DeviceRestService.java b/web_backend/src/main/java/haw/teamagochi/backend/device/service/rest/v1/DeviceRestService.java index b0f8c3dd..a5aca59c 100644 --- a/web_backend/src/main/java/haw/teamagochi/backend/device/service/rest/v1/DeviceRestService.java +++ b/web_backend/src/main/java/haw/teamagochi/backend/device/service/rest/v1/DeviceRestService.java @@ -6,7 +6,7 @@ import haw.teamagochi.backend.device.service.rest.v1.mapper.DeviceMapper; import haw.teamagochi.backend.device.service.rest.v1.model.DeviceDTO; import haw.teamagochi.backend.pet.logic.UcManagePet; -import haw.teamagochi.backend.pet.logic.gameCycle.GameCycleImpl; +import haw.teamagochi.backend.pet.logic.game.GameCycle; import jakarta.inject.Inject; import jakarta.ws.rs.DELETE; import jakarta.ws.rs.GET; @@ -40,8 +40,7 @@ public class DeviceRestService { protected UcManagePet ucManagePet; @Inject - protected GameCycleImpl gameCycle; - + protected GameCycle gameCycle; /** * Get all devices. @@ -87,26 +86,27 @@ public DeviceDTO getDeviceById(@PathParam("deviceId") long deviceId) { @APIResponse(responseCode = "404", description = "Not Found") public DeviceDTO deleteDeviceById(@PathParam("deviceId") long deviceId) { DeviceEntity entity = ucFindDevice.find(deviceId); - boolean wasDeleted = ucManageDevice.deleteById(deviceId); - if (wasDeleted) { + if (ucManageDevice.deleteById(deviceId)) { return deviceMapper.mapEntityToTransferObject(entity); } throw new NotFoundException(); } + + /** + * Reset everything. + */ @DELETE @Path("/reset") - @Operation(summary = "delete all Devices and Pets") + @Operation(summary = "Delete all devices and pets") @APIResponse(responseCode = "200") @APIResponse(responseCode = "404", description = "Not Found") - public List deleteAllPet() { - List entities = ucFindDevice.findAll(); + public void reset() { gameCycle.setStopRequested(true); - while (!gameCycle.isStopped()){ - //waiting for stopped + while (!gameCycle.isStopped()) { + // waiting for stopped } ucManageDevice.deleteAll(); ucManagePet.deleteAll(); gameCycle.setStopRequested(false); - return deviceMapper.mapEntityToTransferObject(entities); } } diff --git a/web_backend/src/main/java/haw/teamagochi/backend/device/service/rest/v1/mapper/DeviceMapper.java b/web_backend/src/main/java/haw/teamagochi/backend/device/service/rest/v1/mapper/DeviceMapper.java index 2ae2e71c..d0d0df38 100644 --- a/web_backend/src/main/java/haw/teamagochi/backend/device/service/rest/v1/mapper/DeviceMapper.java +++ b/web_backend/src/main/java/haw/teamagochi/backend/device/service/rest/v1/mapper/DeviceMapper.java @@ -88,7 +88,10 @@ default DeviceEntity mapTransferObjectToEntity(DeviceDTO deviceDto, @Context UcF @AfterMapping default void findOwner(DeviceDTO dto, @MappingTarget DeviceEntity entity, @Context UcFindUser ucFindUser) { - if (dto.getOwnerId() == null || ucFindUser == null) return; + if (dto.getOwnerId() == null || ucFindUser == null) { + entity.setOwner(null); + return; + } UserEntity dbEntity = ucFindUser.find(dto.getOwnerId()); if (dbEntity != null) { @@ -98,7 +101,10 @@ default void findOwner(DeviceDTO dto, @MappingTarget DeviceEntity entity, @Conte @AfterMapping default void findPet(DeviceDTO dto, @MappingTarget DeviceEntity entity, @Context UcFindPet ucFindPet) { - if (dto.getPetId() == null || ucFindPet == null) return; + if (dto.getPetId() == null || ucFindPet == null) { + entity.setPet(null); + return; + } PetEntity dbEntity = ucFindPet.find(dto.getPetId()); if (dbEntity != null) { @@ -108,7 +114,10 @@ default void findPet(DeviceDTO dto, @MappingTarget DeviceEntity entity, @Context @AfterMapping default void findDeviceIdentifier(@MappingTarget DeviceEntity entity, @Context UcFindDevice ucFindDevice) { - if (ucFindDevice == null) return; + if (ucFindDevice == null) { + entity.setIdentifier(null); + return; + } DeviceEntity dbEntity = ucFindDevice.find(entity.getId()); if (dbEntity != null) { diff --git a/web_backend/src/main/java/haw/teamagochi/backend/pet/dataaccess/model/PetEntity.java b/web_backend/src/main/java/haw/teamagochi/backend/pet/dataaccess/model/PetEntity.java index 404affe5..0e4951b0 100644 --- a/web_backend/src/main/java/haw/teamagochi/backend/pet/dataaccess/model/PetEntity.java +++ b/web_backend/src/main/java/haw/teamagochi/backend/pet/dataaccess/model/PetEntity.java @@ -1,32 +1,31 @@ package haw.teamagochi.backend.pet.dataaccess.model; -import haw.teamagochi.backend.device.dataaccess.model.DeviceEntity; import haw.teamagochi.backend.user.dataaccess.model.UserEntity; -import io.quarkus.hibernate.orm.panache.PanacheEntity; import jakarta.annotation.Nullable; -import jakarta.persistence.CascadeType; import jakarta.persistence.Entity; import jakarta.persistence.GeneratedValue; import jakarta.persistence.Id; import jakarta.persistence.ManyToOne; -import jakarta.persistence.OneToMany; -import jakarta.persistence.OneToOne; import jakarta.validation.constraints.Past; -import jakarta.validation.constraints.Pattern; import jakarta.validation.constraints.PositiveOrZero; import jakarta.validation.constraints.Size; import java.util.Date; -import java.util.ArrayList; -import java.util.List; import java.util.Objects; import lombok.Getter; +import lombok.NoArgsConstructor; import lombok.NonNull; import lombok.RequiredArgsConstructor; import lombok.Setter; +import org.hibernate.annotations.Cascade; +import org.hibernate.annotations.CascadeType; +/** + * Persist-able pet representation. + */ @Getter @Setter @RequiredArgsConstructor +@NoArgsConstructor @Entity public class PetEntity { @@ -34,44 +33,30 @@ public class PetEntity { @GeneratedValue private long id; - public PetEntity() {} - @NonNull @ManyToOne private UserEntity owner; - @NonNull @Size(max = 255) private String name; - - /* - @NonNull - @Pattern(regexp = "^#([A-Fa-f0-9]{6}|[A-Fa-f0-9]{3})$") // source: https://www.geeksforgeeks.org/how-to-validate-hexadecimal-color-code-using-regular-expression/ - private String color; - */ - - private int happiness = 0; - private int wellbeing = 0; - private int health = 100; - private int hunger = 0; - private int cleanliness = 100; - private int fun = 0; - - @PositiveOrZero - private int xp = 0; - @Nullable @Past private Date lastTimeOnDevice; - @ManyToOne @NonNull + @Cascade({CascadeType.PERSIST, CascadeType.MERGE}) private PetTypeEntity petType; - + @PositiveOrZero private int happiness = 0; + @PositiveOrZero private int wellbeing = 0; + @PositiveOrZero private int health = 100; + @PositiveOrZero private int hunger = 0; + @PositiveOrZero private int cleanliness = 100; + @PositiveOrZero private int fun = 0; + @PositiveOrZero private int xp = 0; @Override public boolean equals(Object o) { @@ -90,7 +75,22 @@ public int hashCode() { return Objects.hash(id); } - - + @Override + public String toString() { + return "PetEntity{" + + "id=" + id + + ", owner=" + owner + + ", name='" + name + '\'' + + ", happiness=" + happiness + + ", wellbeing=" + wellbeing + + ", health=" + health + + ", hunger=" + hunger + + ", cleanliness=" + cleanliness + + ", fun=" + fun + + ", xp=" + xp + + ", lastTimeOnDevice=" + lastTimeOnDevice + + ", petType=" + petType + + '}'; + } } diff --git a/web_backend/src/main/java/haw/teamagochi/backend/pet/dataaccess/model/PetTypeEntity.java b/web_backend/src/main/java/haw/teamagochi/backend/pet/dataaccess/model/PetTypeEntity.java index c24700b3..41eb7c19 100644 --- a/web_backend/src/main/java/haw/teamagochi/backend/pet/dataaccess/model/PetTypeEntity.java +++ b/web_backend/src/main/java/haw/teamagochi/backend/pet/dataaccess/model/PetTypeEntity.java @@ -1,17 +1,22 @@ package haw.teamagochi.backend.pet.dataaccess.model; -import io.quarkus.hibernate.orm.panache.PanacheEntity; + import jakarta.persistence.Entity; import jakarta.persistence.GeneratedValue; import jakarta.persistence.Id; import jakarta.validation.constraints.Size; import lombok.Getter; +import lombok.NoArgsConstructor; import lombok.NonNull; import lombok.RequiredArgsConstructor; import lombok.Setter; +/** + * Persist-able pet type representation. + */ @Getter @Setter @RequiredArgsConstructor +@NoArgsConstructor @Entity public class PetTypeEntity { @@ -19,11 +24,15 @@ public class PetTypeEntity { @GeneratedValue private long id; - public PetTypeEntity() {} - @NonNull @Size(max = 255) private String name; - + @Override + public String toString() { + return "PetTypeEntity{" + + "id=" + id + + ", name='" + name + '\'' + + '}'; + } } diff --git a/web_backend/src/main/java/haw/teamagochi/backend/pet/logic/UcPetConditionsImpl.java b/web_backend/src/main/java/haw/teamagochi/backend/pet/logic/UcPetConditionsImpl.java index 9e647395..51c0bdf6 100644 --- a/web_backend/src/main/java/haw/teamagochi/backend/pet/logic/UcPetConditionsImpl.java +++ b/web_backend/src/main/java/haw/teamagochi/backend/pet/logic/UcPetConditionsImpl.java @@ -1,9 +1,7 @@ package haw.teamagochi.backend.pet.logic; import haw.teamagochi.backend.pet.dataaccess.model.PetEntity; -import haw.teamagochi.backend.pet.logic.Events.PetEvents; import jakarta.enterprise.context.ApplicationScoped; -import jakarta.inject.Inject; import static java.lang.Math.max; import static java.lang.Math.min; diff --git a/web_backend/src/main/java/haw/teamagochi/backend/pet/logic/UcPetInteractionsImpl.java b/web_backend/src/main/java/haw/teamagochi/backend/pet/logic/UcPetInteractionsImpl.java index 079420e8..ce58c280 100644 --- a/web_backend/src/main/java/haw/teamagochi/backend/pet/logic/UcPetInteractionsImpl.java +++ b/web_backend/src/main/java/haw/teamagochi/backend/pet/logic/UcPetInteractionsImpl.java @@ -1,7 +1,14 @@ package haw.teamagochi.backend.pet.logic; import haw.teamagochi.backend.pet.dataaccess.model.PetEntity; -import haw.teamagochi.backend.pet.logic.Events.*; +import haw.teamagochi.backend.pet.logic.game.events.CleanlinessVO; +import haw.teamagochi.backend.pet.logic.game.events.FunVO; +import haw.teamagochi.backend.pet.logic.game.events.HappinessVO; +import haw.teamagochi.backend.pet.logic.game.events.HealthVO; +import haw.teamagochi.backend.pet.logic.game.events.HungerVO; +import haw.teamagochi.backend.pet.logic.game.events.PetEvents; +import haw.teamagochi.backend.pet.logic.game.events.WellbeingVO; +import haw.teamagochi.backend.pet.logic.game.events.XpVO; import haw.teamagochi.backend.pet.service.rest.v1.mapper.PetMapper; import haw.teamagochi.backend.pet.service.rest.v1.model.PetStateDTO; import jakarta.enterprise.context.ApplicationScoped; diff --git a/web_backend/src/main/java/haw/teamagochi/backend/pet/logic/UcPetInteractionsOldVersion.java b/web_backend/src/main/java/haw/teamagochi/backend/pet/logic/UcPetInteractionsOldVersion.java deleted file mode 100644 index 2e2e7f04..00000000 --- a/web_backend/src/main/java/haw/teamagochi/backend/pet/logic/UcPetInteractionsOldVersion.java +++ /dev/null @@ -1,47 +0,0 @@ -package haw.teamagochi.backend.pet.logic; - -import haw.teamagochi.backend.pet.dataaccess.model.PetEntity; -import haw.teamagochi.backend.pet.logic.Events.PetEvents; -import haw.teamagochi.backend.pet.logic.UcPetConditionsImpl; -import jakarta.enterprise.context.ApplicationScoped; -import jakarta.inject.Inject; - -public class UcPetInteractionsOldVersion implements UcPetInteractions{ - - @Inject - UcPetConditionsImpl conditions; - @Inject - UcPetStatusImpl status; - - @Override - public void feedPet(PetEntity pet) { - conditions.decreaseHunger(pet); - status.increaseHappiness(pet, PetEvents.FEED); - status.increaseWellbeing(pet, PetEvents.FEED); - status.increaseXP(pet, PetEvents.FEED); - } - - @Override - public void cleanPet(PetEntity pet) { - conditions.increaseCleanliness(pet); - status.increaseHappiness(pet, PetEvents.CLEAN); - status.increaseWellbeing(pet, PetEvents.CLEAN); - status.increaseXP(pet, PetEvents.CLEAN); - } - - @Override - public void medicatePet(PetEntity pet) { - conditions.increaseHealth(pet); - status.increaseHappiness(pet, PetEvents.MEDICATE); - status.increaseWellbeing(pet, PetEvents.MEDICATE); - status.increaseXP(pet, PetEvents.MEDICATE); - } - - @Override - public void playWithPet(PetEntity pet) { - conditions.increaseFun(pet); - status.increaseHappiness(pet, PetEvents.PLAY); - status.increaseWellbeing(pet, PetEvents.PLAY); - status.increaseXP(pet, PetEvents.PLAY); - } -} diff --git a/web_backend/src/main/java/haw/teamagochi/backend/pet/logic/UcPetStatus.java b/web_backend/src/main/java/haw/teamagochi/backend/pet/logic/UcPetStatus.java index 204992cf..326e59cb 100644 --- a/web_backend/src/main/java/haw/teamagochi/backend/pet/logic/UcPetStatus.java +++ b/web_backend/src/main/java/haw/teamagochi/backend/pet/logic/UcPetStatus.java @@ -1,7 +1,7 @@ package haw.teamagochi.backend.pet.logic; import haw.teamagochi.backend.pet.dataaccess.model.PetEntity; -import haw.teamagochi.backend.pet.logic.Events.PetEvents; +import haw.teamagochi.backend.pet.logic.game.events.PetEvents; /** * Use Case for the Status-Values of a pet: diff --git a/web_backend/src/main/java/haw/teamagochi/backend/pet/logic/UcPetStatusImpl.java b/web_backend/src/main/java/haw/teamagochi/backend/pet/logic/UcPetStatusImpl.java index 997f9be1..ab0c965f 100644 --- a/web_backend/src/main/java/haw/teamagochi/backend/pet/logic/UcPetStatusImpl.java +++ b/web_backend/src/main/java/haw/teamagochi/backend/pet/logic/UcPetStatusImpl.java @@ -1,7 +1,7 @@ package haw.teamagochi.backend.pet.logic; import haw.teamagochi.backend.pet.dataaccess.model.PetEntity; -import haw.teamagochi.backend.pet.logic.Events.PetEvents; +import haw.teamagochi.backend.pet.logic.game.events.PetEvents; import jakarta.enterprise.context.ApplicationScoped; @ApplicationScoped diff --git a/web_backend/src/main/java/haw/teamagochi/backend/pet/logic/game/GameCycle.java b/web_backend/src/main/java/haw/teamagochi/backend/pet/logic/game/GameCycle.java new file mode 100644 index 00000000..d3de556e --- /dev/null +++ b/web_backend/src/main/java/haw/teamagochi/backend/pet/logic/game/GameCycle.java @@ -0,0 +1,22 @@ +package haw.teamagochi.backend.pet.logic.game; + +/** + * Game cycle, tick, clock, runner, whatever you name it. + */ +public interface GameCycle { + + /** + * Runs the Game Cycle and therefore updates the state-values of the pet's. + */ + void doWork(); + + /** + * Setter for stopRequest. + */ + void setStopRequested(boolean value); + + /** + * Check if it's working. + */ + boolean isStopped(); +} diff --git a/web_backend/src/main/java/haw/teamagochi/backend/pet/logic/game/GameCycleImpl.java b/web_backend/src/main/java/haw/teamagochi/backend/pet/logic/game/GameCycleImpl.java new file mode 100644 index 00000000..69db4d25 --- /dev/null +++ b/web_backend/src/main/java/haw/teamagochi/backend/pet/logic/game/GameCycleImpl.java @@ -0,0 +1,166 @@ +package haw.teamagochi.backend.pet.logic.game; + +import haw.teamagochi.backend.device.dataaccess.model.DeviceEntity; +import haw.teamagochi.backend.device.logic.UcFindDevice; +import haw.teamagochi.backend.device.logic.UcPetResourceOperations; +import haw.teamagochi.backend.device.logic.devicemanager.DeviceManager; +import haw.teamagochi.backend.pet.dataaccess.model.PetEntity; +import haw.teamagochi.backend.pet.logic.UcPetInteractions; +import haw.teamagochi.backend.pet.logic.game.events.CleanlinessVO; +import haw.teamagochi.backend.pet.logic.game.events.FunVO; +import haw.teamagochi.backend.pet.logic.game.events.HappinessVO; +import haw.teamagochi.backend.pet.logic.game.events.HealthVO; +import haw.teamagochi.backend.pet.logic.game.events.HungerVO; +import haw.teamagochi.backend.pet.logic.game.events.WellbeingVO; +import haw.teamagochi.backend.pet.logic.petmanager.InteractionRecord; +import haw.teamagochi.backend.pet.logic.petmanager.PetManager; +import haw.teamagochi.backend.pet.service.rest.v1.mapper.PetMapper; +import haw.teamagochi.backend.pet.service.rest.v1.model.PetStateDTO; +import io.quarkus.scheduler.Scheduled; +import jakarta.enterprise.context.ApplicationScoped; +import jakarta.inject.Inject; +import jakarta.transaction.Transactional; +import java.util.List; +import java.util.function.Consumer; +import lombok.Getter; +import lombok.Setter; +import org.jboss.logging.Logger; + +/** + * Default implementation for {@link GameCycle}. + */ +@ApplicationScoped +public class GameCycleImpl implements GameCycle { + + private static final Logger LOGGER = Logger.getLogger(GameCycleImpl.class); + + @Inject + UcFindDevice findDevice; + + @Inject + PetMapper petMapper; + + @Inject + HungerVO hungerVO; + + @Inject + HealthVO healthVO; + + @Inject + CleanlinessVO cleanlinessVO; + + @Inject + FunVO funVO; + + @Inject + WellbeingVO wellbeingVO; + + @Inject + HappinessVO happinessVO; + + @Inject + UcPetInteractions ucPetInteractions; + + @Inject + UcPetResourceOperations ucPetResourceOperations; + + @Inject + DeviceManager deviceManager; + + @Inject + PetManager petManager; + + @Setter + private volatile boolean stopRequested = false; + + @Getter + private volatile boolean stopped = false; + + @Override + @Scheduled(every = "{GameCycle.interval}") + @Transactional + public void doWork() { + if (stopRequested) { + // Exit the method if stop is requested + stopped = true; + return; + } + stopped = false; + + List activeDevices = deviceManager.getActiveDevices(); + + // Only pets currently on a device + for (DeviceEntity device : findDevice.findAll()) { + PetEntity pet = device.getPet(); + + if (pet != null) { + processPetInteractions(pet, device.getIdentifier()); + deteriorate(pet); + + if (activeDevices.contains(device.getId())) { + ucPetResourceOperations.writePet(device.getIdentifier(), pet); + + LOGGER.info("Write " + pet.getName() + " to device " + device.getIdentifier() + "."); + } + } + } + } + + private void processPetInteractions(PetEntity pet, String endpoint) { + InteractionRecord interactionRecord = petManager.getCurrentInteraction(pet.getId()); + + if (interactionRecord != null && !interactionRecord.isEvaluated()) { + + Consumer logInteractionFn = (action) -> + LOGGER.info( + "Interaction with " + pet.getName() + " on device " + endpoint + ": " + action); + + if (interactionRecord.getClean() > 0) { + ucPetInteractions.cleanPet(pet); + interactionRecord.setClean(0); + logInteractionFn.accept("clean"); + } + if (interactionRecord.getFeed() > 0) { + ucPetInteractions.feedPet(pet); + interactionRecord.setFeed(0); + logInteractionFn.accept("feed"); + } + if (interactionRecord.getPlay() > 0) { + ucPetInteractions.playWithPet(pet); + interactionRecord.setPlay(0); + logInteractionFn.accept("play"); + } + if (interactionRecord.getMedicate() > 0) { + ucPetInteractions.medicatePet(pet); + interactionRecord.setMedicate(0); + logInteractionFn.accept("medicate"); + } + + interactionRecord.setEvaluated(true); + } + } + + private void deteriorate(PetEntity pet) { + // Deteriorate base attributes + int newHunger = hungerVO.deteriorate(pet.getHunger()); + pet.setHunger(newHunger); + + int newHealth = healthVO.deteriorate(pet.getHealth()); + pet.setHealth(newHealth); + + int newCleanliness = cleanlinessVO.deteriorate(pet.getCleanliness()); + pet.setCleanliness(newCleanliness); + + int newFun = funVO.deteriorate(pet.getFun()); + pet.setFun(newFun); + + // Deteriorate attributes dependent on other attributes + PetStateDTO dto = petMapper.mapEntityToTransferObject(pet).getState(); + + int newWellbeing = wellbeingVO.deteriorate(dto); + pet.setWellbeing(newWellbeing); + + int newHappiness = happinessVO.deteriorate(dto); + pet.setHappiness(newHappiness); + } +} diff --git a/web_backend/src/main/java/haw/teamagochi/backend/pet/logic/Events/CleanlinessVO.java b/web_backend/src/main/java/haw/teamagochi/backend/pet/logic/game/events/CleanlinessVO.java similarity index 89% rename from web_backend/src/main/java/haw/teamagochi/backend/pet/logic/Events/CleanlinessVO.java rename to web_backend/src/main/java/haw/teamagochi/backend/pet/logic/game/events/CleanlinessVO.java index 81352c3e..1edf797d 100644 --- a/web_backend/src/main/java/haw/teamagochi/backend/pet/logic/Events/CleanlinessVO.java +++ b/web_backend/src/main/java/haw/teamagochi/backend/pet/logic/game/events/CleanlinessVO.java @@ -1,4 +1,4 @@ -package haw.teamagochi.backend.pet.logic.Events; +package haw.teamagochi.backend.pet.logic.game.events; import jakarta.enterprise.context.ApplicationScoped; diff --git a/web_backend/src/main/java/haw/teamagochi/backend/pet/logic/Events/Deterioratable.java b/web_backend/src/main/java/haw/teamagochi/backend/pet/logic/game/events/Deterioratable.java similarity index 59% rename from web_backend/src/main/java/haw/teamagochi/backend/pet/logic/Events/Deterioratable.java rename to web_backend/src/main/java/haw/teamagochi/backend/pet/logic/game/events/Deterioratable.java index 5d6eaf3c..58226e28 100644 --- a/web_backend/src/main/java/haw/teamagochi/backend/pet/logic/Events/Deterioratable.java +++ b/web_backend/src/main/java/haw/teamagochi/backend/pet/logic/game/events/Deterioratable.java @@ -1,4 +1,4 @@ -package haw.teamagochi.backend.pet.logic.Events; +package haw.teamagochi.backend.pet.logic.game.events; public interface Deterioratable { int deteriorate(int attributeValue); diff --git a/web_backend/src/main/java/haw/teamagochi/backend/pet/logic/Events/FunVO.java b/web_backend/src/main/java/haw/teamagochi/backend/pet/logic/game/events/FunVO.java similarity index 88% rename from web_backend/src/main/java/haw/teamagochi/backend/pet/logic/Events/FunVO.java rename to web_backend/src/main/java/haw/teamagochi/backend/pet/logic/game/events/FunVO.java index 4f608740..5c2e0afc 100644 --- a/web_backend/src/main/java/haw/teamagochi/backend/pet/logic/Events/FunVO.java +++ b/web_backend/src/main/java/haw/teamagochi/backend/pet/logic/game/events/FunVO.java @@ -1,4 +1,4 @@ -package haw.teamagochi.backend.pet.logic.Events; +package haw.teamagochi.backend.pet.logic.game.events; import jakarta.enterprise.context.ApplicationScoped; diff --git a/web_backend/src/main/java/haw/teamagochi/backend/pet/logic/Events/HappinessVO.java b/web_backend/src/main/java/haw/teamagochi/backend/pet/logic/game/events/HappinessVO.java similarity index 97% rename from web_backend/src/main/java/haw/teamagochi/backend/pet/logic/Events/HappinessVO.java rename to web_backend/src/main/java/haw/teamagochi/backend/pet/logic/game/events/HappinessVO.java index e35ae54a..8433682a 100644 --- a/web_backend/src/main/java/haw/teamagochi/backend/pet/logic/Events/HappinessVO.java +++ b/web_backend/src/main/java/haw/teamagochi/backend/pet/logic/game/events/HappinessVO.java @@ -1,4 +1,4 @@ -package haw.teamagochi.backend.pet.logic.Events; +package haw.teamagochi.backend.pet.logic.game.events; import haw.teamagochi.backend.pet.service.rest.v1.model.PetStateDTO; import jakarta.enterprise.context.ApplicationScoped; diff --git a/web_backend/src/main/java/haw/teamagochi/backend/pet/logic/Events/HealthVO.java b/web_backend/src/main/java/haw/teamagochi/backend/pet/logic/game/events/HealthVO.java similarity index 91% rename from web_backend/src/main/java/haw/teamagochi/backend/pet/logic/Events/HealthVO.java rename to web_backend/src/main/java/haw/teamagochi/backend/pet/logic/game/events/HealthVO.java index 7ae7602f..441bcb69 100644 --- a/web_backend/src/main/java/haw/teamagochi/backend/pet/logic/Events/HealthVO.java +++ b/web_backend/src/main/java/haw/teamagochi/backend/pet/logic/game/events/HealthVO.java @@ -1,4 +1,4 @@ -package haw.teamagochi.backend.pet.logic.Events; +package haw.teamagochi.backend.pet.logic.game.events; import jakarta.enterprise.context.ApplicationScoped; diff --git a/web_backend/src/main/java/haw/teamagochi/backend/pet/logic/Events/HungerVO.java b/web_backend/src/main/java/haw/teamagochi/backend/pet/logic/game/events/HungerVO.java similarity index 89% rename from web_backend/src/main/java/haw/teamagochi/backend/pet/logic/Events/HungerVO.java rename to web_backend/src/main/java/haw/teamagochi/backend/pet/logic/game/events/HungerVO.java index b8c33a6e..db788278 100644 --- a/web_backend/src/main/java/haw/teamagochi/backend/pet/logic/Events/HungerVO.java +++ b/web_backend/src/main/java/haw/teamagochi/backend/pet/logic/game/events/HungerVO.java @@ -1,4 +1,4 @@ -package haw.teamagochi.backend.pet.logic.Events; +package haw.teamagochi.backend.pet.logic.game.events; import jakarta.enterprise.context.ApplicationScoped; diff --git a/web_backend/src/main/java/haw/teamagochi/backend/pet/logic/Events/PetAttributeVO.java b/web_backend/src/main/java/haw/teamagochi/backend/pet/logic/game/events/PetAttributeVO.java similarity index 90% rename from web_backend/src/main/java/haw/teamagochi/backend/pet/logic/Events/PetAttributeVO.java rename to web_backend/src/main/java/haw/teamagochi/backend/pet/logic/game/events/PetAttributeVO.java index 7a73f15b..39179759 100644 --- a/web_backend/src/main/java/haw/teamagochi/backend/pet/logic/Events/PetAttributeVO.java +++ b/web_backend/src/main/java/haw/teamagochi/backend/pet/logic/game/events/PetAttributeVO.java @@ -1,4 +1,4 @@ -package haw.teamagochi.backend.pet.logic.Events; +package haw.teamagochi.backend.pet.logic.game.events; import static java.lang.Math.max; import static java.lang.Math.min; diff --git a/web_backend/src/main/java/haw/teamagochi/backend/pet/logic/Events/PetConditionVO.java b/web_backend/src/main/java/haw/teamagochi/backend/pet/logic/game/events/PetConditionVO.java similarity index 80% rename from web_backend/src/main/java/haw/teamagochi/backend/pet/logic/Events/PetConditionVO.java rename to web_backend/src/main/java/haw/teamagochi/backend/pet/logic/game/events/PetConditionVO.java index dac36692..a4261462 100644 --- a/web_backend/src/main/java/haw/teamagochi/backend/pet/logic/Events/PetConditionVO.java +++ b/web_backend/src/main/java/haw/teamagochi/backend/pet/logic/game/events/PetConditionVO.java @@ -1,4 +1,4 @@ -package haw.teamagochi.backend.pet.logic.Events; +package haw.teamagochi.backend.pet.logic.game.events; public abstract class PetConditionVO extends PetAttributeVO{ diff --git a/web_backend/src/main/java/haw/teamagochi/backend/pet/logic/Events/PetEvents.java b/web_backend/src/main/java/haw/teamagochi/backend/pet/logic/game/events/PetEvents.java similarity index 69% rename from web_backend/src/main/java/haw/teamagochi/backend/pet/logic/Events/PetEvents.java rename to web_backend/src/main/java/haw/teamagochi/backend/pet/logic/game/events/PetEvents.java index dfa2833f..fb3d0d2d 100644 --- a/web_backend/src/main/java/haw/teamagochi/backend/pet/logic/Events/PetEvents.java +++ b/web_backend/src/main/java/haw/teamagochi/backend/pet/logic/game/events/PetEvents.java @@ -1,4 +1,4 @@ -package haw.teamagochi.backend.pet.logic.Events; +package haw.teamagochi.backend.pet.logic.game.events; public enum PetEvents { FEED, diff --git a/web_backend/src/main/java/haw/teamagochi/backend/pet/logic/Events/PetStatusVO.java b/web_backend/src/main/java/haw/teamagochi/backend/pet/logic/game/events/PetStatusVO.java similarity index 83% rename from web_backend/src/main/java/haw/teamagochi/backend/pet/logic/Events/PetStatusVO.java rename to web_backend/src/main/java/haw/teamagochi/backend/pet/logic/game/events/PetStatusVO.java index 4bc6efe8..0c616b52 100644 --- a/web_backend/src/main/java/haw/teamagochi/backend/pet/logic/Events/PetStatusVO.java +++ b/web_backend/src/main/java/haw/teamagochi/backend/pet/logic/game/events/PetStatusVO.java @@ -1,4 +1,4 @@ -package haw.teamagochi.backend.pet.logic.Events; +package haw.teamagochi.backend.pet.logic.game.events; import haw.teamagochi.backend.pet.service.rest.v1.model.PetStateDTO; diff --git a/web_backend/src/main/java/haw/teamagochi/backend/pet/logic/Events/WellbeingVO.java b/web_backend/src/main/java/haw/teamagochi/backend/pet/logic/game/events/WellbeingVO.java similarity index 97% rename from web_backend/src/main/java/haw/teamagochi/backend/pet/logic/Events/WellbeingVO.java rename to web_backend/src/main/java/haw/teamagochi/backend/pet/logic/game/events/WellbeingVO.java index 210297c1..9b6cdd1a 100644 --- a/web_backend/src/main/java/haw/teamagochi/backend/pet/logic/Events/WellbeingVO.java +++ b/web_backend/src/main/java/haw/teamagochi/backend/pet/logic/game/events/WellbeingVO.java @@ -1,4 +1,4 @@ -package haw.teamagochi.backend.pet.logic.Events; +package haw.teamagochi.backend.pet.logic.game.events; import haw.teamagochi.backend.pet.service.rest.v1.model.PetStateDTO; import jakarta.enterprise.context.ApplicationScoped; diff --git a/web_backend/src/main/java/haw/teamagochi/backend/pet/logic/Events/XpVO.java b/web_backend/src/main/java/haw/teamagochi/backend/pet/logic/game/events/XpVO.java similarity index 90% rename from web_backend/src/main/java/haw/teamagochi/backend/pet/logic/Events/XpVO.java rename to web_backend/src/main/java/haw/teamagochi/backend/pet/logic/game/events/XpVO.java index d12c11f9..10f0879f 100644 --- a/web_backend/src/main/java/haw/teamagochi/backend/pet/logic/Events/XpVO.java +++ b/web_backend/src/main/java/haw/teamagochi/backend/pet/logic/game/events/XpVO.java @@ -1,6 +1,5 @@ -package haw.teamagochi.backend.pet.logic.Events; +package haw.teamagochi.backend.pet.logic.game.events; -import haw.teamagochi.backend.pet.service.rest.v1.model.PetDTO; import haw.teamagochi.backend.pet.service.rest.v1.model.PetStateDTO; import jakarta.enterprise.context.ApplicationScoped; import jakarta.inject.Inject; diff --git a/web_backend/src/main/java/haw/teamagochi/backend/pet/logic/gameCycle/GameCycle.java b/web_backend/src/main/java/haw/teamagochi/backend/pet/logic/gameCycle/GameCycle.java deleted file mode 100644 index 85e7aeb2..00000000 --- a/web_backend/src/main/java/haw/teamagochi/backend/pet/logic/gameCycle/GameCycle.java +++ /dev/null @@ -1,14 +0,0 @@ -package haw.teamagochi.backend.pet.logic.gameCycle; - -import haw.teamagochi.backend.pet.dataaccess.model.PetEntity; - -public interface GameCycle { - - - /** - * Runs the Game Cycle and therefore updates the state-values of the pet's - */ - void petGameCycle(); - - -} diff --git a/web_backend/src/main/java/haw/teamagochi/backend/pet/logic/gameCycle/GameCycleImpl.java b/web_backend/src/main/java/haw/teamagochi/backend/pet/logic/gameCycle/GameCycleImpl.java deleted file mode 100644 index 3c2063e5..00000000 --- a/web_backend/src/main/java/haw/teamagochi/backend/pet/logic/gameCycle/GameCycleImpl.java +++ /dev/null @@ -1,106 +0,0 @@ -package haw.teamagochi.backend.pet.logic.gameCycle; - -import haw.teamagochi.backend.device.dataaccess.model.DeviceEntity; -import haw.teamagochi.backend.device.logic.UcFindDevice; -import haw.teamagochi.backend.device.logic.UcFindDeviceImpl; -import haw.teamagochi.backend.pet.dataaccess.model.PetEntity; -import haw.teamagochi.backend.pet.dataaccess.repository.PetRepository; -import haw.teamagochi.backend.pet.logic.*; -import haw.teamagochi.backend.pet.logic.Events.*; -import haw.teamagochi.backend.pet.service.rest.v1.mapper.PetMapper; -import haw.teamagochi.backend.pet.service.rest.v1.model.PetStateDTO; -import io.quarkus.scheduler.Scheduled; -import jakarta.inject.Inject; -import java.util.Random; - -import jakarta.transaction.Transactional; -import lombok.Getter; -import lombok.Setter; -import org.eclipse.microprofile.config.inject.ConfigProperty; - - -public class GameCycleImpl implements GameCycle{ - - @Inject - UcFindDevice findDevice; - - @Inject - PetMapper petMapper; - - @Inject - HungerVO hungerVO; - - @Inject - HealthVO healthVO; - - @Inject - CleanlinessVO cleanlinessVO; - - @Inject - FunVO funVO; - - - @Inject - WellbeingVO wellbeingVO; - - @Inject - HappinessVO happinessVO; - - - @Setter - private volatile boolean stopRequested = false; - - @Getter - private volatile boolean stopped = false; - - - @Override - @Scheduled(every = "{GameCycle.interval}") - @Transactional - public void petGameCycle() { - if (stopRequested) { - // Exit the method if stop is requested - stopped = true; - }else { - stopped = false; - for(DeviceEntity device: findDevice.findAll()){//only pets currently on a device - - PetEntity pet = device.getPet(); - if(pet != null) { - deteriorate(pet); - } - } - - }//method - } - - @Transactional - // Needs to be public bc. Transactional. - public void deteriorate(PetEntity pet) { - // Deteriorate base attributes - int newHunger = hungerVO.deteriorate(pet.getHunger()); // saved in var, for debugging - pet.setHunger(newHunger); - - int newHealth = healthVO.deteriorate(pet.getHealth()); - pet.setHealth(newHealth); - - int newCleanliness = cleanlinessVO.deteriorate(pet.getCleanliness()); - pet.setCleanliness(newCleanliness); - - int newFun = funVO.deteriorate(pet.getFun()); - pet.setFun(newFun); - - // Deteriorate attributes dependent on other attributes - PetStateDTO dto = petMapper.mapEntityToTransferObject(pet).getState(); - - int newWellbeing = wellbeingVO.deteriorate(dto); - pet.setWellbeing(newWellbeing); - - int newHappiness = happinessVO.deteriorate(dto); - pet.setHappiness(newHappiness); - } - - - - -} diff --git a/web_backend/src/main/java/haw/teamagochi/backend/pet/logic/petmanager/ConditionRecord.java b/web_backend/src/main/java/haw/teamagochi/backend/pet/logic/petmanager/ConditionRecord.java new file mode 100644 index 00000000..bf5389df --- /dev/null +++ b/web_backend/src/main/java/haw/teamagochi/backend/pet/logic/petmanager/ConditionRecord.java @@ -0,0 +1,29 @@ +package haw.teamagochi.backend.pet.logic.petmanager; + +import lombok.AllArgsConstructor; +import lombok.Getter; +import lombok.Setter; +import lombok.ToString; + +/** + * A summary of pet conditions. + */ +@Getter +@Setter +@ToString +@AllArgsConstructor +public class ConditionRecord { + private boolean evaluated; + private Integer hungry; + private Integer ill; + private Integer bored; + private Integer dirty; + + public ConditionRecord() { + this.evaluated = false; + this.hungry = 0; + this.ill = 0; + this.bored = 0; + this.dirty = 0; + } +} diff --git a/web_backend/src/main/java/haw/teamagochi/backend/pet/logic/petmanager/InteractionRecord.java b/web_backend/src/main/java/haw/teamagochi/backend/pet/logic/petmanager/InteractionRecord.java new file mode 100644 index 00000000..acba099a --- /dev/null +++ b/web_backend/src/main/java/haw/teamagochi/backend/pet/logic/petmanager/InteractionRecord.java @@ -0,0 +1,29 @@ +package haw.teamagochi.backend.pet.logic.petmanager; + +import lombok.AllArgsConstructor; +import lombok.Getter; +import lombok.Setter; +import lombok.ToString; + +/** + * A summary of pet interactions. + */ +@Getter +@Setter +@ToString +@AllArgsConstructor +public class InteractionRecord { + private boolean evaluated; + private Integer feed; + private Integer medicate; + private Integer play; + private Integer clean; + + public InteractionRecord() { + this.evaluated = false; + this.feed = 0; + this.medicate = 0; + this.play = 0; + this.clean = 0; + } +} diff --git a/web_backend/src/main/java/haw/teamagochi/backend/pet/logic/petmanager/InteractionStack.java b/web_backend/src/main/java/haw/teamagochi/backend/pet/logic/petmanager/InteractionStack.java new file mode 100644 index 00000000..a5d97265 --- /dev/null +++ b/web_backend/src/main/java/haw/teamagochi/backend/pet/logic/petmanager/InteractionStack.java @@ -0,0 +1,47 @@ +package haw.teamagochi.backend.pet.logic.petmanager; + +import java.util.ArrayDeque; +import java.util.Deque; +import lombok.Getter; + +/** + * A sized stack for {@link InteractionRecord InteractionRecords}. + */ +public class InteractionStack { + + @Getter + private final int maxSize; + + private final Deque stack; + + public InteractionStack() { + this(20); + } + + public InteractionStack(int size) { + this.maxSize = size; + this.stack = new ArrayDeque<>(size); + } + + /** + * Pushes an item onto the top of this stack. + */ + public void push(InteractionRecord record) { + try { + if (!stack.offerFirst(record)) { + // TODO it could be checked if the removed element was ever evaluated + stack.removeLast(); + stack.push(record); + } + } catch (NullPointerException e) { + throw new IllegalArgumentException("Record must not be null"); + } + } + + /** + * Looks at the object at the top of this stack without removing it from the stack. + */ + public InteractionRecord peek() { + return this.stack.peek(); + } +} diff --git a/web_backend/src/main/java/haw/teamagochi/backend/pet/logic/petmanager/MemoryPetManager.java b/web_backend/src/main/java/haw/teamagochi/backend/pet/logic/petmanager/MemoryPetManager.java new file mode 100644 index 00000000..48036e49 --- /dev/null +++ b/web_backend/src/main/java/haw/teamagochi/backend/pet/logic/petmanager/MemoryPetManager.java @@ -0,0 +1,98 @@ +package haw.teamagochi.backend.pet.logic.petmanager; + +import haw.teamagochi.backend.pet.dataaccess.model.PetEntity; +import jakarta.enterprise.context.ApplicationScoped; +import java.util.Map; +import java.util.concurrent.ConcurrentHashMap; + +/** + * In-memory implementation of a {@link PetManager}. + */ +@ApplicationScoped +public class MemoryPetManager implements PetManager { + + //private static final Logger LOGGER = Logger.getLogger(MemoryPetManager.class); + + private static final int historyMaxLength = 10; + + Map petInteractionMap; + + public MemoryPetManager() { + petInteractionMap = new ConcurrentHashMap<>(); + } + + /** + * {@inheritDoc} + */ + @Override + public boolean contains(Long petId) { + return petInteractionMap.containsKey(petId); + } + + /** + * {@inheritDoc} + */ + @Override + public void add(PetEntity pet) { + if (pet == null) { + throw new IllegalArgumentException("Pet must not be null"); + } + if (contains(pet.getId())) { + return; + } + + petInteractionMap.putIfAbsent(pet.getId(), initializeInteractionStack()); + } + + /** + * Create new {@link InteractionStack} containing an empty element. + */ + private InteractionStack initializeInteractionStack() { + InteractionStack newStack = new InteractionStack(historyMaxLength); + newStack.push(new InteractionRecord()); + return newStack; + } + + /** + * {@inheritDoc} + */ + @Override + public void addInteraction(Long petId, InteractionRecord record) { + if (!contains(petId)) { + throw new IllegalStateException("Pet must be know known to the manager"); + } + + petInteractionMap.get(petId).push(record); + } + + /** + * {@inheritDoc} + */ + @Override + public InteractionRecord getLastInteraction(Long petId) { + if (petId == null) { + throw new IllegalArgumentException("Pet must not be null"); + } + if (!contains(petId)) { + throw new IllegalStateException("Pet must be know known to the manager"); + } + + return petInteractionMap.get(petId).peek(); + } + + /** + * {@inheritDoc} + */ + @Override + public InteractionRecord getCurrentInteraction(Long petId) { + try { + InteractionRecord record = getLastInteraction(petId); + if (record.isEvaluated()) { + return null; + } + return record; + } catch (IllegalStateException e) { + return null; + } + } +} diff --git a/web_backend/src/main/java/haw/teamagochi/backend/pet/logic/petmanager/PetManager.java b/web_backend/src/main/java/haw/teamagochi/backend/pet/logic/petmanager/PetManager.java new file mode 100644 index 00000000..0145be5d --- /dev/null +++ b/web_backend/src/main/java/haw/teamagochi/backend/pet/logic/petmanager/PetManager.java @@ -0,0 +1,50 @@ +package haw.teamagochi.backend.pet.logic.petmanager; + +import haw.teamagochi.backend.pet.dataaccess.model.PetEntity; + +/** + * Defines a pet manager. + * + *

Caches pets which are currently active and stores non-persistent computations. + */ +public interface PetManager { + + /** + * Check if a pet is known to the manager. + * + * @param petId to check for + * @return true if pet is known, otherwise false + */ + boolean contains(Long petId); + + /** + * Add a pet. + * + * @param pet entity which should be managed + */ + void add(PetEntity pet); + + /** + * Add an {@link InteractionRecord} for a given pet. + * + * @param petId which identifies the pet + * @param record to be added + */ + void addInteraction(Long petId, InteractionRecord record); + + /** + * Get the last {@link InteractionRecord} for a given pet. + * + * @param petId which identifies the pet + * @return the last record + */ + InteractionRecord getLastInteraction(Long petId); + + /** + * Get the last non-evaluated {@link InteractionRecord} for a given pet. + * + * @param petId which identifies the pet + * @return the last record if it hasn't been evaluated, otherwise null + */ + InteractionRecord getCurrentInteraction(Long petId); +} diff --git a/web_backend/src/main/java/haw/teamagochi/backend/pet/service/rest/v1/mapper/PetMapper.java b/web_backend/src/main/java/haw/teamagochi/backend/pet/service/rest/v1/mapper/PetMapper.java index 8cc172ea..dcbb8b18 100644 --- a/web_backend/src/main/java/haw/teamagochi/backend/pet/service/rest/v1/mapper/PetMapper.java +++ b/web_backend/src/main/java/haw/teamagochi/backend/pet/service/rest/v1/mapper/PetMapper.java @@ -13,7 +13,6 @@ import org.mapstruct.Mapper; import org.mapstruct.Mapping; import org.mapstruct.MappingTarget; -import org.mapstruct.ObjectFactory; import org.mapstruct.factory.Mappers; /** @@ -65,7 +64,10 @@ public interface PetMapper { @AfterMapping default void findOwner(PetDTO dto, @MappingTarget PetEntity entity, @Context UcFindUser ucFindUser) { - if (dto.getOwnerId() == null || ucFindUser == null) return; + if (dto.getOwnerId() == null || ucFindUser == null) { + entity.setOwner(null); + return; + } UserEntity dbEntity = ucFindUser.find(dto.getOwnerId()); if (dbEntity != null) { @@ -74,13 +76,15 @@ default void findOwner(PetDTO dto, @MappingTarget PetEntity entity, @Context UcF } @AfterMapping - default void findPetTyp( PetDTO dto,@MappingTarget PetEntity entity, @Context UcFindPetType ucFindPetTyp) { - if (dto.getType() == null || ucFindPetTyp == null) return; + default void findPetType(PetDTO dto, @MappingTarget PetEntity entity, @Context UcFindPetType ucFindPetType) { + if (dto.getType() == null || ucFindPetType == null) { + entity.setPetType(null); + return; + } - PetTypeEntity dbEntity = ucFindPetTyp.find(dto.getType()); + PetTypeEntity dbEntity = ucFindPetType.find(dto.getType()); if (dbEntity != null) { entity.setPetType(dbEntity); } } - } diff --git a/web_backend/src/main/java/haw/teamagochi/backend/user/dataaccess/model/UserEntity.java b/web_backend/src/main/java/haw/teamagochi/backend/user/dataaccess/model/UserEntity.java index e8c2b636..1d17712a 100644 --- a/web_backend/src/main/java/haw/teamagochi/backend/user/dataaccess/model/UserEntity.java +++ b/web_backend/src/main/java/haw/teamagochi/backend/user/dataaccess/model/UserEntity.java @@ -1,24 +1,23 @@ package haw.teamagochi.backend.user.dataaccess.model; -import haw.teamagochi.backend.device.dataaccess.model.DeviceEntity; -import haw.teamagochi.backend.pet.dataaccess.model.PetEntity; import jakarta.persistence.Column; import jakarta.persistence.Entity; import jakarta.persistence.GeneratedValue; import jakarta.persistence.Id; -import jakarta.persistence.OneToMany; -import jakarta.validation.Valid; -import java.util.ArrayList; -import java.util.List; import java.util.UUID; import lombok.Getter; +import lombok.NoArgsConstructor; import lombok.NonNull; import lombok.RequiredArgsConstructor; import lombok.Setter; +/** + * Persist-able user representation. + */ @Getter @Setter @RequiredArgsConstructor +@NoArgsConstructor @Entity public class UserEntity { @@ -26,12 +25,15 @@ public class UserEntity { @GeneratedValue private long id; - public UserEntity() {} - @NonNull @Column(unique = true) private UUID externalID; - - + @Override + public String toString() { + return "UserEntity{" + + "id=" + id + + ", externalID=" + externalID + + '}'; + } } diff --git a/web_backend/src/main/resources/application.properties b/web_backend/src/main/resources/application.properties index e3ac3ff8..63101aaa 100644 --- a/web_backend/src/main/resources/application.properties +++ b/web_backend/src/main/resources/application.properties @@ -27,6 +27,11 @@ leshan.api-url=https://leshan.eclipseprojects.io/api %prod.leshan.api-url=http://teashan:8080/api %teashan.leshan.api-url=http://localhost:4000/leshan/api +leshan.client-endpoint-name.prefix=urn:t8i:dev +# Filter device handling by client endpoint name prefix. Modes: all, receive, write +leshan.client-endpoint-name.filter-mode=write +%prod,teashan.leshan.client-endpoint-name.filter-mode=all + keycloak.enabled=false %prod.keycloak.enabled=true %keycloak.keycloak.enabled=true @@ -35,7 +40,8 @@ keycloak.auth-server-url=http://localhost:4000/kc/realms/teamagochi %prod.keycloak.auth-server-url=http://keycloak:8080/kc/realms/teamagochi dummydata.load=true -%prod.dummydata.load=false +# Just always load them, it's convenient for testing the Pi... +%prod.dummydata.load=true postgres.jdbc.url=jdbc:postgresql://localhost:5432/backend?currentSchema=backend %prod.postgres.jdbc.url=jdbc:postgresql://postgres:5432/backend?currentSchema=backend @@ -56,8 +62,10 @@ db.password=postgres-backend-password # General # # # # # # # +quarkus.log.level=INFO + quarkus.http.cors=true -quarkus.http.cors.origins=* +quarkus.http.cors.origins=/.*/ %test.quarkus.http.test-timeout=5s %test.quarkus.oidc.enabled=false @@ -75,8 +83,8 @@ quarkus.rest-client.leshan-event-api.http2=true # Uncomment to print request logs to stdout #quarkus.rest-client.logging.scope=request-response -#quarkus.rest-client.logging.body-limit=300 -#quarkus.log.category."org.jboss.resteasy.reactive.client.logging".level=DEBUG +#quarkus.rest-client.logging.body-limit=1500 +#quarkus.log.category."org.jboss.resteasy.reactive.client.logging".level=INFO # # # # # # # # # # REST Services # @@ -94,7 +102,7 @@ quarkus.smallrye-openapi.security-scheme=jwt quarkus.smallrye-openapi.jwt-security-scheme-value=bearer quarkus.smallrye-openapi.jwt-bearer-format=JWT -%prod.quarkus.smallrye-openapi.servers=http://localhost:4000/backend +%prod.quarkus.smallrye-openapi.servers=https://teamagochi/backend %prod.quarkus.swagger-ui.urls.default=/backend/q/openapi # # # # # # # # # # diff --git a/web_backend/src/main/resources/google_checks.xml b/web_backend/src/main/resources/google_checks.xml index 53dcdb17..2680c414 100644 --- a/web_backend/src/main/resources/google_checks.xml +++ b/web_backend/src/main/resources/google_checks.xml @@ -333,7 +333,7 @@ - + diff --git a/web_backend/src/main/resources/jsonschema/common/BindingMode.json b/web_backend/src/main/resources/jsonschema/common/BindingMode.json index bc5c10ba..f7504dd0 100644 --- a/web_backend/src/main/resources/jsonschema/common/BindingMode.json +++ b/web_backend/src/main/resources/jsonschema/common/BindingMode.json @@ -6,6 +6,7 @@ "U", "S", "US", + "UT", "UQ", "SQ", "USQ" diff --git a/web_backend/src/main/resources/jsonschema/rest/Resource.json b/web_backend/src/main/resources/jsonschema/common/Resource.json similarity index 95% rename from web_backend/src/main/resources/jsonschema/rest/Resource.json rename to web_backend/src/main/resources/jsonschema/common/Resource.json index e514d68c..60ef7edf 100644 --- a/web_backend/src/main/resources/jsonschema/rest/Resource.json +++ b/web_backend/src/main/resources/jsonschema/common/Resource.json @@ -14,7 +14,7 @@ "type": "string" }, "value": { - "type": "string" + "type": "any" }, "values": { "type": "object", diff --git a/web_backend/src/main/resources/jsonschema/events/Notification.json b/web_backend/src/main/resources/jsonschema/events/Notification.json new file mode 100644 index 00000000..a52820f4 --- /dev/null +++ b/web_backend/src/main/resources/jsonschema/events/Notification.json @@ -0,0 +1,26 @@ +{ + "$schema": "http://json-schema.org/draft-06/schema#", + "title": "Notification", + "type": "object", + "additionalProperties": false, + "properties": { + "ep": { + "type": "string" + }, + "kind": { + "type": "string" + }, + "res": { + "type": "string" + }, + "val": { + "$ref": "../common/Resource.json" + } + }, + "required": [ + "ep", + "kind", + "res", + "val" + ] +} diff --git a/web_backend/src/main/resources/jsonschema/rest/ObjectInstance.json b/web_backend/src/main/resources/jsonschema/rest/ObjectInstance.json index a46ed269..59ee9fe2 100644 --- a/web_backend/src/main/resources/jsonschema/rest/ObjectInstance.json +++ b/web_backend/src/main/resources/jsonschema/rest/ObjectInstance.json @@ -10,7 +10,7 @@ "resources": { "type": "array", "items": { - "$ref": "Resource.json" + "$ref": "../common/Resource.json" } }, "id": { diff --git a/web_backend/src/main/resources/jsonschema/rest/ObjectInstanceResponseContent.json b/web_backend/src/main/resources/jsonschema/rest/ObjectInstanceResponseContent.json index 4e29aedc..51562736 100644 --- a/web_backend/src/main/resources/jsonschema/rest/ObjectInstanceResponseContent.json +++ b/web_backend/src/main/resources/jsonschema/rest/ObjectInstanceResponseContent.json @@ -10,7 +10,7 @@ "resources": { "type": "array", "items": { - "$ref": "Resource.json" + "$ref": "../common/Resource.json" } }, "id": { diff --git a/web_backend/src/main/resources/jsonschema/rest/ResourceResponse.json b/web_backend/src/main/resources/jsonschema/rest/ResourceResponse.json index 23fb5ef3..045cbc14 100644 --- a/web_backend/src/main/resources/jsonschema/rest/ResourceResponse.json +++ b/web_backend/src/main/resources/jsonschema/rest/ResourceResponse.json @@ -17,7 +17,7 @@ "type": "boolean" }, "content": { - "$ref": "Resource.json" + "$ref": "../common/Resource.json" } }, "required": [ diff --git a/web_backend/src/main/resources/jsonschema/rest/ResourceResponseContent.json b/web_backend/src/main/resources/jsonschema/rest/ResourceResponseContent.json index 38ad11b0..b43d698c 100644 --- a/web_backend/src/main/resources/jsonschema/rest/ResourceResponseContent.json +++ b/web_backend/src/main/resources/jsonschema/rest/ResourceResponseContent.json @@ -10,7 +10,7 @@ "resources": { "type": "array", "items": { - "$ref": "Resource.json" + "$ref": "../common/Resource.json" } }, "id": { diff --git a/web_backend/src/test/java/haw/teamagochi/backend/device/logic/clients/rest/LeshanClientResourceTests.java b/web_backend/src/test/java/haw/teamagochi/backend/device/logic/clients/rest/LeshanClientResourceTests.java index 8b68c793..34d4124c 100644 --- a/web_backend/src/test/java/haw/teamagochi/backend/device/logic/clients/rest/LeshanClientResourceTests.java +++ b/web_backend/src/test/java/haw/teamagochi/backend/device/logic/clients/rest/LeshanClientResourceTests.java @@ -6,7 +6,7 @@ import haw.teamagochi.backend.leshanclient.datatypes.rest.ObjectInstanceResponseDto; import haw.teamagochi.backend.leshanclient.datatypes.rest.ObjectResponseDto; import haw.teamagochi.backend.leshanclient.datatypes.rest.ObjectspecDto; -import haw.teamagochi.backend.leshanclient.datatypes.rest.ResourceDto; +import haw.teamagochi.backend.leshanclient.datatypes.common.ResourceDto; import haw.teamagochi.backend.leshanclient.datatypes.rest.ResourceResponseDto; import io.quarkus.test.junit.QuarkusTest; import java.util.Set; @@ -188,7 +188,7 @@ public void testGetClientResource_singleResource() { assertEquals("singleResource", contentDto.kind); assertEquals(resourceId, contentDto.id); assertEquals("INTEGER", contentDto.type); - assertNotNull(Integer.valueOf(contentDto.value)); + assertNotNull(contentDto.value); assertNull(contentDto.values); // because type == singleResource // Print diff --git a/web_backend/src/test/java/haw/teamagochi/backend/pet/dataaccess/repository/PetRepositoryTests.java b/web_backend/src/test/java/haw/teamagochi/backend/pet/dataaccess/repository/PetRepositoryTests.java index ed77e99d..41868240 100644 --- a/web_backend/src/test/java/haw/teamagochi/backend/pet/dataaccess/repository/PetRepositoryTests.java +++ b/web_backend/src/test/java/haw/teamagochi/backend/pet/dataaccess/repository/PetRepositoryTests.java @@ -101,9 +101,15 @@ public void testDeletePetEntityDoesNotDeletePetTypeEntity() { petRepository.persist(entity); // When - Executable deleteFn = () -> petRepository.delete(entity); + Executable deleteFn = () -> { + petRepository.delete(entity); + + // Needed for exception to be thrown, otherwise assertThrows is always false + petTypeRepository.flush(); + }; // Then + //assertThrows(PersistenceException.class, deleteFn); assertDoesNotThrow(deleteFn); assertNotNull(petTypeRepository.findById(petType.getId())); } @@ -128,6 +134,6 @@ public void testDeletePetEntityDoesNotDeletePetTypeEntity2() { }; // Then - assertThrows(PersistenceException.class, deleteFn); + assertDoesNotThrow(deleteFn); } } diff --git a/web_backend/src/test/java/haw/teamagochi/backend/pet/logic/UcPetStatusImplTest.java b/web_backend/src/test/java/haw/teamagochi/backend/pet/logic/UcPetStatusImplTest.java index 441c97d6..f651bd67 100644 --- a/web_backend/src/test/java/haw/teamagochi/backend/pet/logic/UcPetStatusImplTest.java +++ b/web_backend/src/test/java/haw/teamagochi/backend/pet/logic/UcPetStatusImplTest.java @@ -4,7 +4,7 @@ import haw.teamagochi.backend.pet.dataaccess.model.PetEntity; import haw.teamagochi.backend.pet.dataaccess.model.PetTypeEntity; -import haw.teamagochi.backend.pet.logic.Events.PetEvents; +import haw.teamagochi.backend.pet.logic.game.events.PetEvents; import haw.teamagochi.backend.user.dataaccess.model.UserEntity; import haw.teamagochi.backend.user.logic.UcManageUser; import io.quarkus.test.junit.QuarkusTest;