ECE366 - Lesson 8

Putting Everything Together

Instructor: Professor Hong

Review & Questions

## Putting it all together - DB
## setup.sql ```dockerfile FROM postgres # Copy the SQL files to the container COPY setup.sql /docker-entrypoint-initdb.d/0_setup.sql COPY player.sql /docker-entrypoint-initdb.d/1_player.sql COPY hand.sql /docker-entrypoint-initdb.d/2_hand.sql COPY game.sql /docker-entrypoint-initdb.d/3_game.sql COPY sample_records.sql /docker-entrypoint-initdb.d/4_sample_records.sql ```
## player.sql ```sql CREATE TABLE public.player ( user_name varchar(255) NOT NULL, "password" varchar(255) NOT NULL, total_wins int4 DEFAULT 0 NULL, total_losses int4 DEFAULT 0 NULL, total_games int4 NULL, CONSTRAINT player_pkey PRIMARY KEY (user_name) ); ```
## game.sql ```sql CREATE SEQUENCE game_seq start with 1; CREATE TABLE public.game ( game_id int4 DEFAULT nextval('game_seq'::regclass) NOT NULL, p1 varchar(255) NULL, p2 varchar(255) NULL, p1_hand varchar(255) NULL, p2_hand varchar(255) NULL, winner varchar(255) NULL, CONSTRAINT game_pkey PRIMARY KEY (game_id), CONSTRAINT game_p1_fkey FOREIGN KEY (p1) REFERENCES public.player(user_name), CONSTRAINT game_p1_hand_fkey FOREIGN KEY (p1_hand) REFERENCES public.hand(hand_letter), CONSTRAINT game_p2_fkey FOREIGN KEY (p2) REFERENCES public.player(user_name), CONSTRAINT game_p2_hand_fkey FOREIGN KEY (p2_hand) REFERENCES public.hand(hand_letter), CONSTRAINT game_winner_fkey FOREIGN KEY (winner) REFERENCES public.player(user_name) ); ```
## hand.sql ```sql CREATE TABLE public.hand ( hand_letter bpchar(1) NOT NULL, hand_name varchar(10) NOT NULL, CONSTRAINT hand_pkey PRIMARY KEY (hand_letter) ); ```
## sample_records.sql ```sql -- Insert hand records first (referenced by game) INSERT INTO public.hand (hand_letter, hand_name) VALUES ('R', 'Rock'), ('P', 'Paper'), ('S', 'Scissors'); -- Insert player records (referenced by game) INSERT INTO public.player (user_name, password, total_wins, total_losses) VALUES ('jshron', 'password123', 0, 0), ('kenc', 'pikachu', 0, 0), ('username', 'HELP', 0, 0), ('wilson', 'newpassword', 10, 0); -- Insert game records INSERT INTO public.game (game_id, p1, p2, p1_hand, p2_hand, winner) VALUES (1, 'jshron', 'kenc', 'R', 'S', 'jshron'), (2, 'kenc', 'jshron', 'P', 'R', 'kenc'), (3, 'jshron', 'kenc', 'S', 'P', 'jshron'), (4, 'kenc', 'jshron', 'R', 'S', 'kenc'), (5, 'jshron', 'kenc', 'P', 'R', 'jshron'), (6, 'username', 'jshron', 'S', 'P', 'username'), (7, 'kenc', 'username', 'R', 'S', 'kenc'), (8, 'username', 'kenc', 'P', 'R', 'username'), (9, 'jshron', 'kenc', NULL, NULL, NULL), (10, 'jshron', 'kenc', 'R', 'S', 'jshron'); ```
## Initializing database Dockerfile ```dockerfile FROM postgres # Copy the SQL files to the container COPY setup.sql /docker-entrypoint-initdb.d/0_setup.sql COPY player.sql /docker-entrypoint-initdb.d/1_player.sql COPY hand.sql /docker-entrypoint-initdb.d/2_hand.sql COPY game.sql /docker-entrypoint-initdb.d/3_game.sql COPY sample_records.sql /docker-entrypoint-initdb.d/4_sample_records.sql ```
## Docker Compose docker-compose.yaml ```yaml services: db: image: postgres build: db environment: - POSTGRES_DB=postgres - POSTGRES_PASSWORD=password expose: - "5432" ports: - "5432:5432" restart: always ```
## Putting it all together - Spring Boot
## Spring Boot Service ```dockerfile FROM maven:3-eclipse-temurin-25 AS build ADD . /project WORKDIR /project RUN mvn -e -Dmaven.test.skip package FROM eclipse-temurin:25-jre COPY --from=build /project/target/rpsjpa-0.0.1-SNAPSHOT.jar /app/rps.jar ENTRYPOINT java -jar /app/rps.jar ```
## Docker Compose docker-compose.yaml ```yaml services: db: image: postgres build: db environment: - POSTGRES_DB=postgres - POSTGRES_PASSWORD=password expose: - "5432" ports: - "5432:5432" restart: always app: build: svc/rpsjpa environment: - POSTGRES_HOST=db - POSTGRES_DB=rps - POSTGRES_USER=postgres - POSTGRES_PASSWORD=password expose: - "8080" ports: - "8080:8080" depends_on: - db ```
## Putting it all together - UI
## vite.config.js Local testing ```javascript import { defineConfig } from 'vite' import react from '@vitejs/plugin-react' // https://vite.dev/config/ export default defineConfig({ plugins: [react()], server: { proxy: { '/api': 'http://localhost:8080', }, }, }) ```
## vite.config.js Docker compose updates ```javascript import { defineConfig } from 'vite' import react from '@vitejs/plugin-react' // https://vite.dev/config/ export default defineConfig({ plugins: [react()], server: { host: true, proxy: { '/api': 'http://app:8080', }, }, }) ```
## React UI Dockerfile ```dockerfile # pull official base image FROM node:22 # set working directory WORKDIR /app # add `/app/node_modules/.bin` to $PATH ENV PATH=/app/node_modules/.bin:$PATH # install app dependencies COPY package*.json ./ RUN npm ci # add app COPY . ./ # start Vite dev server and bind all interfaces for Docker port mapping CMD ["npm", "run", "dev", "--", "--host", "0.0.0.0"] ```
## Docker-Compose docker-compose.yaml ```yaml services: db: image: postgres build: db environment: - POSTGRES_DB=postgres - POSTGRES_PASSWORD=password expose: - "5432" ports: - "5432:5432" restart: always app: build: svc/rpsjpa environment: - POSTGRES_HOST=db - POSTGRES_DB=rps - POSTGRES_USER=postgres - POSTGRES_PASSWORD=password expose: - "8080" ports: - "8080:8080" depends_on: - db ui: build: ui ports: - "5173:5173" ```
## Websocket + STOMP
## STOMP - Simple/Streaming Text Oriented Message Protocol - Allows you to create an interactive web application - The web is the STOMP client - The service is the message broker - Example from [https://spring.io/guides/gs/messaging-stomp-websocket/](https://spring.io/guides/gs/messaging-stomp-websocket/)
## Pre-initialized Spring Initializr [Pre-initialized Initializr](https://start.spring.io/#!type=maven-project&groupId=com.example&artifactId=messaging-stomp-websocket&name=messaging-stomp-websocket&description=Demo%20project%20for%20Spring%20Boot&packageName=com.example.messaging-stomp-websocket&dependencies=websocket) Additional pom.xml dependencies Note: Since we load jQuery, Bootstrap, and STOMP from CDNs in index.html, no additional webjar dependencies are needed beyond the websocket starter.
## HelloMessage Class HelloMessage.java ```java package com.example.messagingstompwebsocket; public class HelloMessage { private String name; public HelloMessage() { } public HelloMessage(String name) { this.name = name; } public String getName() { return name; } public void setName(String name) { this.name = name; } } ```
## Greeting Class Greeting.java ```java package com.example.messagingstompwebsocket; public class Greeting { private String content; public Greeting() { } public Greeting(String content) { this.content = content; } public String getContent() { return content; } } ```
## GreetingController Class GreetingController.java ```java package com.example.messagingstompwebsocket; import org.springframework.messaging.handler.annotation.MessageMapping; import org.springframework.messaging.handler.annotation.SendTo; import org.springframework.stereotype.Controller; import org.springframework.web.util.HtmlUtils; @Controller public class GreetingController { @MessageMapping("/hello") @SendTo("/topic/greetings") public Greeting greeting(HelloMessage message) throws Exception { Thread.sleep(1000); // simulated delay return new Greeting("Hello, " + HtmlUtils.htmlEscape(message.getName()) + "!"); } } ```
## WebSocketConfig Class WebSocketConfig.java ```java package com.example.messagingstompwebsocket; import org.springframework.context.annotation.Configuration; import org.springframework.messaging.simp.config.MessageBrokerRegistry; import org.springframework.web.socket.config.annotation.EnableWebSocketMessageBroker; import org.springframework.web.socket.config.annotation.StompEndpointRegistry; import org.springframework.web.socket.config.annotation.WebSocketMessageBrokerConfigurer; @Configuration @EnableWebSocketMessageBroker public class WebSocketConfig implements WebSocketMessageBrokerConfigurer { @Override public void configureMessageBroker(MessageBrokerRegistry config) { config.enableSimpleBroker("/topic"); config.setApplicationDestinationPrefixes("/app"); } @Override public void registerStompEndpoints(StompEndpointRegistry registry) { registry.addEndpoint("/gs-guide-websocket"); } } ```
## app.js ```javascript const stompClient = new StompJs.Client({ brokerURL: 'ws://localhost:8080/gs-guide-websocket' }); stompClient.onConnect = (frame) => { setConnected(true); console.log('Connected: ' + frame); stompClient.subscribe('/topic/greetings', (greeting) => { showGreeting(JSON.parse(greeting.body).content); }); }; stompClient.onWebSocketError = (error) => { console.error('Error with websocket', error); }; stompClient.onStompError = (frame) => { console.error('Broker reported error: ' + frame.headers['message']); console.error('Additional details: ' + frame.body); }; function setConnected(connected) { $("#connect").prop("disabled", connected); $("#disconnect").prop("disabled", !connected); if (connected) { $("#conversation").show(); } else { $("#conversation").hide(); } $("#greetings").html(""); } function connect() { stompClient.activate(); } function disconnect() { stompClient.deactivate(); setConnected(false); console.log("Disconnected"); } function sendName() { stompClient.publish({ destination: "/app/hello", body: JSON.stringify({'name': $("#name").val()}) }); } function showGreeting(message) { $("#greetings").append("" + message + ""); } $(function () { $("form").on('submit', (e) => e.preventDefault()); $( "#connect" ).click(() => connect()); $( "#disconnect" ).click(() => disconnect()); $( "#send" ).click(() => sendName()); }); ```
## RPS Game Logic - Add server side logic - Add UI to communicate with server