From 1aa10aee47ab5ae8b623a0a7e107e9e017c73227 Mon Sep 17 00:00:00 2001 From: seskildsen Date: Wed, 20 May 2026 11:08:03 +0200 Subject: [PATCH 1/7] feat: update Spring Boot from 2.2.5.RELEASE to 4.0.6 --- edu-spring-postgres/backend/Dockerfile | 63 +++++++++----- edu-spring-postgres/backend/entrypoint.sh | 10 ++- edu-spring-postgres/backend/pom.xml | 27 +++--- .../java/com/company/project/Application.java | 10 +-- .../controller/GreetingController.java | 78 +++++++++++++++++ .../project/controllers/HomeController.java | 80 ----------------- .../com/company/project/entity/Greeting.java | 87 ++++++++++--------- ...pository.java => IGreetingRepository.java} | 5 +- .../project/service/GreetingService.java | 45 ++++++++++ .../project/service/IGreetingService.java | 16 ++++ .../src/main/resources/application.properties | 9 +- .../backend/src/main/resources/data.sql | 5 +- .../backend/src/main/resources/schema.sql | 2 +- edu-spring-postgres/compose.yaml | 2 +- 14 files changed, 262 insertions(+), 177 deletions(-) create mode 100755 edu-spring-postgres/backend/src/main/java/com/company/project/controller/GreetingController.java delete mode 100755 edu-spring-postgres/backend/src/main/java/com/company/project/controllers/HomeController.java rename edu-spring-postgres/backend/src/main/java/com/company/project/repository/{GreetingRepository.java => IGreetingRepository.java} (53%) create mode 100644 edu-spring-postgres/backend/src/main/java/com/company/project/service/GreetingService.java create mode 100644 edu-spring-postgres/backend/src/main/java/com/company/project/service/IGreetingService.java diff --git a/edu-spring-postgres/backend/Dockerfile b/edu-spring-postgres/backend/Dockerfile index ce2778a..09f4cdb 100755 --- a/edu-spring-postgres/backend/Dockerfile +++ b/edu-spring-postgres/backend/Dockerfile @@ -1,26 +1,47 @@ # syntax=docker/dockerfile:1.4 -FROM --platform=$BUILDPLATFORM maven:3.9-eclipse-temurin-17 AS builder -WORKDIR /workdir/server -COPY pom.xml /workdir/server/pom.xml -RUN mvn dependency:go-offline - -COPY src /workdir/server/src -RUN mvn install - -FROM builder AS prepare-production -RUN mkdir -p target/dependency -WORKDIR /workdir/server/target/dependency -RUN jar -xf ../*.jar - -FROM eclipse-temurin:17-jre-noble - -EXPOSE 8080 -VOLUME /tmp -ARG DEPENDENCY=/workdir/server/target/dependency -COPY --from=prepare-production ${DEPENDENCY}/BOOT-INF/lib /app/lib -COPY --from=prepare-production ${DEPENDENCY}/META-INF /app/META-INF -COPY --from=prepare-production ${DEPENDENCY}/BOOT-INF/classes /app +# Stage 1: Build +FROM maven:3.9.14-eclipse-temurin-25-alpine AS builder + +RUN apk upgrade --available --no-cache + +WORKDIR /app + +COPY pom.xml . + +RUN mvn --batch-mode --errors --quiet dependency:go-offline + +# Copy the relevant directory +COPY src ./src + +# If check passes, build the jar +RUN mvn --batch-mode --errors --quiet clean package -DskipTests + +# Extract the jar file using an efficient layout +RUN java -Djarmode=tools -jar target/project.jar extract --layers --destination extracted + +# Stage 2: Run the application +FROM eclipse-temurin:25.0.2_10-jre-alpine-3.23 + +LABEL org.opencontainers.image.source=https://github.com/notalib/workshop-containerisation +LABEL org.opencontainers.image.description="Spring Boot example application" + +# Upgrade packages +RUN apk upgrade --available --no-cache + +# Copy the extracted jar contents from the builder container into the working directory in the runtime container +# Every copy step creates a new docker layer +# This allows docker to only pull the changes it really needs +COPY --from=builder /app/extracted/dependencies/ ./ +COPY --from=builder /app/extracted/spring-boot-loader/ ./ +COPY --from=builder /app/extracted/snapshot-dependencies/ ./ +COPY --from=builder /app/extracted/application/ ./ + COPY entrypoint.sh /entrypoint.sh RUN chmod +x /entrypoint.sh + +# Add a non-root user +RUN addgroup --system spring && adduser --system --ingroup spring spring +USER spring:spring + ENTRYPOINT ["/entrypoint.sh"] diff --git a/edu-spring-postgres/backend/entrypoint.sh b/edu-spring-postgres/backend/entrypoint.sh index fa1b61b..b52b01d 100644 --- a/edu-spring-postgres/backend/entrypoint.sh +++ b/edu-spring-postgres/backend/entrypoint.sh @@ -5,4 +5,12 @@ if [ -f /run/secrets/db-password ]; then export POSTGRES_PASSWORD="$(cat /run/secrets/db-password)" fi -exec java -cp "app:app/lib/*" com.company.project.Application +# Run Spring Boot app +# Start the application jar - this is not the uber jar used by the builder +# This jar only contains application code and references to the extracted jar files +# This layout is efficient to start up and AOT cache (and CDS) friendly +# -XX:+UseContainerSupport is on by default in Java 11+ but explicit is better than implicit. +# -XX:MaxRAMPercentage=75.0 tells the JVM to use up to 75% of the container’s available memory for the heap. Leaving the remaining 25% for native memory, Metaspace, and thread stacks is a reasonable default for most applications. +# -XX:+ExitOnOutOfMemoryError makes the JVM exit (and the container restart) rather than limping along in a degraded state when it runs out of memory + +exec java -XX:+UseContainerSupport -XX:MaxRAMPercentage=75.0 -XX:+ExitOnOutOfMemoryError -jar project.jar diff --git a/edu-spring-postgres/backend/pom.xml b/edu-spring-postgres/backend/pom.xml index b0471a1..16357ee 100755 --- a/edu-spring-postgres/backend/pom.xml +++ b/edu-spring-postgres/backend/pom.xml @@ -2,26 +2,23 @@ 4.0.0 - - com.company - project - 0.0.1-SNAPSHOT - jar - - New App - My new SpringBoot app - org.springframework.boot spring-boot-starter-parent - 2.2.5.RELEASE + 4.0.6 + com.company + project + 0.0.1-SNAPSHOT + New App + My new SpringBoot app + jar UTF-8 UTF-8 - 11 + 25 @@ -44,6 +41,7 @@ org.postgresql postgresql + 42.7.11 runtime @@ -60,16 +58,11 @@ org.springframework.boot spring-boot-starter-test test - - - org.junit.vintage - junit-vintage-engine - - + ${project.artifactId} org.springframework.boot diff --git a/edu-spring-postgres/backend/src/main/java/com/company/project/Application.java b/edu-spring-postgres/backend/src/main/java/com/company/project/Application.java index 16b8c45..7a5a05d 100755 --- a/edu-spring-postgres/backend/src/main/java/com/company/project/Application.java +++ b/edu-spring-postgres/backend/src/main/java/com/company/project/Application.java @@ -1,16 +1,12 @@ package com.company.project; import org.springframework.boot.SpringApplication; -import org.springframework.boot.autoconfigure.EnableAutoConfiguration; import org.springframework.boot.autoconfigure.SpringBootApplication; -import org.springframework.context.annotation.ComponentScan; @SpringBootApplication -@EnableAutoConfiguration -@ComponentScan(basePackages = {"com.company.project"}) public class Application { - public static void main(String[] args) { - SpringApplication.run(Application.class, args); - } + public static void main(String[] args) { + SpringApplication.run(Application.class, args); + } } diff --git a/edu-spring-postgres/backend/src/main/java/com/company/project/controller/GreetingController.java b/edu-spring-postgres/backend/src/main/java/com/company/project/controller/GreetingController.java new file mode 100755 index 0000000..0f757d9 --- /dev/null +++ b/edu-spring-postgres/backend/src/main/java/com/company/project/controller/GreetingController.java @@ -0,0 +1,78 @@ +package com.company.project.controller; + +import com.company.project.entity.Greeting; +import com.company.project.service.IGreetingService; +import java.util.UUID; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.http.HttpStatus; +import org.springframework.stereotype.Controller; +import org.springframework.ui.Model; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PathVariable; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.RequestParam; +import org.springframework.web.server.ResponseStatusException; + +@Controller +public class GreetingController { + + private static final Logger log = LoggerFactory.getLogger(GreetingController.class); + + private final IGreetingService iGreetingService; + + @Autowired + public GreetingController(IGreetingService iGreetingService) { + this.iGreetingService = iGreetingService; + } + + @GetMapping("/") + public String showHome(Model model) { + String greetingDocker = "Docker"; + log.info("GET / — loading default greeting with name='{}'", greetingDocker); + Greeting dockerGreeting = iGreetingService.showHome(greetingDocker).orElseThrow(() -> { + log.warn("Greeting with name='{}' not found", greetingDocker); + return new ResponseStatusException(HttpStatus.NOT_FOUND, "Greeting '" + greetingDocker + "' not found"); + }); + log.info("Loaded greeting: '{}'", dockerGreeting.getName()); + model.addAttribute("name", dockerGreeting.getName()); + model.addAttribute("body", "Connected to database!"); + return "greeting-single"; + } + + @GetMapping("/greetings") + public String listGreetings(Model model) { + log.info("GET /greetings — listing all greetings"); + Iterable greetings = iGreetingService.listGreetings(); + model.addAttribute("greetings", greetings); + return "greetings"; + } + + @GetMapping("/greetings/{id}") + public String sayHello(@PathVariable UUID id, Model model) { + log.info("GET /greetings/{} — looking up greeting", id); + Greeting greeting = iGreetingService.sayHello(id) + .orElseThrow(() -> { + log.warn("Greeting with id='{}' not found", id); + return new ResponseStatusException(HttpStatus.NOT_FOUND, "Greeting '" + id + "' not found"); + }); + log.info("Found greeting id='{}' name='{}'", id, greeting.getName()); + model.addAttribute("name", greeting.getName()); + model.addAttribute("body", "Greeting #" + id); + return "greeting-single"; + } + + @GetMapping("/new") + public String newGreetingForm() { + return "new"; + } + + @PostMapping("/greetings") + public String createGreeting(@RequestParam String name) { + log.info("POST /greetings — creating new greeting with name='{}'", name); + Greeting greeting = iGreetingService.createGreeting(name); + log.info("Saved new greeting id='{}' name='{}'", greeting.getId(), greeting.getName()); + return "redirect:/greetings/" + greeting.getId(); + } +} diff --git a/edu-spring-postgres/backend/src/main/java/com/company/project/controllers/HomeController.java b/edu-spring-postgres/backend/src/main/java/com/company/project/controllers/HomeController.java deleted file mode 100755 index 9ae2ba4..0000000 --- a/edu-spring-postgres/backend/src/main/java/com/company/project/controllers/HomeController.java +++ /dev/null @@ -1,80 +0,0 @@ -package com.company.project.controllers; - -import com.company.project.entity.Greeting; -import com.company.project.repository.GreetingRepository; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; -import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.beans.factory.annotation.Value; -import org.springframework.http.HttpStatus; -import org.springframework.stereotype.Controller; -import org.springframework.ui.Model; -import org.springframework.web.bind.annotation.GetMapping; -import org.springframework.web.bind.annotation.PathVariable; -import org.springframework.web.bind.annotation.PostMapping; -import org.springframework.web.bind.annotation.RequestParam; -import org.springframework.web.server.ResponseStatusException; - -@Controller -public class HomeController { - - private static final Logger log = LoggerFactory.getLogger(HomeController.class); - - @Autowired - private GreetingRepository repository; - - @Value("${spring.datasource.url}") - private String datasourceUrl; - - @GetMapping("/") - public String showHome(String name, Model model) { - log.info("GET / — loading default greeting (id=1) from {}", datasourceUrl); - Greeting dockerGreeting = repository.findById(1).orElseGet(() -> { - log.warn("Default greeting (id=1) not found in DB"); - return new Greeting("DB not found!"); - }); - log.info("Loaded greeting: {}", dockerGreeting.getName()); - model = model.addAttribute("name", dockerGreeting.getName()); - model = model.addAttribute("body", "Connected to database: " + datasourceUrl); - return "greeting-single"; - } - - @GetMapping("/greetings") - public String listGreetings(Model model) { - log.info("GET /greetings — listing all greetings"); - Iterable greetings = repository.findAll(); - long count = 0; - for (Greeting g : greetings) count++; - log.info("Returned {} greetings from DB", count); - model.addAttribute("greetings", greetings); - return "greetings"; - } - - @GetMapping("/greetings/{id}") - public String sayHello(@PathVariable int id, Model model) { - log.info("GET /greetings/{} — looking up greeting", id); - Greeting greeting = repository.findById(id) - .orElseThrow(() -> { - log.warn("Greeting {} not found", id); - return new ResponseStatusException(HttpStatus.NOT_FOUND, "Greeting " + id + " not found"); - }); - log.info("Found greeting #{}: {}", id, greeting.getName()); - model.addAttribute("name", greeting.getName()); - model.addAttribute("body", "Greeting #" + id); - return "greeting-single"; - } - - @GetMapping("/new") - public String newGreetingForm() { - return "new"; - } - - @PostMapping("/greetings") - public String createGreeting(@RequestParam String name) { - log.info("POST /greetings — creating new greeting with name='{}'", name); - Greeting saved = repository.save(new Greeting(name)); - log.info("Saved new greeting id={} name='{}'", saved.getId(), saved.getName()); - return "redirect:/greetings/" + saved.getId(); - } - -} diff --git a/edu-spring-postgres/backend/src/main/java/com/company/project/entity/Greeting.java b/edu-spring-postgres/backend/src/main/java/com/company/project/entity/Greeting.java index db4fe3b..5fcc543 100644 --- a/edu-spring-postgres/backend/src/main/java/com/company/project/entity/Greeting.java +++ b/edu-spring-postgres/backend/src/main/java/com/company/project/entity/Greeting.java @@ -1,60 +1,65 @@ package com.company.project.entity; -import javax.persistence.Entity; -import javax.persistence.GeneratedValue; -import javax.persistence.GenerationType; -import javax.persistence.Id; -import javax.persistence.Table; +import jakarta.persistence.Entity; +import jakarta.persistence.GeneratedValue; +import jakarta.persistence.GenerationType; +import jakarta.persistence.Id; +import jakarta.persistence.Table; +import java.util.UUID; @Entity @Table(name = "GREETINGS") public class Greeting { - @Id - @GeneratedValue(strategy = GenerationType.IDENTITY) - private int id; - private String name; + @Id + @GeneratedValue(strategy = GenerationType.UUID) + private UUID id; + private String name; - public Greeting() { - } + public Greeting() { + } - public Greeting(String name) { - this.name = name; - } + public Greeting(String name) { + this.name = name; + } - public Greeting(int id, String name) { - this.id = id; - this.name = name; - } + public Greeting(UUID id, String name) { + this.id = id; + this.name = name; + } - public int getId() { - return id; - } + public UUID getId() { + return id; + } - public void setId(int id) { - this.id = id; - } + public void setId(UUID id) { + this.id = id; + } - public String getName() { - return name; - } + public String getName() { + return name; + } - public void setName(String name) { - this.name = name; - } + public void setName(String name) { + this.name = name; + } - @Override - public boolean equals(Object o) { - if (this == o) return true; - if (o == null || getClass() != o.getClass()) return false; + @Override + public boolean equals(Object o) { + if (this == o) { + return true; + } + if (o == null || getClass() != o.getClass()) { + return false; + } - Greeting greeting = (Greeting) o; + Greeting greeting = (Greeting) o; - return name.equals(greeting.name); - } + return name.equals(greeting.name); + } - @Override - public int hashCode() { - return name.hashCode(); - } + @Override + public int hashCode() { + return name.hashCode(); + } } diff --git a/edu-spring-postgres/backend/src/main/java/com/company/project/repository/GreetingRepository.java b/edu-spring-postgres/backend/src/main/java/com/company/project/repository/IGreetingRepository.java similarity index 53% rename from edu-spring-postgres/backend/src/main/java/com/company/project/repository/GreetingRepository.java rename to edu-spring-postgres/backend/src/main/java/com/company/project/repository/IGreetingRepository.java index 8927337..ea915fb 100644 --- a/edu-spring-postgres/backend/src/main/java/com/company/project/repository/GreetingRepository.java +++ b/edu-spring-postgres/backend/src/main/java/com/company/project/repository/IGreetingRepository.java @@ -1,9 +1,12 @@ package com.company.project.repository; import com.company.project.entity.Greeting; +import java.util.Optional; +import java.util.UUID; import org.springframework.data.repository.CrudRepository; import org.springframework.stereotype.Repository; @Repository -public interface GreetingRepository extends CrudRepository { +public interface IGreetingRepository extends CrudRepository { + Optional findGreetingByName(String name); } diff --git a/edu-spring-postgres/backend/src/main/java/com/company/project/service/GreetingService.java b/edu-spring-postgres/backend/src/main/java/com/company/project/service/GreetingService.java new file mode 100644 index 0000000..c089e04 --- /dev/null +++ b/edu-spring-postgres/backend/src/main/java/com/company/project/service/GreetingService.java @@ -0,0 +1,45 @@ +package com.company.project.service; + +import com.company.project.entity.Greeting; +import com.company.project.repository.IGreetingRepository; +import java.util.Optional; +import java.util.UUID; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Service; + +@Service +public class GreetingService implements IGreetingService { + + private static final Logger log = LoggerFactory.getLogger(GreetingService.class); + + private final IGreetingRepository iGreetingRepository; + + @Autowired + public GreetingService(IGreetingRepository iGreetingRepository) { + this.iGreetingRepository = iGreetingRepository; + } + + public Optional showHome(String name) { + Optional greeting = iGreetingRepository.findGreetingByName(name); + return greeting; + } + + public Iterable listGreetings() { + Iterable greetings = iGreetingRepository.findAll(); + long countedGreetings = iGreetingRepository.count(); + log.info("Returned {} greetings from DB", countedGreetings); + return greetings; + } + + public Optional sayHello(UUID id) { + Optional greeting = iGreetingRepository.findById(id); + return greeting; + } + + public Greeting createGreeting(String name) { + Greeting greeting = iGreetingRepository.save(new Greeting(name)); + return greeting; + } +} diff --git a/edu-spring-postgres/backend/src/main/java/com/company/project/service/IGreetingService.java b/edu-spring-postgres/backend/src/main/java/com/company/project/service/IGreetingService.java new file mode 100644 index 0000000..3f008f1 --- /dev/null +++ b/edu-spring-postgres/backend/src/main/java/com/company/project/service/IGreetingService.java @@ -0,0 +1,16 @@ +package com.company.project.service; + +import com.company.project.entity.Greeting; +import java.util.Optional; +import java.util.UUID; + +public interface IGreetingService { + + Optional showHome(String name); + + Iterable listGreetings(); + + Optional sayHello(UUID id); + + Greeting createGreeting(String name); +} \ No newline at end of file diff --git a/edu-spring-postgres/backend/src/main/resources/application.properties b/edu-spring-postgres/backend/src/main/resources/application.properties index db6b627..66b692b 100755 --- a/edu-spring-postgres/backend/src/main/resources/application.properties +++ b/edu-spring-postgres/backend/src/main/resources/application.properties @@ -1,7 +1,5 @@ -spring.jpa.properties.hibernate.dialect = org.hibernate.dialect.PostgreSQLDialect spring.jpa.properties.hibernate.format_sql=true spring.jpa.hibernate.ddl-auto=none -spring.jpa.hibernate.show-sql=true logging.level.com.company.project=DEBUG logging.level.org.hibernate.SQL=DEBUG @@ -11,10 +9,9 @@ logging.level.org.springframework.jdbc.datasource.init=INFO spring.datasource.url=jdbc:postgresql://db:5432/${POSTGRES_DB} spring.datasource.username=postgres spring.datasource.password=${POSTGRES_PASSWORD:db-wrz2z} -spring.datasource.initialization-mode=always -spring.datasource.initialize=true -spring.datasource.schema=classpath:/schema.sql -spring.datasource.continue-on-error=true +spring.sql.init.mode=always +spring.sql.init.schema-locations=classpath:/schema.sql +spring.sql.init.continue-on-error=true spring.datasource.hikari.connection-timeout=5000 spring.datasource.hikari.initialization-fail-timeout=-1 diff --git a/edu-spring-postgres/backend/src/main/resources/data.sql b/edu-spring-postgres/backend/src/main/resources/data.sql index aeba1be..1fd1261 100644 --- a/edu-spring-postgres/backend/src/main/resources/data.sql +++ b/edu-spring-postgres/backend/src/main/resources/data.sql @@ -1 +1,4 @@ -INSERT INTO GREETINGS(name) VALUES ('Docker'), ('Workshop'), ('The Future') ON CONFLICT (name) DO NOTHING; +INSERT INTO GREETINGS(id, name) +VALUES (gen_random_uuid(), 'Docker'), + (gen_random_uuid(), 'Workshop'), + (gen_random_uuid(), 'The Future') ON CONFLICT (name) DO NOTHING; diff --git a/edu-spring-postgres/backend/src/main/resources/schema.sql b/edu-spring-postgres/backend/src/main/resources/schema.sql index 195738e..4e70fc7 100644 --- a/edu-spring-postgres/backend/src/main/resources/schema.sql +++ b/edu-spring-postgres/backend/src/main/resources/schema.sql @@ -1,4 +1,4 @@ CREATE TABLE IF NOT EXISTS GREETINGS ( - id serial PRIMARY KEY, + id UUID PRIMARY KEY, name varchar(50) NOT NULL UNIQUE ); diff --git a/edu-spring-postgres/compose.yaml b/edu-spring-postgres/compose.yaml index 2840ff8..ace2722 100644 --- a/edu-spring-postgres/compose.yaml +++ b/edu-spring-postgres/compose.yaml @@ -13,7 +13,7 @@ services: - spring-postgres db: - image: postgres:17 + image: postgres:18.3-alpine3.23 restart: always secrets: - db-password From 9b89ab72feef839288a0356534e4b52877f76ab4 Mon Sep 17 00:00:00 2001 From: seskildsen Date: Wed, 20 May 2026 13:52:38 +0200 Subject: [PATCH 2/7] test: use resttestclient instead of testresttemplate https://github.com/spring-projects/spring-boot/wiki/Spring-Boot-4.0-Migration-Guide#using-webclient-or-testresttemplate-and-springboottest --- edu-spring-postgres/backend/pom.xml | 6 --- .../HomeControllerIntegrationTest.java | 52 ++++++++++++++----- 2 files changed, 40 insertions(+), 18 deletions(-) diff --git a/edu-spring-postgres/backend/pom.xml b/edu-spring-postgres/backend/pom.xml index f4d5959..fc5ebcc 100755 --- a/edu-spring-postgres/backend/pom.xml +++ b/edu-spring-postgres/backend/pom.xml @@ -65,12 +65,6 @@ spring-boot-resttestclient test - - org.testcontainers - testcontainers - ${testcontainers.version} - test - org.testcontainers testcontainers-junit-jupiter diff --git a/edu-spring-postgres/backend/src/test/java/com/company/project/HomeControllerIntegrationTest.java b/edu-spring-postgres/backend/src/test/java/com/company/project/HomeControllerIntegrationTest.java index 9be64d0..59f1f29 100644 --- a/edu-spring-postgres/backend/src/test/java/com/company/project/HomeControllerIntegrationTest.java +++ b/edu-spring-postgres/backend/src/test/java/com/company/project/HomeControllerIntegrationTest.java @@ -1,19 +1,20 @@ package com.company.project; -import org.junit.jupiter.api.Assertions; +import static org.junit.jupiter.api.Assertions.assertEquals; + +import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import org.springframework.boot.test.context.SpringBootTest; -import org.springframework.boot.resttestclient.TestRestTemplate; -import org.springframework.http.ResponseEntity; +import org.springframework.core.ParameterizedTypeReference; import org.springframework.test.context.DynamicPropertyRegistry; import org.springframework.test.context.DynamicPropertySource; +import org.springframework.test.web.servlet.client.RestTestClient; +import org.springframework.web.context.WebApplicationContext; import org.testcontainers.junit.jupiter.Container; import org.testcontainers.junit.jupiter.EnabledIfDockerAvailable; import org.testcontainers.junit.jupiter.Testcontainers; import org.testcontainers.postgresql.PostgreSQLContainer; -import static org.junit.jupiter.api.Assertions.assertTrue; - @Testcontainers @EnabledIfDockerAvailable @SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT) @@ -22,8 +23,15 @@ class HomeControllerIntegrationTest { @Container static PostgreSQLContainer postgreSQLContainer = new PostgreSQLContainer("postgres:18.3-alpine3.23"); + RestTestClient restTestClient; + + @BeforeEach + public void setup(WebApplicationContext context) { + restTestClient = RestTestClient.bindToApplicationContext(context).build(); + } + @DynamicPropertySource - static void neo4jProperties(DynamicPropertyRegistry registry) { + static void setProperties(DynamicPropertyRegistry registry) { registry.add("spring.datasource.url", () -> postgreSQLContainer.getJdbcUrl()); registry.add("spring.datasource.driver-class-name", () -> postgreSQLContainer.getDriverClassName()); registry.add("spring.datasource.username", () -> postgreSQLContainer.getUsername()); @@ -32,12 +40,32 @@ static void neo4jProperties(DynamicPropertyRegistry registry) { @Test void testHomeEndpoint() { - TestRestTemplate testRestTemplate = new TestRestTemplate(); - ResponseEntity response = testRestTemplate.getForEntity("/", String.class); - assertTrue(response.getStatusCode().is2xxSuccessful()); - Assertions.assertNotNull(response.getBody()); - assertTrue(response.getBody().contains(postgreSQLContainer.getJdbcUrl())); + String greeting = restTestClient.get() + .uri("/") + .exchange() + .expectStatus().isOk() + .expectBody(new ParameterizedTypeReference() {}) + .returnResult() + .getResponseBody(); - } + assertEquals(""" + + + + Getting Started: Serving Web Content + + + + +

