ECE366 - Lesson 8
Putting Everything Together
Instructor: Professor Hong
## 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"
```
## 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