Java 3 - 2026

Week 2

Lesson 1 - Setup Pet Clinic App. Deployment using Docker.
Lesson 2 - Create, Read, Update, Delete (CRUD) functionality.
Lesson 3 - Homepage with content from the database. Custom not found and error pages.
Lesson 4 - Users, roles, and permissions setup from Java 2. User Registration.
Lesson 5 - Login and logout. Cookies. User permission access.

Lesson 6 - Edit and delete user profile. Password reset.
Lesson 7 - Pagination. Filtering and limiting records by category.
Lesson 8 - Web Sockets.
Lesson 9 - Internationalization. Date and currency formatting.
Lesson 10 - Email and SMS messaging. Failed login attempts.
Lesson 11 - Shopping Cart. Payment processing.

Course Plan

  • Modify the Pet Clinic application based on the user stories and personas you created in Java 2.
  • Derive an entity-relationship diagram and SQL queries for the data you will collect.
  • Create user interface drawings and wireframes for all web layouts.
  • Write use case narratives and build diagrams for each feature you add.
  • Write unit tests as necessary.

Expectations

  • Navigate to your Azure Container App.
  • On the left menu, scroll down to Settings and click Custom domains.

  • Click + Add custom domain.

  • Select a Managed Certificate.

  • Type your registered domain name. I prefer adding a "www." subdomain.

  • Select A record for the Hostname record type.

  • Do not click "Add" yet. Look at the box that appears below. It will show you two critical values you need to copy:

    • A record IP address (A record or CNAME)

    • Verification ID (TXT, a long string of random characters)

Azure Custom Domain

  • I registered a domain using SiteGround for $20/year.
  • name.com offers a free one-year domain to GitHub Students.
  • Log in to SiteGround -> Services -> Domains for your domain.

  • Find the domain you want to use.

  • Go to Manage Domain -> DNS Zone Editor. Add two records.

  • The "Proof of Ownership" (TXT)

    • Name: If using the root domain (domain.com), enter: asuid

      If using a subdomain (www.domain.com), enter: asuid.www
    • Value: Paste the Verification ID from Azure.

  • The Traffic Route (A Record)

    • Name: If using the root domain, leave it Empty (or type @).

      If using a subdomain (www), type www

    • Value: Paste the IP address from Azure.

