Session 8 · Programming Technology
Graphics, animation and a first database
Drawing on a panel with paintComponent and animating it with one Swing Timer that steps a Swing-free model; then a first database – JDBC with PreparedStatement, SQLite by default or MySQL in Docker, and a UI → backend → repository split that tests without a database.
Two new things this session, and both are the foundation of the third assignment. The first is elementary graphics: so far we built windows from buttons and labels; now we draw on a panel ourselves and animate the drawing with a Timer. The second is the database: the games of the third assignment save the player's result at the end and read back the top ten. Here we look at the smallest program that talks to a database; next session puts it into games.
The examples: UFOs (UFOs flying around a panel, everything in the view), UFOsMV (the same in model–view, with tests) and HighScores (a console leaderboard on top of SQLite or MySQL).
Two ideas carry the whole session: paintComponent only draws what the model says, and only the repository knows that a database exists. Everything else is detail.Drawing on a panel: paintComponent
Until now components drew themselves. To show something else, extend JPanel and override paintComponent:
class DrawArea extends JPanel {
@Override
protected void paintComponent(Graphics g) {
super.paintComponent(g); // clears the area with the background colour
Graphics2D g2 = (Graphics2D) g;
g2.setColor(Color.RED);
g2.fillOval(100, 50, 200, 30); // top-left corner (x, y), width, height
}
}| Rule | Why |
|---|---|
You never call paintComponent | Swing calls it whenever the component must be drawn: the first time, after a resize, when another window uncovered it, or when you asked. |
If what you draw changed, call repaint() | repaint() only requests a redraw; Swing soon calls paintComponent on the EDT. Several requests are merged into one. |
Start with super.paintComponent(g) | It clears the area. Without it the previous frame stays, and the UFOs leave trails. |
| Don't change the component while painting | No setBackground in paintComponent: it requests another repaint, which calls paintComponent again… Draw the background as a filled rectangle. |
Graphics2D is the pen | setColor, fillOval, fillRect, drawLine, fillPolygon, drawString, drawImage; an antialiasing rendering hint smooths the edges. |
Animation: one Timer, one step
An animation is many still frames in a row. A javax.swing.Timer calls a listener at a fixed interval – here every 30 ms, about 33 frames per second – and that listener steps the model and asks for a repaint:
timer = new Timer(30, e -> {
space.step(); // the model: every UFO moves on
drawArea.repaint(); // the view: ask for a redraw
});
timer.start();The Timer's listener runs on the event dispatch thread (EDT), just like a button's, so it may touch both the components and the model – there is no threading problem to solve. The third assignment's games (moving sprites, a clock) work exactly like this.
The mouse
Mouse events go to a component: MouseListener (click, press, release) and MouseMotionListener (move, drag). MouseAdapter implements both with empty methods, so you override only what you need. Dragging in the UFO program pulls every UFO to the mouse:
addMouseMotionListener(new MouseAdapter() {
@Override
public void mouseDragged(MouseEvent e) {
space.moveAllTo(e.getX() - Ufo.WIDTH / 2.0, e.getY() - Ufo.HEIGHT / 2.0);
repaint();
}
});The keyboard (WASD control) comes next session.
UFOs in model–view
The starting UFOs program works, but it repeats every mistake of the flashcards session:
- The rules are in the view. How many UFOs there are, how a new one is made, how often one changes colour – all in
UFOGUI. No test can reach them. - The model depends on AWT.
UFOstores ajava.awt.Colorand draws itself onto aGraphics2D. - Three classes share one
ArrayList(UFOGUI,DrawArea, the Timers) and none of them owns it. - Two Timers do what one could.
setBackgroundinsidepaintComponent– changing the component while drawing it.
In UFOsMV, Space is the model: it owns the list, the size, the background colour and the rules (addUfos, removeUfos, step, moveAllTo, newBackground). It gets its Random from the constructor, so a test can fix the seed. step is one tick of "physics", without Swing:
public void step() {
for (Ufo ufo : ufos) {
ufo.move(width, height); // wraps around at the edges
if (random.nextDouble() < RECOLOR_PROBABILITY) {
ufo.setRgb(randomRgb()); // 0xRRGGBB as an int, not a Color
}
}
}The view reads the model in two places: in paintComponent (background, UFOs) and in the status bar (count). drawUfo is the only place that decides what a UFO looks like – the model doesn't know it is a dome on a saucer.
DrawArea.paintComponent – the view draws the model
@Override
protected void paintComponent(Graphics g) {
super.paintComponent(g);
Graphics2D g2 = (Graphics2D) g;
g2.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON);
g2.setColor(new Color(space.getBackgroundRgb())); // the background is the model's, a filled rectangle
g2.fillRect(0, 0, getWidth(), getHeight());
for (Ufo ufo : space.getUfos()) {
drawUfo(g2, ufo); // the only place that knows what a UFO looks like
}
}| UFOs | UFOsMV | |
|---|---|---|
| Owner of the list | nobody – three classes change it | Space |
| Colour | java.awt.Color in the model | int (0xRRGGBB) in the model, Color in the view |
| Who draws a UFO | the UFO itself | DrawArea.drawUfo |
| Background | setBackground in paintComponent | the model's colour, a filled rectangle |
| Timers | two (move, recolour) | one – step does both |
| Size of the space | the Timer asks the panel | the model's; the panel calls space.resize |
| Randomness | Math.random() | Random from the constructor |
| Tests | none | 21 |
Why a database?
In the third assignment the player's name and score are saved when the game ends, and a menu item shows the top ten. A text file could do that too, but:
| A file | A database |
|---|---|
| you invent the format and write the parser | the structure (table, columns, types) is given and enforced |
| "top 10" = read everything, sort, cut | SELECT … ORDER BY score DESC LIMIT 10 – the database's job |
| two programs writing at once corrupt it | many users at once, transactions |
| a crash halfway leaves a half-written file | an operation happens completely or not at all |
In bigger programs – web, mobile, enterprise – data practically always lives in a database. For the assignment you need the basics: one table, insert, query.
SQL in one minute
A relational database consists of tables. A table is like a spreadsheet: it has columns (with a name and a type) and rows (one row = one result). You talk to it in SQL:
-- Create the table (once). IF NOT EXISTS: no error if it's already there.
CREATE TABLE IF NOT EXISTS highscores (
name VARCHAR(50) NOT NULL, -- text, at most 50 characters, required
score INTEGER NOT NULL, -- whole number
created_at TIMESTAMP NOT NULL -- date and time
);
-- Insert a row
INSERT INTO highscores (name, score, created_at) VALUES ('Anna', 42, CURRENT_TIMESTAMP);
-- The top 10: highest score first, on a tie the earlier one wins
SELECT name, score, created_at
FROM highscores
ORDER BY score DESC, created_at ASC
LIMIT 10;
-- Delete every row
DELETE FROM highscores;That is all the SQL the assignment needs. (If a task says "increase the winner's count, or insert a new row", you also need UPDATE – see next session.)
JDBC: how Java talks to a database
JDBC (Java Database Connectivity) is part of the JDK: one interface for every database. The database-specific part is the driver, a jar that Maven downloads from the pom.xml. The program is the same whether SQLite or MySQL is behind it – only the JDBC URL differs.
| Class (java.sql) | What it is for |
|---|---|
DriverManager.getConnection(url, user, password) | opens a connection; the URL prefix (jdbc:sqlite:, jdbc:mysql:) selects the driver |
Connection | one connection; must be closed → try-with-resources |
PreparedStatement | an SQL statement with ? placeholders; setString(1, name), setInt(2, score); executeUpdate() for INSERT/UPDATE/DELETE, executeQuery() for SELECT |
ResultSet | the rows of a query, one at a time: while (rows.next()), rows.getString("name") |
SQLException | checked exception: the server isn't running, wrong password, no such table … |
JdbcHighScoreRepository.save
public void save(HighScore highScore) throws StorageException {
try (Connection connection = connect();
PreparedStatement statement = connection.prepareStatement(
"INSERT INTO highscores (name, score, created_at) VALUES (?, ?, ?)")) {
statement.setString(1, highScore.name());
statement.setInt(2, highScore.score());
statement.setTimestamp(3, Timestamp.valueOf(highScore.createdAt()));
statement.executeUpdate();
} catch (SQLException e) {
throw new StorageException("Could not save the score: " + e.getMessage(), e);
}
}JdbcHighScoreRepository.findTop – the core
try (Connection connection = connect();
PreparedStatement statement = connection.prepareStatement(SELECT_TOP)) {
statement.setInt(1, limit);
try (ResultSet rows = statement.executeQuery()) {
while (rows.next()) {
result.add(new HighScore(rows.getString("name"), rows.getInt("score"),
rows.getTimestamp("created_at").toLocalDateTime()));
}
}
} catch (SQLException e) {
throw new StorageException("Could not read the leaderboard: " + e.getMessage(), e);
}Every resource is opened in a try-with-resources header, so it is closed even when an exception flies. And the repository wraps SQLException into its own StorageException: the caller doesn't depend on java.sql and doesn't care whether the failure came from an SQL server or a file.
SQLite or MySQL?
The projects contain both, and the code is the same: three lines of Database.java decide which one is used.
| SQLite | MySQL | |
|---|---|---|
| What it is | a file (highscores.db) in the project folder; the driver contains the whole database engine | a server running in the background, accepting connections on a port |
| Installation | none: Maven downloads the driver | a running server – for us, a Docker container |
| User, password | none | yes |
| Good for | single-user desktop and mobile apps (browsers, phones, the assignment) | many users, web and enterprise systems |
| In the assignment | the default: always works, anywhere | an option: if Docker runs, one comment swap |
// Database.java – SQLite (the default)
public static final String URL = "jdbc:sqlite:highscores.db";
public static final String USER = null;
public static final String PASSWORD = null;
// MySQL in Docker: comment out the three lines above, uncomment these
// public static final String URL = "jdbc:mysql://localhost:3306/progtech";
// public static final String USER = "progtech";
// public static final String PASSWORD = "progtech";Each database stores values in its own way (SQLite keeps a timestamp as a number, MySQL as a date), but on the Java side getTimestamp returns the same LocalDateTime from both. Storage is the database's business; through JDBC the program sees one uniform picture.
Code-first: the program creates the table
The repository runs CREATE TABLE IF NOT EXISTS on its first connection. So nothing has to be prepared in the database: an empty SQLite file or a freshly started MySQL container is immediately fine.
private Connection connect() throws SQLException {
Connection connection = DriverManager.getConnection(url, user, password);
if (!tableChecked) { // code-first: once per repository
try (Statement statement = connection.createStatement()) {
statement.execute(CREATE_TABLE); // CREATE TABLE IF NOT EXISTS ...
tableChecked = true;
} catch (SQLException e) {
connection.close();
throw e;
}
}
return connection;
}- Anyone can start it who downloads the project: no SQL script to run, no database to click together. Including the person grading it.
- The table's structure is in the code, versioned with it. A new column changes the code and the table together.
- The tests run on a temporary, empty file – and the repository creates the table there too.
This is what frameworks like JPA/Hibernate or Entity Framework call "code-first", in its simplest form. If you want an id column (a leaderboard doesn't need one, a "real" table usually has one), the two databases spell it differently, which is why the example leaves it out:
-- SQLite
id INTEGER PRIMARY KEY AUTOINCREMENT,
-- MySQL
id INT AUTO_INCREMENT PRIMARY KEY,MySQL in a Docker container
A database server is traditionally installed: an installer, a service always running in the background, config files, and traces left behind when you remove it. Docker runs the program in a container instead – an isolated, pre-packaged environment built from an image that contains MySQL and everything it needs. A container
- starts with one command and is deleted with one, without a trace;
- is the same for everyone: the MySQL server image gives the same server with the same settings on Windows, macOS and Linux;
- is isolated: it touches nothing else on the machine, and you see only the port you publish (3306);
- is not a virtual machine: no whole operating system runs in it, only the program, so it starts in seconds and uses little memory.
This is how development databases are run in industry today. Cloud providers offer managed MySQL too: there you also get a URL, a user and a password, and your program stays the same – only Database.java changes.
Starting it
Install Docker Desktop (free for personal and educational use; on Windows it uses WSL, which the installer enables), start it, and check that docker version prints both a Client and a Server part. Then start the container from the project's compose.yaml:
compose.yaml
services:
mysql:
image: mysql # the official MySQL server image
container_name: progtech-mysql
environment:
MYSQL_ROOT_PASSWORD: root # the admin password
MYSQL_DATABASE: progtech # created on the first start ...
MYSQL_USER: progtech # ... with a user who may do anything in it
MYSQL_PASSWORD: progtech
ports:
- "3306:3306" # host:container
volumes:
- mysql-data:/var/lib/mysql # the data survives the container
volumes:
mysql-data:docker version # Client and Server both answer? Docker is running
docker compose up -d # start the container described by compose.yaml
docker logs progtech-mysql # wait for "ready for connections"
docker ps # running containers; PORTS shows 0.0.0.0:3306->3306/tcp
docker exec -it progtech-mysql mysql -u progtech -p # a MySQL prompt inside the container
docker compose stop # stop – the data stays
docker compose down -v # delete the container AND the volume – a clean slateThe same container without a compose file is a single (long) command; the compose file is the readable version of it, kept next to the project:
docker run --name progtech-mysql -e MYSQL_ROOT_PASSWORD=root -e MYSQL_DATABASE=progtech -e MYSQL_USER=progtech -e MYSQL_PASSWORD=progtech -p 3306:3306 -d mysqlOn the first start the server needs 10–20 seconds to initialise; ready for connections at the end of docker logs means it's done. To look inside, you don't need a MySQL client on your machine – the container has one (password: progtech):
USE progtech;
SHOW TABLES;
SELECT * FROM highscores ORDER BY score DESC;
EXITGraphical tools work too (DBeaver, MySQL Workbench, the database windows of IntelliJ or NetBeans): host localhost, port 3306, database, user and password progtech. For the SQLite file, DB Browser for SQLite or DBeaver opens highscores.db directly.
When it doesn't connect
| Message (from the SQLException) | Cause and fix |
|---|---|
Communications link failure | the container isn't running (docker ps) or isn't up yet (docker logs), or the port isn't 3306 |
Access denied for user 'progtech' | wrong user or password – check the container's environment (docker inspect progtech-mysql) |
Unknown database 'progtech' | the container was started with a different MYSQL_DATABASE |
port is already allocated | something else uses 3306 (another MySQL); stop it, or publish on -p 3307:3306 and use localhost:3307 in the URL |
No suitable driver found | the driver isn't on the classpath: is mysql-connector-j in the pom.xml? Reload Maven. |
no such table | the table wasn't created: does connect() run CREATE TABLE IF NOT EXISTS? |
database is locked | SQLite: another program (a DB browser) holds the file open for writing – close one |
Three layers: UI, backend, storage
HighScores is a console program: a menu to add a score, list the top ten and clear the board. It has three layers, and this structure carries straight into next session's games and into your assignment.
| Layer | Class | Job |
|---|---|---|
| UI | HighScores | the console menu; calls only the backend and gets DTOs; no SQL, no connection, no SQLException |
| backend | Leaderboard | the rules: which name is acceptable, when a score is made, what the rank is; save, top, clear |
| backend | HighScore | the model: one result as the backend and the repository handle it – one row of the table (record) |
| backend | HighScoreDto | the DTO: one row as the UI receives it, with its rank (record) |
| backend | HighScoreRepository | interface: what the backend expects from storage – save, findTop, deleteAll |
| storage | JdbcHighScoreRepository | the interface with JDBC: the only class with SQL in it; creates the table |
| storage | Database | the connection details in one place |
The layers are wired together in main:
Leaderboard leaderboard = new Leaderboard(new JdbcHighScoreRepository());
leaderboard.save(name, score);
for (HighScoreDto highScore : leaderboard.top(10)) { ... }Model and DTO: why two?
HighScore is the result as the backend and the repository handle it. HighScoreDto is what the UI receives: exactly what the display needs. Today they are nearly identical, but not the same:
- The DTO has something the model doesn't: the rank. The table doesn't know which place a row is in; the backend computes it from the query result (
toDto). - The model may hold things that don't belong on screen – in a bigger system internal ids, password hashes, audit data. The DTO carries none of them out.
- If the model changes (a new column, different storage), the DTO – and with it the UI – can stay the same. The DTO is the contract between backend and UI.
Leaderboard – validation, model in, DTO out
public void save(String name, int score) throws StorageException {
if (!isValidName(name)) {
throw new IllegalArgumentException("Invalid name: '" + name + "'");
}
repository.save(new HighScore(name.strip(), score, LocalDateTime.now()));
}
static HighScoreDto toDto(int rank, HighScore highScore) {
return new HighScoreDto(rank, highScore.name(), highScore.score(), highScore.createdAt());
}Validation in two places
The UI checks that the name is not empty or too long (Leaderboard.isValidName) and that the score is a number; if not, it prints a message and doesn't call the backend. The backend checks the same again and throws IllegalArgumentException. That is the real protection, because this UI is not the only caller – a game's view, a test, later a web API. The UI's check is for the user (a sensible message, not an exception); the backend's is for the data.
Tests
Why is the repository an interface? Because then the backend can be tested without a database: LeaderboardTest gives the Leaderboard a repository backed by a list, and checks the name validation, the timestamp and the length of the list with no file and no server. This is the repository pattern of the Spring and .NET worlds, in its simplest form.
/** A repository in a list – instead of the JDBC one, for the test. */
private static class InMemoryRepository implements HighScoreRepository {
private final List<HighScore> scores = new ArrayList<>();
@Override
public void save(HighScore highScore) {
scores.add(highScore);
}
// findTop: sort the list, limit; deleteAll: clear
}The SQL is tested separately by JdbcHighScoreRepositoryTest. Every test gets an empty, temporary SQLite file and deletes it at the end, so the tests never write into the real highscores.db and don't depend on each other. For that the repository has a second constructor that takes the URL, user and password.
@Before
public void createEmptyDatabase() throws IOException {
file = Files.createTempFile("highscores-test", ".db");
repository = new JdbcHighScoreRepository("jdbc:sqlite:" + file, null, null);
}
@After
public void deleteDatabase() throws IOException {
Files.deleteIfExists(file);
}The first test doubles as the code-first check (the empty file has no table; the repository creates it). Others check that a saved model comes back unchanged, the order and the LIMIT, that SQL typed into the name stays plain text, deleting, two repositories on one file seeing the same data, and that an unreachable server gives a StorageException, not an SQLException.
On the graphics side, UfoTest and SpaceTest check that a step adds the velocity, that a UFO wraps around at all four edges, that the same seed gives the same space, that after 10 000 steps every UFO is still inside, and that the list can't be modified from outside.
Practice
Task 1
Pause and speed
Add a Pause button to UFOsMV that stops and restarts the animation, and two buttons that make the animation faster and slower. Which class does each change belong to?
Approach
Both are about the clock, not the rules, so they stay in the view: timer.stop() / timer.start(), and timer.setDelay(…) for the speed. The model's step doesn't change at all.
pauseButton.addActionListener(e -> {
if (timer.isRunning()) {
timer.stop();
} else {
timer.start();
}
});Task 2
A new UFO every few seconds
Every 100th step a new UFO should appear. Implement it without a second Timer, and write a test that the count grows by exactly one after 100 steps.
Approach
A step counter in Space; step() checks it. In the test, create a Space with a fixed Random, call step() 99 times (no change), then once more.
private long steps;
public void step() {
steps++;
if (steps % SPAWN_PERIOD == 0) {
addUfos(1);
}
// ... move and recolour as before
}Task 3
A player's best score
Add a menu item to HighScores that asks for a name and prints that player's best score, or "no scores yet". Extend every layer, and test the backend with the in-memory repository.
Layer by layer
Interface first (findBest(name)), then the JDBC implementation with a ? for the name, then the in-memory fake in the test (a stream with filter and max), then Leaderboard.best(name) with the same name validation as save, and finally the menu. Note that MAX of no rows is SQL NULL – check rows.wasNull().
// HighScoreRepository
OptionalInt findBest(String name) throws StorageException;
// JdbcHighScoreRepository – the name goes in through ?, never by concatenation
"SELECT MAX(score) AS best FROM highscores WHERE name = ?"Task 4
Switch to MySQL
Start the MySQL container, switch Database.java to MySQL, save a few scores, then look at the table from inside the container. Finally stop the container and see what the program says.
Commands
docker compose up -d in the project folder, wait for ready for connections in docker logs progtech-mysql, then docker exec -it progtech-mysql mysql -u progtech -p and SELECT * FROM highscores;. With the container stopped you should get a StorageException message, not a stack trace.
Summary
- paintComponent
- the method Swing calls to draw a component; you override it and never call it
- repaint()
- a request for a redraw; Swing calls paintComponent soon, on the EDT
- Timer
- calls a listener at a fixed interval on the EDT – the clock of an animation
- JDBC
- the java.sql interface to every database; the driver and the URL choose the database
- PreparedStatement
- an SQL statement with ? placeholders; values go in as data, never as SQL
- Repository
- an interface for storage; the JDBC class implements it, a test can fake it
- DTO
- the object the UI receives – exactly what the display needs, e.g. with the rank
- Code-first
- the program creates its own tables (CREATE TABLE IF NOT EXISTS)
- paintComponent only draws the model; call repaint() when it changed, and start with super.paintComponent(g).
- One Timer calls one step() in the model, then repaint() – everything that happens over time goes into step().
- Positions and colours are numbers in the model; Color and drawing live in the view.
- JDBC + PreparedStatement + try-with-resources; wrap SQLException into your own exception.
- SQLite by default, MySQL in Docker as an option – only the URL changes.
- UI → backend → repository interface → JDBC; the backend is tested with an in-memory repository.