Hello from Spring Boot in Docker!

+

Connected to database!

+

Other pages:

+ + + """, greeting); + } } From ce0ecd3072c76e68249cc751379c6235bd5b9b2c Mon Sep 17 00:00:00 2001 From: seskildsen Date: Wed, 20 May 2026 13:52:58 +0200 Subject: [PATCH 3/7] build: use docker syntax 1.6 instead of 1.4 --- edu-spring-postgres/backend/Dockerfile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/edu-spring-postgres/backend/Dockerfile b/edu-spring-postgres/backend/Dockerfile index 09f4cdb..50b6db2 100755 --- a/edu-spring-postgres/backend/Dockerfile +++ b/edu-spring-postgres/backend/Dockerfile @@ -1,4 +1,4 @@ -# syntax=docker/dockerfile:1.4 +# syntax=docker/dockerfile:1.6 # Stage 1: Build FROM maven:3.9.14-eclipse-temurin-25-alpine AS builder From 987a29e7e6f4bb2c5b07d16cf6fa48c4ff269992 Mon Sep 17 00:00:00 2001 From: seskildsen Date: Wed, 20 May 2026 13:55:47 +0200 Subject: [PATCH 4/7] style: use indent 4 instead of 2 --- .../controller/GreetingController.java | 95 +++++++++---------- .../com/company/project/entity/Greeting.java | 80 ++++++++-------- .../repository/IGreetingRepository.java | 2 +- .../project/service/GreetingService.java | 60 ++++++------ .../project/service/IGreetingService.java | 10 +- .../HomeControllerIntegrationTest.java | 10 +- 6 files changed, 128 insertions(+), 129 deletions(-) diff --git a/edu-spring-postgres/backend/src/main/java/com/company/project/controller/GreetingController.java b/edu-spring-postgres/backend/src/main/java/com/company/project/controller/GreetingController.java index 0f757d9..224442a 100755 --- a/edu-spring-postgres/backend/src/main/java/com/company/project/controller/GreetingController.java +++ b/edu-spring-postgres/backend/src/main/java/com/company/project/controller/GreetingController.java @@ -18,61 +18,60 @@ @Controller public class GreetingController { - private static final Logger log = LoggerFactory.getLogger(GreetingController.class); + private static final Logger log = LoggerFactory.getLogger(GreetingController.class); - private final IGreetingService iGreetingService; + private final IGreetingService iGreetingService; - @Autowired - public GreetingController(IGreetingService iGreetingService) { - this.iGreetingService = iGreetingService; - } + @Autowired + public GreetingController(IGreetingService iGreetingService) { + this.iGreetingService = iGreetingService; + } - @GetMapping("/") - public String showHome(Model model) { - String greetingDocker = "Docker"; - log.info("GET / — loading default greeting with name='{}'", greetingDocker); - Greeting dockerGreeting = iGreetingService.showHome(greetingDocker).orElseThrow(() -> { - log.warn("Greeting with name='{}' not found", greetingDocker); - return new ResponseStatusException(HttpStatus.NOT_FOUND, "Greeting '" + greetingDocker + "' not found"); + @GetMapping("/") + public String showHome(Model model) { + String greetingDocker = "Docker"; + log.info("GET / — loading default greeting with name='{}'", greetingDocker); + Greeting dockerGreeting = iGreetingService.showHome(greetingDocker).orElseThrow(() -> { + log.warn("Greeting with name='{}' not found", greetingDocker); + return new ResponseStatusException(HttpStatus.NOT_FOUND, "Greeting '" + greetingDocker + "' not found"); }); - log.info("Loaded greeting: '{}'", dockerGreeting.getName()); - model.addAttribute("name", dockerGreeting.getName()); - model.addAttribute("body", "Connected to database!"); - return "greeting-single"; - } + log.info("Loaded greeting: '{}'", dockerGreeting.getName()); + model.addAttribute("name", dockerGreeting.getName()); + model.addAttribute("body", "Connected to database!"); + return "greeting-single"; + } - @GetMapping("/greetings") - public String listGreetings(Model model) { - log.info("GET /greetings — listing all greetings"); - Iterable greetings = iGreetingService.listGreetings(); - model.addAttribute("greetings", greetings); - return "greetings"; - } + @GetMapping("/greetings") + public String listGreetings(Model model) { + log.info("GET /greetings — listing all greetings"); + Iterable greetings = iGreetingService.listGreetings(); + model.addAttribute("greetings", greetings); + return "greetings"; + } - @GetMapping("/greetings/{id}") - public String sayHello(@PathVariable UUID id, Model model) { - log.info("GET /greetings/{} — looking up greeting", id); - Greeting greeting = iGreetingService.sayHello(id) - .orElseThrow(() -> { - log.warn("Greeting with id='{}' not found", id); - return new ResponseStatusException(HttpStatus.NOT_FOUND, "Greeting '" + id + "' not found"); + @GetMapping("/greetings/{id}") + public String sayHello(@PathVariable UUID id, Model model) { + log.info("GET /greetings/{} — looking up greeting", id); + Greeting greeting = iGreetingService.sayHello(id).orElseThrow(() -> { + log.warn("Greeting with id='{}' not found", id); + return new ResponseStatusException(HttpStatus.NOT_FOUND, "Greeting '" + id + "' not found"); }); - log.info("Found greeting id='{}' name='{}'", id, greeting.getName()); - model.addAttribute("name", greeting.getName()); - model.addAttribute("body", "Greeting #" + id); - return "greeting-single"; - } + log.info("Found greeting id='{}' name='{}'", id, greeting.getName()); + model.addAttribute("name", greeting.getName()); + model.addAttribute("body", "Greeting #" + id); + return "greeting-single"; + } - @GetMapping("/new") - public String newGreetingForm() { - return "new"; - } + @GetMapping("/new") + public String newGreetingForm() { + return "new"; + } - @PostMapping("/greetings") - public String createGreeting(@RequestParam String name) { - log.info("POST /greetings — creating new greeting with name='{}'", name); - Greeting greeting = iGreetingService.createGreeting(name); - log.info("Saved new greeting id='{}' name='{}'", greeting.getId(), greeting.getName()); - return "redirect:/greetings/" + greeting.getId(); - } + @PostMapping("/greetings") + public String createGreeting(@RequestParam String name) { + log.info("POST /greetings — creating new greeting with name='{}'", name); + Greeting greeting = iGreetingService.createGreeting(name); + log.info("Saved new greeting id='{}' name='{}'", greeting.getId(), greeting.getName()); + return "redirect:/greetings/" + greeting.getId(); + } } diff --git a/edu-spring-postgres/backend/src/main/java/com/company/project/entity/Greeting.java b/edu-spring-postgres/backend/src/main/java/com/company/project/entity/Greeting.java index 5fcc543..0b477c7 100644 --- a/edu-spring-postgres/backend/src/main/java/com/company/project/entity/Greeting.java +++ b/edu-spring-postgres/backend/src/main/java/com/company/project/entity/Greeting.java @@ -11,55 +11,55 @@ @Table(name = "GREETINGS") public class Greeting { - @Id - @GeneratedValue(strategy = GenerationType.UUID) - private UUID id; - private String name; + @Id + @GeneratedValue(strategy = GenerationType.UUID) + private UUID id; - public Greeting() { - } + private String name; - public Greeting(String name) { - this.name = name; - } + public Greeting() {} - public Greeting(UUID id, String name) { - this.id = id; - this.name = name; - } + public Greeting(String name) { + this.name = name; + } - public UUID getId() { - return id; - } + public Greeting(UUID id, String name) { + this.id = id; + this.name = name; + } - public void setId(UUID id) { - this.id = id; - } + public UUID getId() { + return id; + } - public String getName() { - return name; - } + public void setId(UUID id) { + this.id = id; + } - public void setName(String name) { - this.name = name; - } + public String getName() { + return name; + } - @Override - public boolean equals(Object o) { - if (this == o) { - return true; - } - if (o == null || getClass() != o.getClass()) { - return false; - } + public void setName(String name) { + this.name = name; + } - Greeting greeting = (Greeting) o; + @Override + public boolean equals(Object o) { + if (this == o) { + return true; + } + if (o == null || getClass() != o.getClass()) { + return false; + } - return name.equals(greeting.name); - } + Greeting greeting = (Greeting) o; - @Override - public int hashCode() { - return name.hashCode(); - } + return name.equals(greeting.name); + } + + @Override + public int hashCode() { + return name.hashCode(); + } } diff --git a/edu-spring-postgres/backend/src/main/java/com/company/project/repository/IGreetingRepository.java b/edu-spring-postgres/backend/src/main/java/com/company/project/repository/IGreetingRepository.java index ea915fb..4ef91a9 100644 --- a/edu-spring-postgres/backend/src/main/java/com/company/project/repository/IGreetingRepository.java +++ b/edu-spring-postgres/backend/src/main/java/com/company/project/repository/IGreetingRepository.java @@ -8,5 +8,5 @@ @Repository public interface IGreetingRepository extends CrudRepository { - Optional findGreetingByName(String name); + Optional findGreetingByName(String name); } diff --git a/edu-spring-postgres/backend/src/main/java/com/company/project/service/GreetingService.java b/edu-spring-postgres/backend/src/main/java/com/company/project/service/GreetingService.java index c089e04..d49e356 100644 --- a/edu-spring-postgres/backend/src/main/java/com/company/project/service/GreetingService.java +++ b/edu-spring-postgres/backend/src/main/java/com/company/project/service/GreetingService.java @@ -12,34 +12,34 @@ @Service public class GreetingService implements IGreetingService { - private static final Logger log = LoggerFactory.getLogger(GreetingService.class); - - private final IGreetingRepository iGreetingRepository; - - @Autowired - public GreetingService(IGreetingRepository iGreetingRepository) { - this.iGreetingRepository = iGreetingRepository; - } - - public Optional showHome(String name) { - Optional greeting = iGreetingRepository.findGreetingByName(name); - return greeting; - } - - public Iterable listGreetings() { - Iterable greetings = iGreetingRepository.findAll(); - long countedGreetings = iGreetingRepository.count(); - log.info("Returned {} greetings from DB", countedGreetings); - return greetings; - } - - public Optional sayHello(UUID id) { - Optional greeting = iGreetingRepository.findById(id); - return greeting; - } - - public Greeting createGreeting(String name) { - Greeting greeting = iGreetingRepository.save(new Greeting(name)); - return greeting; - } + private static final Logger log = LoggerFactory.getLogger(GreetingService.class); + + private final IGreetingRepository iGreetingRepository; + + @Autowired + public GreetingService(IGreetingRepository iGreetingRepository) { + this.iGreetingRepository = iGreetingRepository; + } + + public Optional showHome(String name) { + Optional greeting = iGreetingRepository.findGreetingByName(name); + return greeting; + } + + public Iterable listGreetings() { + Iterable greetings = iGreetingRepository.findAll(); + long countedGreetings = iGreetingRepository.count(); + log.info("Returned {} greetings from DB", countedGreetings); + return greetings; + } + + public Optional sayHello(UUID id) { + Optional greeting = iGreetingRepository.findById(id); + return greeting; + } + + public Greeting createGreeting(String name) { + Greeting greeting = iGreetingRepository.save(new Greeting(name)); + return greeting; + } } diff --git a/edu-spring-postgres/backend/src/main/java/com/company/project/service/IGreetingService.java b/edu-spring-postgres/backend/src/main/java/com/company/project/service/IGreetingService.java index 3f008f1..5db9d88 100644 --- a/edu-spring-postgres/backend/src/main/java/com/company/project/service/IGreetingService.java +++ b/edu-spring-postgres/backend/src/main/java/com/company/project/service/IGreetingService.java @@ -6,11 +6,11 @@ public interface IGreetingService { - Optional showHome(String name); + Optional showHome(String name); - Iterable listGreetings(); + Iterable listGreetings(); - Optional sayHello(UUID id); + Optional sayHello(UUID id); - Greeting createGreeting(String name); -} \ No newline at end of file + Greeting createGreeting(String name); +} diff --git a/edu-spring-postgres/backend/src/test/java/com/company/project/HomeControllerIntegrationTest.java b/edu-spring-postgres/backend/src/test/java/com/company/project/HomeControllerIntegrationTest.java index 59f1f29..6762c62 100644 --- a/edu-spring-postgres/backend/src/test/java/com/company/project/HomeControllerIntegrationTest.java +++ b/edu-spring-postgres/backend/src/test/java/com/company/project/HomeControllerIntegrationTest.java @@ -25,11 +25,6 @@ class HomeControllerIntegrationTest { RestTestClient restTestClient; - @BeforeEach - public void setup(WebApplicationContext context) { - restTestClient = RestTestClient.bindToApplicationContext(context).build(); - } - @DynamicPropertySource static void setProperties(DynamicPropertyRegistry registry) { registry.add("spring.datasource.url", () -> postgreSQLContainer.getJdbcUrl()); @@ -38,6 +33,11 @@ static void setProperties(DynamicPropertyRegistry registry) { registry.add("spring.datasource.password", () -> postgreSQLContainer.getPassword()); } + @BeforeEach + public void setup(WebApplicationContext context) { + restTestClient = RestTestClient.bindToApplicationContext(context).build(); + } + @Test void testHomeEndpoint() { String greeting = restTestClient.get() From efdda2959dd205c7f1563c1c77e76827cf90fc5d Mon Sep 17 00:00:00 2001 From: seskildsen Date: Wed, 20 May 2026 14:02:46 +0200 Subject: [PATCH 5/7] test: use isEqualsTo instead of assertEquals --- .../HomeControllerIntegrationTest.java | 46 ++++++++----------- 1 file changed, 19 insertions(+), 27 deletions(-) diff --git a/edu-spring-postgres/backend/src/test/java/com/company/project/HomeControllerIntegrationTest.java b/edu-spring-postgres/backend/src/test/java/com/company/project/HomeControllerIntegrationTest.java index 6762c62..ad4b662 100644 --- a/edu-spring-postgres/backend/src/test/java/com/company/project/HomeControllerIntegrationTest.java +++ b/edu-spring-postgres/backend/src/test/java/com/company/project/HomeControllerIntegrationTest.java @@ -1,11 +1,8 @@ package com.company.project; -import static org.junit.jupiter.api.Assertions.assertEquals; - import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import org.springframework.boot.test.context.SpringBootTest; -import org.springframework.core.ParameterizedTypeReference; import org.springframework.test.context.DynamicPropertyRegistry; import org.springframework.test.context.DynamicPropertySource; import org.springframework.test.web.servlet.client.RestTestClient; @@ -40,32 +37,27 @@ public void setup(WebApplicationContext context) { @Test void testHomeEndpoint() { - String greeting = restTestClient.get() + restTestClient.get() .uri("/") .exchange() .expectStatus().isOk() - .expectBody(new ParameterizedTypeReference() {}) - .returnResult() - .getResponseBody(); - - assertEquals(""" - - - - Getting Started: Serving Web Content - - - - -