SiteGround Registration

  • If you use a CNAME record, make sure that it points www.[domain]. to [webapp].azurewebsites.net. (include the periods at the end.
  • An error message "You cannot create a CNAME record for a hostname that already has a DNS record created for it" may occur because DNS specifications do not allow a CNAME record to coexist with other record types (such as an A, AAAA, or TXT record) for the exact same hostname (or alias).
  • To resolve this, you must delete the existing, conflicting DNS record before adding the new CNAME record. Look for an A, AAAA, or TXT record (or any other type) that has the exact same name (host) as the CNAME record you are trying to create. Once you locate the conflicting record, click the trash can (Delete) icon next to it.

SiteGround Registration

  • Go back to your Azure Portal tab.
  • Click the Validate button.

    • It might take 1–5 minutes for SiteGround's changes to propagate. If it fails, wait a few minutes and try clicking Validate again.

  • Once the checks turn green, click Add.

  • Click on the "Add Binding" option.

  • Select the existing certificate.

  • Click Add.

  • Azure will now issue a free SSL certificate for your SiteGround domain.

Finish and Secure in Azure

  • Log in to SiteGround -> Websites.

  • Create a new empty website with the domain. When created, click the "Site Tools" button.
  • Click Domain > Redirects.
    • Select the domain.
    • Select "Permanent (301)" as the Redirect Type.
    • Redirect the domain to "https://www.[domain]"
  • Visit the URL in your browser. Entering the domain name without www should redirect to www.[domain]

  • A secure lock icon will appear when you visit the domain in the browser shortly.

Redirect domain to www

  • Install the AWS Command Line Interface (CLI).
  • Open a terminal and type this command to confirm it was installed.
    aws --version
  • For AWS, the direct equivalent to Azure Container Apps is AWS App Runner. It is the modern, "serverless" way to run containers without managing servers.
  • Unlike Azure (and Google Cloud), AWS App Runner does not support pulling images directly from Docker Hub. It only trusts its own registry, called Amazon ECR (Elastic Container Registry).
  • Sign up for AWS Educate.
  • Sign in to the AWS Management Console as the root user.
  • Using your "Root" account (the email/password you use to log into the console) can be a major security risk, especially for the CLI

AWS

  • Search for IAM in the top search bar and click it (Identity and Access Management).
  • On the left menu, click Users.

  • You will create a limited "Service User" that can only touch Container Registry (ECR) and App Runner.
  • Click the orange Create user button.

  • User details:

    • User name: petclinic-deployer (or whatever you prefer).

    • Leave the "Provide user access to the AWS Management Console" box unchecked. (This user is for the CLI only, not for clicking around the website).

    • Click Next.

Create a User in AWS IAM

  • We are going to attach two specific "Managed Policies." This gives the user power over only the tools you are currently using.

  • Permission options: Select Attach policies directly.

  • Permissions policies: In the search box, type and check the box for these two policies:

    • AmazonEC2ContainerRegistryPowerUser (This allows reading/writing images to the registry, but prevents deleting the registry itself.)

    • AWSAppRunnerFullAccess (This allows the user to create and manage the App Runner service.)

  • Click Next.

  • Review and click Create user.

Grant Permissions

  • Now that the user exists, you need to generate the "password" (Keys) for your terminal.

  • You should be back on the "Users" list. Click on the name of the user you just created (petclinic-deployer).

  • Click the Security credentials tab.

  • Scroll down to the Access keys section and click Create access key.

  • For the UseCase , select Command Line Interface (CLI).

  • Scroll down, check the "I understand..." confirmation box, and click Next.

  • (Optional) Set a description tag like "Laptop CLI", then click Create access key.

Create Access Keys

  • You will see an Access Key ID (starts with AKIA...) and a Secret Access Key (a long string of random characters).

  • Download the .csv file.

  • This is the only time AWS will ever show you the Secret Key. If you close this window without saving it, you have to delete the key and start over.

Create Access Keys

  • In the AWS Management Console, search for "ECR" or "Elastic Container Registry".
  • Click Create repository.

  • Give the repository a name, like petclinic.

  • Click Create.

  • Click the radio button next to the repository's name.
  • Click the View push commands button in the top right.

  • AWS provides 4 commands you need to run. We'll do that in a later slide.

Create Repository

  • Go back to your terminal and run this command:
    aws configure

  • It will ask you four questions. Copy/paste the answers from the CSV file you downloaded.

    • AWS Access Key ID: [Paste the AKIA... string]

    • AWS Secret Access Key: [Paste the secret string]

    • Default region name: Look at your ECR push command from before. If it said ecr.us-east-1.amazonaws.com, type us-east-1. If it said us-east-2 (Ohio) or us-west-2 (Oregon), match that.

    • Default output format: json

  • Run this command. If it replies with an "Arn" that ends in user/project-name-deployer, you are authenticated safely.
    aws sts get-caller-identity

Configure Your Computer

  • Run those 4 commands in your terminal (PowerShell).
    • Command 1: Logs your local Docker client into AWS.

    • Command 2: Builds the image (you can skip this since you already built it).

    • Command 3: Tags your existing image.

    • Command 4: Pushes the image to AWS.

  • Before running commands 3 and4, run this command and use the name of your image.
    docker images

  • After running command 3, run the docker images command again to verify that both images share the same ID.

  • After running command 4, go back to the AWS Managment Console, close the push commands modal, and click the copy URI button.

Create Repository

  • In the AWS Management Console, search for "App Runner" in the top search bar.
  • Click Create an App Runner service.

  • Select "Container registry" for the repository type.

  • Select "Amazon ECR" as the provider (the Private one, not Public).

  • Paste your container image URI or click the Browse button to select your image repository and tag.

  • Select "Create new service role". It will likely suggest a default name like AppRunnerECRAccessRole. You can leave this as is.

  • Click Next.

Create Service

  • Enter "petclinic-service" for the Service name.
  • 1 vCPU and 2 GB Memory is a safe choice for Spring Boot. It balances cost with the assurance that your app will actually start up without crashing or timing out. Read more on the next slides.

  • Environment variables: Click "Add environment variable" for each of your secrets:

    • MYSQL_URL : jdbc:mysql://...

    • MYSQL_USER : ...

    • MYSQL_PASS : ...

  • Enter "8080" for the port number for Spring Boot projects.

  • Click Next.
  • Click "Create & deploy" at the end of the Review & Create page.
  • It will take several minutes to provision the infrastructure, pull your image, and go live. Be patient.

Configure Service

  • Think of this as the "horsepower" of your engine.

  • It determines how fast your code executes, and especially for Spring Boot, it determines how fast your app starts.

  • 0.25 vCPU: Too weak. Spring Boot does a lot of complex work immediately when it turns on (scanning classes, building beans). If the CPU is this small, the startup might take so long that AWS thinks the app is "frozen" and kills it before it finishes loading.

  • 1 vCPU: The Sweet Spot. This provides enough power to start the application quickly (usually under 30 seconds) and handle normal web traffic smoothly.

  • 2 or 4 vCPU: Overkill. Unless you have hundreds of users hitting this specific demo app simultaneously, you are just paying for power you won't use.

Virtual CPU (vCPU)

  • Think of this as the "desk space" your app has to work on.

  • The Java Virtual Machine (JVM) heap requires a baseline amount of memory just to exist.

  • 2 GB: Recommended. This gives the Java Virtual Machine (JVM) plenty of room.

  • 3 GB / 4 GB: Unnecessary for this specific app.

  • You could try 1 GB if available, but Java apps in containers commonly have "Out of Memory" errors. If the app tries to grab 1.1 GB of RAM and the limit is 1 GB, the container vanishes instantly. 2 GB buys you safety.

Virtual Memory (GB)

  • AWS App Runner charges you based on these settings per second.

  • You pay for the memory (~$0.007/GB-hour) while the app is idle.

  • You pay for the vCPU only when the app is actively processing a request or starting up.

  • By choosing 1 vCPU / 2 GB, you are choosing a configuration that costs roughly $0.015 - $0.02 per hour (approx. $11-15/month) if left running 24/7.

Cost Implication

  • After deployment, the status changes to "Running":
  • Click the "Default domain" awsapprunner.com URL to test your app.

  • View the "Logs" tab in the App Runner dashboard.

    • Look for "Application logs" to see your Spring Boot startup output.

    • Unlike Azure's live stream command, AWS logs here might have a slight delay.

Live URL and Logs

  • Unlike Azure Container Apps or Google Cloud Run, AWS App Runner does not "scale to zero". It keeps at least one instance running to ensure instant responses.
  • The Good: No "cold starts" (no 20-second delay on the first request).
  • The Bad: It burns through your free tier hours faster if you leave it running 24/7.
  • You must remember to "Pause" or "Delete" the service when you are done testing.

  • To Stop Billing, go to the App Runner service list, select your service, and click Actions -> Pause. This keeps your configuration but stops the hourly charge.

  • To Destroy, click Actions -> Delete.

Cleanup (Money Saving Tip)

  • From the App Runner Service, click the "Custom domains" tab.
  • Click the "Link Domain" button.
  • You can register a domain through Amazon Route 53 or your own registrar.
  • Select "Non-Amazon" and enter the domain.
  • Click "Link Domain".

AWS Domain

  • I registered a domain using SiteGround.
  • Log in to SiteGround -> Websites -> Site Tools for your domain.

  • Go to Domain -> DNS Zone Editor. Add two records.

  • Delete any existing CNAME records.

  • Configure certificate validation. Create the two given CNAME records.

    • Name: Enter everything provided except .domain.com.

    • Value: Paste the values from AWS.

  • Configure DNS target. The the one given CNAME record.

    • Name: If using the root domain, leave it Empty (or type @).

      If using a subdomain (www), type www

    • Value: Paste the value from AWS.

SiteGround Registration

  • Go back to your AWS Management Console.
  • Wait for status to become 'Active'. It can take 24-48 hours after adding the records for the status to change.

  • AWSwill issue a free SSL certificate for your SiteGround domain.

  • A secure lock icon will appear when you visit the domain.

  • Follow the steps from before to redirect the domain to www

Finish and Secure in Azure

Plan an ER Diagram

Miro Board

  • I prompted Miro's AI with this:
    I need the following tables: users, user_roles, roles, role_permissions, permissions, schools, locations, leagues, events, teams, team_users, waivers, user_waivers, messages, message_reactions, subscription, school_subscriptions, fee_payments, matches, standings. Users and roles are joined through user_roles. Roles and permissions are joined though role_permissions. Locations are joined to schools. Team_users are joined to teams. Teams are joined to leagues. Events are joined to leagues. Leagues are joined to schools and locations. User_waivers are joined to users and waivers. Messages are joined to users. Message_reactions are joined to messages. School_subscriptions are joined to schools and subscriptions. Fee_payments are joined to team_users. Matches are joined to events. Standings are joined to leagues and teams.

  • This was the result.

  • Here are some quick edits.

Miro AI

  • Step 5: Refine and validate your diagram

    • Rearrange, duplicate, or remove items as needed.

  • Step 6: Document and share

    • When your ER diagram is ready, invite teammates to review, comment, or co-edit in real time. Record a Talktrack to walk stakeholders through your logic asynchronously and keep everyone on the same page. When it’s time to distribute, export your diagram as an image or PDF.

Miro Board

SQL Create Table Statements

DROP TABLE IF EXISTS users;
CREATE TABLE users (
    id INT AUTO_INCREMENT PRIMARY KEY,
    first_name VARCHAR(50),
    last_name VARCHAR(50),
    nickname VARCHAR(50),
    nickname_is_flagged TINYINT DEFAULT 0,
    email VARCHAR(255) NOT NULL,
    public_email TINYINT DEFAULT 0,
    phone VARCHAR(255),
    public_phone TINYINT DEFAULT 0,
    password_hash VARCHAR(255),
    created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
    updated_at DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
    deleted_at DATETIME,
    
    -- Performance: Unique index for fast login
    UNIQUE INDEX idx_users_email (email),
    -- Performance: Index for searching people by name
    INDEX idx_users_name (last_name, first_name)
);
  • In week 3, I changed the schools and locations tables

2027

  • In the pet clinic project, owners have pets.
  • In my demo, schools have locations.

More Create Table Statements

DROP TABLE IF EXISTS schools;
CREATE TABLE schools (
    id INT AUTO_INCREMENT PRIMARY KEY,
    name VARCHAR(255) NOT NULL,
    domain VARCHAR(255) NOT NULL,
    status_id ENUM('active', 'inactive', 'suspended') DEFAULT 'active',
    created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
    updated_at DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,

    -- Integrity: No two schools can share the same domain
    UNIQUE INDEX idx_schools_domain (domain)
);

DROP TABLE IF EXISTS locations;
CREATE TABLE locations (
    id INT AUTO_INCREMENT PRIMARY KEY,
    school_id INT NOT NULL,
    parent_location_id INT NULL, -- Self-referencing FK (e.g., Court 1 inside Gym A)
    name VARCHAR(255) NOT NULL,
    description TEXT,
    address VARCHAR(255),
    latitude DECIMAL(8,4),
    longitude DECIMAL(8,4),
    status_id ENUM('draft', 'active', 'closed', 'coming_soon') DEFAULT 'active',
    created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
    updated_at DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,

    CONSTRAINT fk_locations_school FOREIGN KEY (school_id) REFERENCES schools(id) ON DELETE CASCADE,
    CONSTRAINT fk_locations_parent FOREIGN KEY (parent_location_id) REFERENCES locations(id) ON DELETE SET NULL
);
  • Open src/main/resources/templates/
  • Thymeleaf is a modern server-side Java template engine. In the context of Spring Boot, it is the technology used to render the user interface (the "View" in MVC).
  • Unlike JSP, that often required custom tags, Thymeleaf uses Natural Templating.

    • This means the template files are valid HTML files that can be opened and viewed in a browser even without the server running.

  • Thymeleaf works by looking for special attributes (prefixed with th:) in your HTML tags and replacing the static content with dynamic data from your Java application.

Pet Clinic HTML

  • fragments/layout.html is the most critical file for understanding the UI structure. It acts as the "skeleton" for the entire application.

  • th:fragment="layout (template, menu)": This defines the file as a reusable layout that accepts two arguments: the specific page content (template) and the active menu item (menu).

  • Structure: It contains the standard HTML head (CSS/JS imports) and the navbar (navigation menu) that appears on every page.

  • Dynamic Insertion: The line <th:block th:insert="${template}" /> is the placeholder where the specific content of other pages (like the Welcome page or Owner form) will be injected.

The Master Layout

  • The following pages define the specific content for a single view.

  • They do not contain the full HTML structure (like <html>, <head>, or <body> setup) themselves. Instead, they "wrap" themselves in the layout.

  • welcome.html Layout implementation:

    • It uses th:replace="~{fragments/layout :: layout (~{::body},'home')}". This tells Thymeleaf: "Take the layout fragment from fragments/layout.html, pass in my <body> tag as the content, and highlight the 'home' menu item.".

  • welcome.html Internationalization:

    • th:text="#{welcome}" replaces the text "Welcome" with a value from your messages.properties file (in resources/messages/), supporting multiple languages.

The Content Pages

  • The .html pages define the specific content for a single view.

  • They do not contain the full HTML structure (like <html>, <head>, or <body> setup) themselves. Instead, they "wrap" themselves in the layout.

  • welcome.html Layout implementation:

    • It uses th:replace="~{fragments/layout :: layout (~{::body},'home')}". This tells Thymeleaf: "Take the layout fragment from fragments/layout.html, pass in my <body> tag as the content, and highlight the 'home' menu item.".

  • welcome.html Internationalization:

    • th:text="#{welcome}" replaces the text "Welcome" with a value from your messages.properties file (in resources/messages/), supporting multiple languages.

The Home Page

  • The messages.properties files are part of the Internationalization (i18n) system, which allows the application to be displayed in different languages without changing the code.

  • These files act as a dictionary that maps a "key" (used in your HTML) to a specific text string (displayed to the user).

  • messages/messages.properties: This is the default file. It contains the English text. For example, the key welcome is mapped to "Welcome".

  • messages/messages_es.properties: This is the Spanish language file (indicated by the _es suffix). It maps the same keys to Spanish text. For example, the same welcome key is mapped to "Bienvenido".

Localization

  • To switch to Spanish: Simply add ?lang=es to the end of your URL in the browser.

  • The logic for switching languages is defined in src/main/java/org/springframework/samples/petclinic
    /system/WebConfiguration.java.

  • This configuration sets up a LocaleChangeInterceptor with the parameter name "lang". This means the application is watching the URL for that specific query parameter.

  • This configuration uses a SessionLocaleResolver. Once you switch languages, the application will remember your choice for your entire browsing session (until you close the browser).

How to change the language

  • owners/createOrUpdateOwnerForm.html Form Binding:

    • The attribute th:object="${owner}" binds this HTML form to the Owner Java object passed from the Controller. 

    • Instead of writing raw <input> tags, it uses th:replace="~{fragments/inputField :: input ...}". This calls a reusable fragment (defining labels, error handling, and styling) to keep the form code clean and consistent.

Create/Update Form

  • Key Thymeleaf Attributes You Will See

    • th:text: Replaces the body of the tag with a string or variable.

    • th:href: Creates dynamic URLs (e.g., @{/resources/css/petclinic.css} ensures the correct path context).

    • th:each: Loops over a list (used in vetList.html to generate table rows).

    • th:if / th:unless: Conditionally renders elements.

    • th:replace / th:insert: Injects code from one template into another.

Thymeleaf Attributes

  • src/main/scss/ contains the source code for your application's styling. It uses Sass (Syntactically Awesome Style Sheets), which is a "pre-processor" language that adds programming features like variables and imports to standard CSS.

  • petclinic.scss: This is the "master" file that bundles the other files together. It imports the Bootstrap framework and defines the application's color palette (e.g., $spring-green)

  • header.scss: Contains specific styles for the navigation bar and the Spring logo branding.

  • typography.scss: Defines the custom fonts used in the application ("Varela Round" and "Montserrat").

  • responsive.scss: Handles "media queries" to ensure the site looks good on mobile devices (e.g., adjusting the navbar).

Sass Files

  • src/main/resources/static/resources is the "public" folder. Anything inside static is served directly to the web browser. The browser cannot read SCSS files, so it reads the standard CSS and assets found here.

  • css/petclinic.css: This is the compiled result. The build process takes all the files from the scss folder, merges them with Bootstrap code, and outputs this single, standard CSS file that the browser understands.

    • DO NOT edit this file. Only edit the .scss files.

  • images/ & fonts/: These are static assets (logos, icons, font files) referenced by the CSS. For example, header.scss refers to ../images/spring-logo-dataflow.png, which physically exists in this static images folder.

Static Files

  • The connection between Sass and CSS happens in your pom.xml file. It includes a plugin called libsass-maven-plugin.

  • When you run the application, this plugin:

    • Reads the files in src/main/scss/.

    • Combines them with the Bootstrap library (downloaded via WebJars).

    • "Compiles" them into standard CSS.

    • Saves the result to resources/css/petclinic.css.

  • If you want to change the look of the site (e.g., change the green color to blue), you should edit the files in src/main/scss/. If you edit the file in static/resources/css/ directly, your changes will be overwritten the next time the project is built.

CSS Build Process

Pet Clinic Packages

  • These classes define the relationships between the data. The hierarchy creates a clear chain of ownership: Owner > Pet > Visit.
  • Owner.java: The root entity representing a pet owner. This class extends the Person class from the model package.

  • Pet.java: Represents a specific animal belonging to an owner. This class extends the NamedEntity class from the model package.

  • Note how both Person and NamedEntity extend BaseEntity. This class defines the id field. 

    • Note how the owners database table uses id, not owner_id.

  • PetType.java: A simple lookup entity defining the species (e.g., "Cat", "Dog", "Hamster", etc.).

  • Visit.java: Represents a medical appointment. It contains a date (defaulting to "now") and a description.

 Domain Entities (Data Model)

  • Owner.java:

    • Relationships: Has a One-to-Many relationship with Pet. It manages this list with methods like addPet() and getPet().

    • Validation: Includes constraints like @Pattern(regexp = "\\d{10}") to ensure telephone numbers are exactly 10 digits.

  • Pet.java:

    • Relationships:

      • One-to-Many with Visit (a pet can have multiple doctor visits).
      • Many-to-One with PetType (a pet belongs to one species).

    • Logic: It automatically sorts visits by date using @OrderBy("date ASC").

Relationships, Validation, Logic

  • OwnerRepository.java: The primary way to access owner data.

    • Key Feature: It extends JpaRepository and adds a custom search method: findByLastNameStartingWith, which supports the "Find Owner" search bar feature.

  • PetTypeRepository.java: Provides a list of all valid animal types (findPetTypes), ordered by name, which is used to populate dropdown menus in the UI.

Repository Layer (The Database Access)

  • OwnerController.java: Manages the high-level Owner operations.

    • Routes: Handles listing owners (/owners), searching (/owners/find), and creating/editing owner profiles (/owners/new, /owners/{ownerId}/edit).

  • Note the use of @GetMapping for GET requests and @PostMapping for POST requests.

  • One bug I discovered is

Controller Layer (The HTTP Request Handling)

  • PetController.java: Manages the "sub-resource" of adding or editing a pet for a specific owner.

    • Routes: It operates under the /owners/{ownerId} path. For example, creating a new pet is handled at /owners/{ownerId}/pets/new.

  • VisitController.java: Manages the creation of new visits.

    • Routes: This is deeply nested: /owners/{ownerId}/pets/{petId}/visits/new. It ensures that before a visit is created, the system validates that the Pet and Owner actually exist.

Controller Layer (The HTTP Request Handling)

  • PetValidator.java: A custom validator that ensures a Pet has a name, a birthdate, and a type. It is manually applied in the PetController.

  • PetTypeFormatter.java: Helps Spring MVC convert the string "Cat" from a web form into the actual PetType Java object, and vice versa.

Support Classes (Validation & Formatting)

  • It is possible to replace Thymeleaf with a frontend JavaScript framework like React, Angular, or Vue.

  • This involves shifting your application from a Server-Side Rendered (SSR) architecture to a Client-Side Rendered (CSR) or Single Page Application (SPA) architecture.

  • Here is how that transition works:

    • Thymeleaf: The Spring Boot server generates the full HTML page. It merges the data (e.g., a list of Vets) with the template (vetList.html) and sends the finished HTML to the browser.

    • React/Angular/Vue: The Spring Boot server becomes a REST API. It sends only the raw data (usually in JSON format). The JavaScript framework running in the browser receives that JSON and builds the HTML dynamically.

Frontend JavaScript Frameworks

  • A benefit of the layered architecture (Controller > Service > Repository) is that you do not need to change your Database or Repository layers—you only need to modify the Controller layer.

  • If you look at VetController.java, the application already has an endpoint ready for a JavaScript framework to use:

Frontend JavaScript Frameworks

// This method is for Thymeleaf (Returns a View)
@GetMapping("/vets.html")
public String showVetList(...) { ... }

// This method is for External Clients/JS Frameworks (Returns JSON/XML)
@GetMapping({ "/vets" })
public @ResponseBody Vets showResourcesVetList() {
    Vets vets = new Vets();
    vets.getVetList().addAll(this.vetRepository.findAll());
    return vets;
}
  • The @ResponseBody annotation tells Spring: "Do not look for a Thymeleaf template. Just take this Java object, convert it to JSON, and send it back."

  • A React or Angular app would make a fetch('/vets') call to this URL, receive the list of doctors, and render the table itself.

  • To fully replace Thymeleaf, you would:

  • Update Controllers: Change your @Controller classes to @RestController (which automatically applies @ResponseBody to every method).

  • Return Data, Not Strings: Instead of returning strings like "owners/createOrUpdateOwnerForm", your methods would return Owner objects or ResponseEntity objects.

  • Delete Templates: You would eventually delete the src/main/resources/templates folder since the Java app no longer generates HTML.

  • Frontend Build: You would build your React/Angular app separately. You can then either run it on a separate server (like Node.js) that talks to your Spring Boot API, or package the built JavaScript files into the Spring Boot static folder to serve them together.

Frontend JavaScript Frameworks

  • the src/test folder contains the automated tests that ensure the application works correctly. They are organized to test different "layers" of the application, from simple logic to full database integrations.

  • These files test start up the full Spring Boot application context to verify that all components work together.

  • PetClinicIntegrationTests.java: The main integration test suite that checks standard application flows.

  • MySqlIntegrationTests.java and PostgresIntegrationTests.java: These are specialized tests that verify the application works correctly when connected to real MySQL or PostgreSQL databases (often using Testcontainers) instead of the default in-memory H2 database.

Integration Tests

  • Located in subpackages like owner and vet, these tests use @WebMvcTest. They focus only on the URL routing and HTML generation. They "mock" (fake) the service layer so they can test the UI without needing a real database.

  • OwnerControllerTests.java: Verifies that URLs like /owners/new return the correct view and that form submissions work.

Controller Tests (The Web Layer)

  • These test individual classes in isolation.

  • PetValidatorTests.java: Checks specifically that the PetValidator correctly identifies invalid inputs (like a missing name).

  • VetTests.java: Tests the internal logic of the Vet entity, such as serialization.

    • Serialization is the process of converting an object's state into a sequence of bytes, which can then be saved to a file, stored in a database, or transmitted over a network.

    • The reverse process, converting the byte stream back into a live object, is called deserialization.

  • PetTypeFormatterTests.java: Ensures the string-to-object conversion for Pet Types functions as expected.

Unit Tests (Specific Logic)

  • CrashControllerTests.java: specifically tests that the "Error" page triggers correctly.

  • I18nPropertiesSyncTest.java: A quality assurance test that ensures your translation files (like Spanish and English) stay in sync and don't have missing keys.

  • EntityUtils.java: Not a test itself, but a helper class used by other tests to look up objects in collections.

System & Utility Tests

  • This is the Maven configuration file. It tells Java exactly which third-party libraries (dependencies) your project needs to run.

  • Parent POM: You will see a <parent> tag for spring-boot-starter-parent. This is the "magic" that manages versions for you. You don't need to specify the version for every single library because the parent knows which versions work together.

  • Starters: Notice dependencies like spring-boot-starter-web, spring-boot-starter-data-jpa, and spring-boot-starter-thymeleaf. These are pre-packaged bundles. For example, starter-web automatically gives you Tomcat (server), Spring MVC (framework), and Jackson (JSON parser) in one go.

  • Java Version: It explicitly sets the Java version to 17 (<java.version>17</java.version>).

pom.xml

  • Located in src/main/resources/, this is where you configure the application's behavior without changing Java code.

  • Database Settings: The line database=h2 tells the app to use the H2 in-memory database by default. This is why you don't need to install MySQL to run the demo; the database is created in RAM when you start the app and destroyed when you stop it.

  • Database Initialization: It points to schema.sql (to create tables) and data.sql (to load dummy data).

  • Logging: You can control how much info appears in the console. For example, logging.level.org.springframework=INFO.

application.properties

  • This is the file with the public static void main method that launches the app.

  • @SpringBootApplication: This single annotation actually triggers three powerful features:

    1. Configuration: It marks the class as a source of bean definitions.

    2. EnableAutoConfiguration: It tells Spring Boot to configure your app based on the libraries in your pom.xml 

    3. ComponentScan: It tells Spring to scan the current package (and all sub-packages) for your Controllers, Services, and Repositories.

PetClinicApplication.java

  • While application.properties is good for simple key-value pairs, complex configuration is done in Java classes marked with @Configuration.

    1. @EnableCaching: This annotation turns on Spring's caching ability.

    2. @Bean: The method petclinicCacheConfigurationCustomizer() is marked with @Bean. This tells Spring: "Execute this method and manage the object it returns.". It is used here to set up a specific cache for "vets" so the database doesn't have to be queried every time the user refreshes the veterinarian list.

CacheConfiguration.java

  • The term "Bean" is used in two distinct ways in this project. It can be confusing because they share the same name, but they serve different purposes.

What is a Java Bean?

  • In the comments of files like Vet.java and Owner.java, you will see them described as a "Simple JavaBean domain object".

  • A standard JavaBean is simply a Java class that follows a specific set of naming conventions so other tools can easily use it.

  • Private Properties: Data fields are hidden (e.g., private String address; in Owner.java).

  • Public Getters/Setters: The data is accessed via public methods named get... and set... (e.g., getAddress() and setAddress()).

  • In your project: Classes like Vet, Owner, Pet, and Visit are standard JavaBeans. They are just containers for your data.

  • No-Argument Constructor: It usually has a default constructor so frameworks can create an empty instance of it easily.

  • JavaBean is a pattern for writing data classes (like Vet or Owner).

 The "JavaBean" (Data Standard)

  • In CacheConfiguration.java and WebConfiguration.java, you saw the @Bean annotation.

  • A Spring Bean is an object that is created, configured, and managed by the Spring Framework itself (the "Spring Container").

  • Managed Lifecycle: You do not create these objects with new (e.g., new VetController()). Instead, Spring creates them for you at startup.

  • Dependency Injection: Spring "injects" these beans into other beans that need them. For example, VetRepository is a Spring Bean that is automatically injected into the VetController.

  • Spring Bean is a specific object living in the Spring system (like VetController or the CacheManager).

The "Spring Bean" (Managed Component)

  • GitHub Actions works as your "robot butler." Instead of you manually running gradle build, docker build, and docker push on your laptop, GitHub does it automatically every time you push code.

    Here is the workflow:

  • You push code to your GitHub repository.

  • GitHub Actions wakes up, spins up a temporary server (runner).

  • It builds your jar using Gradle.

  • It logs in to Docker Hub (using secrets you provide).

  • It builds and pushes your Docker image.

GitHub Actions

  • You should never put your Docker Hub password directly in the file. You use GitHub Secrets.

  • Go to your GitHub repository.

  • Click Settings -> Secrets and variables -> Actions.

  • Click New repository secret.

  • Add these two:

    • Name: DOCKERHUB_USERNAME

      • Value: your_actual_username

    • Name: DOCKERHUB_TOKEN

      • Value: (Go to Docker Hub -> Account Settings -> Security -> New Access Token. Use this token instead of your real password.)

Set up Secrets

  • In your project, create this directory structure and file: .github/workflows/deploy.yml

  • Paste this content in. I have customized it for Gradle and Java 17.

Create the Workflow File

name: Build and Push Docker Image

on:
  push:
    branches: [ "main" ]

jobs:
  build-and-push:
    runs-on: ubuntu-latest

    steps:
    # 1. Download your code
    - name: Checkout code
      uses: actions/checkout@v4

    # 2. Set up Java 17 (Same as your local environment)
    - name: Set up JDK 17
      uses: actions/setup-java@v4
      with:
        java-version: '17'
        distribution: 'temurin' # or 'microsoft' if you prefer

    # 3. Build the JAR file with Gradle
    # We skip tests here to speed it up, but you can remove '-x test' to be safer
    - name: Build with Gradle
      run: ./gradlew clean bootJar -x test -x jar

    # 4. Set up Docker Buildx (required for modern Docker builds)
    - name: Set up Docker Buildx
      uses: docker/setup-buildx-action@v3

    # 5. Log in to Docker Hub
    - name: Log in to Docker Hub
      uses: docker/login-action@v3
      with:
        username: ${{ secrets.DOCKERHUB_USERNAME }}
        password: ${{ secrets.DOCKERHUB_TOKEN }}

    # 6. Build and Push the Docker image
    - name: Build and push
      uses: docker/build-push-action@v5
      with:
        context: .
        push: true
        tags: ${{ secrets.DOCKERHUB_USERNAME }}/petclinic:latest
  • Commit this file and push it to GitHub.
    git add .
    git commit -m "Add GitHub Action workflow"
    git push

  • Go to the "Actions" tab in your GitHub repository. You will see the workflow running. When it turns green, check Docker Hub—your image will have been freshly updated!

Trigger it

  • Right now, GitHub pushes the image, but Azure doesn't know about it yet. You have two choices:

  • Continuous Deployment (CD) in Azure:

    • Go to your Azure Container App in the portal.

    • Under Application -> Revision management (or Continuous deployment), you can enable a setting that says "Create a new revision when a new image is pushed."

    • Note: This often requires a webhook setup.

  • Add an Azure Step to the YAML:

    • You can add a final step to the GitHub Action that runs az containerapp update to tell Azure to pull the new image immediately.

  • Would you like the code snippet to force Azure to update at the end of this workflow?

How does Azure know to update?

  • X

More Create Table Statements

DROP TABLE IF EXISTS leagues;
CREATE TABLE leagues (
    id INT AUTO_INCREMENT PRIMARY KEY,
    school_id INT NOT NULL,
    location_id INT, -- Default location
    user_id INT, -- League Manager/Contact
    name VARCHAR(255) NOT NULL,
    description TEXT,
    registration_start DATETIME,
    registration_end DATETIME,
    league_start DATETIME,
    league_end DATETIME,
    is_public TINYINT DEFAULT 1,
    type ENUM('male', 'female', 'coed') NOT NULL,
    capacity INT,
    capacity_type ENUM('team', 'individual') NOT NULL,
    fee DECIMAL(6,2),
    status_id ENUM('draft', 'active', 'inactive', 'postponed', 'cancelled', 'past') DEFAULT 'draft',
    created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
    updated_at DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,

    CONSTRAINT fk_leagues_school FOREIGN KEY (school_id) REFERENCES schools(id) ON DELETE CASCADE,
    CONSTRAINT fk_leagues_location FOREIGN KEY (location_id) REFERENCES locations(id) ON DELETE SET NULL,
    CONSTRAINT fk_leagues_manager FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE SET NULL,

    -- Performance: Quickly find active leagues for a specific school
    INDEX idx_leagues_school_status (school_id, status_id)
);

DROP TABLE IF EXISTS teams;
CREATE TABLE teams (
    id INT AUTO_INCREMENT PRIMARY KEY,
    league_id INT NOT NULL,
    captain_user_id INT NOT NULL,
    name VARCHAR(255) NOT NULL,
    logo_url VARCHAR(255),
    status_id ENUM('active', 'inactive', 'suspended') DEFAULT 'active',
    created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
    updated_at DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,

    CONSTRAINT fk_teams_league FOREIGN KEY (league_id) REFERENCES leagues(id) ON DELETE CASCADE,
    CONSTRAINT fk_teams_captain FOREIGN KEY (captain_user_id) REFERENCES users(id) ON DELETE CASCADE,

    -- Performance: List all teams in a league
    INDEX idx_teams_league (league_id)
);

DROP TABLE IF EXISTS team_users;
CREATE TABLE team_users (
    id INT AUTO_INCREMENT PRIMARY KEY,
    team_id INT NOT NULL,
    user_id INT NOT NULL,
    role ENUM('member', 'captain') DEFAULT 'member',
    created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
    updated_at DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,

    CONSTRAINT fk_team_users_team FOREIGN KEY (team_id) REFERENCES teams(id) ON DELETE CASCADE,
    CONSTRAINT fk_team_users_user FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE,

    -- Integrity: A user cannot join the same team twice
    UNIQUE INDEX idx_team_users_unique (team_id, user_id)
);
  • X

More Create Table Statements

DROP TABLE IF EXISTS events;
CREATE TABLE events (
    id INT AUTO_INCREMENT PRIMARY KEY,
    league_id INT NOT NULL,
    location_id INT,
    user_id INT, -- Event organizer/referee
    name VARCHAR(255) NOT NULL,
    description TEXT,
    event_start DATETIME,
    event_end DATETIME,
    status_id ENUM('draft', 'active', 'ongoing', 'postponed', 'cancelled', 'final') DEFAULT 'draft',
    created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
    updated_at DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,

    CONSTRAINT fk_events_league FOREIGN KEY (league_id) REFERENCES leagues(id) ON DELETE CASCADE,
    CONSTRAINT fk_events_location FOREIGN KEY (location_id) REFERENCES locations(id) ON DELETE SET NULL,
    CONSTRAINT fk_events_contact FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE SET NULL,

    -- Integrity & Performance: Check for double-bookings at a location
    INDEX idx_events_location_time (location_id, event_start)
);

DROP TABLE IF EXISTS matches;
CREATE TABLE matches (
    id INT AUTO_INCREMENT PRIMARY KEY,
    event_id INT NOT NULL,
    home_team_id INT,
    away_team_id INT,
    winner_team_id INT,
    home_score INT DEFAULT 0,
    away_score INT DEFAULT 0,
    status ENUM('scheduled', 'in_progress', 'final', 'forfeit') DEFAULT 'scheduled',
    created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
    updated_at DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,

    CONSTRAINT fk_matches_event FOREIGN KEY (event_id) REFERENCES events(id) ON DELETE CASCADE,
    CONSTRAINT fk_matches_home FOREIGN KEY (home_team_id) REFERENCES teams(id) ON DELETE SET NULL,
    CONSTRAINT fk_matches_away FOREIGN KEY (away_team_id) REFERENCES teams(id) ON DELETE SET NULL,
    CONSTRAINT fk_matches_winner FOREIGN KEY (winner_team_id) REFERENCES teams(id) ON DELETE SET NULL,

    -- Performance: Essential for calculating standings (W/L records)
    INDEX idx_matches_home (home_team_id),
    INDEX idx_matches_away (away_team_id)
);
  • X

More Create Table Statements

DROP TABLE IF EXISTS messages;
CREATE TABLE messages (
    id INT AUTO_INCREMENT PRIMARY KEY,
    user_id_from INT NOT NULL,
    user_id_to INT,
    league_id INT,
    event_id INT,
    parent_message_id INT, -- For threaded replies
    message VARCHAR(255) NOT NULL,
    is_flagged TINYINT DEFAULT 0,
    status_id ENUM('draft', 'active', 'hidden') DEFAULT 'active',
    created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
    updated_at DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,

    CONSTRAINT fk_messages_from FOREIGN KEY (user_id_from) REFERENCES users(id) ON DELETE CASCADE,
    CONSTRAINT fk_messages_to FOREIGN KEY (user_id_to) REFERENCES users(id) ON DELETE SET NULL,
    CONSTRAINT fk_messages_league FOREIGN KEY (league_id) REFERENCES leagues(id) ON DELETE CASCADE,
    CONSTRAINT fk_messages_event FOREIGN KEY (event_id) REFERENCES events(id) ON DELETE CASCADE,
    CONSTRAINT fk_messages_parent FOREIGN KEY (parent_message_id) REFERENCES messages(id) ON DELETE CASCADE,

    -- Performance: Quickly load chat history for a league or event
    INDEX idx_messages_context (league_id, event_id)
);

DROP TABLE IF EXISTS message_reactions;
CREATE TABLE message_reactions (
    id INT AUTO_INCREMENT PRIMARY KEY,
    message_id INT NOT NULL,
    user_id INT NOT NULL,
    reaction ENUM('like', 'dislike', 'love', 'hug', 'sad', 'angry'),
    created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
    
    CONSTRAINT fk_reactions_message FOREIGN KEY (message_id) REFERENCES messages(id) ON DELETE CASCADE,
    CONSTRAINT fk_reactions_user FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE,

    -- Integrity: One reaction per user per message
    UNIQUE INDEX idx_reactions_unique (message_id, user_id)
);

Java 3 - Week 2

By Marc Hauschildt

Java 3 - Week 2

  • 123