Hello from Spring Boot in Docker!

-

Connected to database!

-

Other pages:

- - - """, greeting); - + .expectBody(String.class).isEqualTo(""" + + + + Getting Started: Serving Web Content + + + + +

Hello from Spring Boot in Docker!

+

Connected to database!

+

Other pages:

+ + + """); } } From 2a403e80a07dfb584b4bc5fc7b6547c488907c9d Mon Sep 17 00:00:00 2001 From: seskildsen Date: Thu, 21 May 2026 09:49:58 +0200 Subject: [PATCH 6/7] test: update tests to use resttestclient instead of testresttemplate --- edu-spring-postgres/backend/pom.xml | 6 + .../HomeControllerIntegrationTest.java | 142 +++++++++++++++--- 2 files changed, 126 insertions(+), 22 deletions(-) diff --git a/edu-spring-postgres/backend/pom.xml b/edu-spring-postgres/backend/pom.xml index fc5ebcc..4454646 100755 --- a/edu-spring-postgres/backend/pom.xml +++ b/edu-spring-postgres/backend/pom.xml @@ -77,6 +77,12 @@ ${testcontainers.version} test
+ + org.jsoup + jsoup + 1.22.2 + test + diff --git a/edu-spring-postgres/backend/src/test/java/com/company/project/HomeControllerIntegrationTest.java b/edu-spring-postgres/backend/src/test/java/com/company/project/HomeControllerIntegrationTest.java index ad4b662..263791b 100644 --- a/edu-spring-postgres/backend/src/test/java/com/company/project/HomeControllerIntegrationTest.java +++ b/edu-spring-postgres/backend/src/test/java/com/company/project/HomeControllerIntegrationTest.java @@ -1,11 +1,21 @@ package com.company.project; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import org.jsoup.Jsoup; +import org.jsoup.nodes.Document; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.http.HttpHeaders; +import org.springframework.http.MediaType; import org.springframework.test.context.DynamicPropertyRegistry; import org.springframework.test.context.DynamicPropertySource; import org.springframework.test.web.servlet.client.RestTestClient; +import org.springframework.util.LinkedMultiValueMap; +import org.springframework.util.MultiValueMap; import org.springframework.web.context.WebApplicationContext; import org.testcontainers.junit.jupiter.Container; import org.testcontainers.junit.jupiter.EnabledIfDockerAvailable; @@ -37,27 +47,115 @@ public void setup(WebApplicationContext context) { @Test void testHomeEndpoint() { - restTestClient.get() - .uri("/") - .exchange() - .expectStatus().isOk() - .expectBody(String.class).isEqualTo(""" - - - - Getting Started: Serving Web Content - - - - -

Hello from Spring Boot in Docker!

-

Connected to database!

-

Other pages:

- - - """); + restTestClient + .get() + .uri("/") + .exchange() + .expectStatus() + .isOk() + .expectBody(String.class) + .isEqualTo(""" + + + + Getting Started: Serving Web Content + + + + +

Hello from Spring Boot in Docker!

+

Connected to database!

+

Other pages:

+ + + """); + } + + @Test + void testGreetingsEndpoint() { + RestTestClient.ResponseSpec responseSpec = + restTestClient.get().uri("/greetings").exchange().expectStatus().isOk(); + + assertNotNull(responseSpec.returnResult().toString()); + Document document = Jsoup.parse(responseSpec.returnResult().toString()); + assertEquals(1, document.select("h1:contains(Greetings)").size()); + assertTrue(document.select("table tbody tr").size() >= 3); + assertEquals(1, document.select("table tbody tr td a:contains(Docker)").size()); + assertEquals( + 1, document.select("table tbody tr td a:contains(Workshop)").size()); + assertEquals( + 1, document.select("table tbody tr td a:contains(The Future)").size()); + } + + @Test + void testNewGreetingEndpoint() { + RestTestClient.ResponseSpec responseSpec = + restTestClient.get().uri("/new").exchange().expectStatus().isOk(); + + assertNotNull(responseSpec.returnResult().toString()); + Document formPageDocument = Jsoup.parse(responseSpec.returnResult().toString()); + + assertTrue(formPageDocument.select("form#new-greeting").size() == 1); + + HttpHeaders headers = new HttpHeaders(); + headers.setContentType(MediaType.APPLICATION_FORM_URLENCODED); + + MultiValueMap form = new LinkedMultiValueMap<>(); + String newGreetingName = "Integration Test Greeting"; + form.add("name", newGreetingName); + + RestTestClient.ResponseSpec createResponse = restTestClient + .post() + .uri("/greetings") + .body(form) + .exchange() + .expectStatus() + .is3xxRedirection(); + + if (createResponse.returnResult().getResponseHeaders().getLocation().getPath() != null) { + String createdGreetingPath = createResponse + .returnResult() + .getResponseHeaders() + .getLocation() + .getPath(); + + assertTrue(createdGreetingPath.matches( + "/greetings/[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}")); + + RestTestClient.ResponseSpec createdGreetingResponse = restTestClient + .get() + .uri(createdGreetingPath) + .exchange() + .expectStatus() + .isOk(); + + assertNotNull(createdGreetingResponse.returnResult().toString()); + + Document createdGreetingResponseDocument = + Jsoup.parse(createdGreetingResponse.returnResult().toString()); + + assertEquals( + 1, + createdGreetingResponseDocument + .select("p:contains(" + newGreetingName + ")") + .size()); + } + + RestTestClient.ResponseSpec greetingsResponse = + restTestClient.get().uri("/greetings").exchange().expectStatus().isOk(); + + assertNotNull(greetingsResponse.returnResult().toString()); + + Document greetingsResponseDocument = + Jsoup.parse(greetingsResponse.returnResult().toString()); + + assertEquals( + 1, + greetingsResponseDocument + .select("table tbody tr td a:contains(" + newGreetingName + ")") + .size()); } } From 701ad99675559979af963b866c4800b3063ac0d9 Mon Sep 17 00:00:00 2001 From: seskildsen Date: Thu, 21 May 2026 09:58:22 +0200 Subject: [PATCH 7/7] chore: remove checkstyle format file --- .gitignore | 1 + edu-spring-postgres/backend/project.iml | 6 ------ 2 files changed, 1 insertion(+), 6 deletions(-) delete mode 100644 edu-spring-postgres/backend/project.iml diff --git a/.gitignore b/.gitignore index 8823ae7..eb14443 100644 --- a/.gitignore +++ b/.gitignore @@ -7,3 +7,4 @@ **/*.jar **/target .idea/ +**/**/project.iml \ No newline at end of file diff --git a/edu-spring-postgres/backend/project.iml b/edu-spring-postgres/backend/project.iml deleted file mode 100644 index 9e3449c..0000000 --- a/edu-spring-postgres/backend/project.iml +++ /dev/null @@ -1,6 +0,0 @@ - - - - - \ No newline at end of file