Level: Beginner to intermediate
Table of Contents
- Introduction and prerequisites
- Tools used
- What we will build
- Setting up a Maven project
- Compile and run first class
- Introduction of a first dependency (SLF4J)
- Module 2 Summary
- Overview
- Using Java HttpClient
- HttpClient Improvement
- Introduction of a Java Record
- JSON binding with Jackson
- Course filtering
- Writing the first unit tests (JUnit)
- Unit test settings
- Module 3 Summary
- Overview
- Refactoring to multiple Maven modules
- Introduction of the Repository abstraction
- Repository configuration with H2 and JDBC
- JDBC Repository Implementation
- Using the Repository from the CLI
- Module 4 Summary
- Overview
- Creating a JAX-RS Resource
- Resource Exposure via Jersey (HTTP)
- Returning JSON with JAX-RS and Jersey
- Added storage of notes in the Repository
- Adding notes via REST API
- Module 5 Summary
- Unification of application logging
- External application configuration
- Maven dependency management
- Creating a standalone executable JAR
- Next steps for the project
- Next steps for learner
- pom.xml parent
- pom.xml — course-info-repository
- pom.xml — course-info-cli
- pom.xml — course-info-server
- Course.java (domain)
- CourseRepository.java (interface)
- CourseJdbcRepository.java (JDBC implementation)
- RepositoryException.java
- db_init.sql
- CourseTest.java
- CourseRetriever.java (main)
- CourseRetrievalService.java
- PluralsightCourse.java (record)
- CourseStorageService.java
- CourseStorageServiceTest.java
- PluralsightCourseTest.java
1. Course Overview
This course teaches how to build a true end-to-end Java application, using industry standard tools, libraries, and practices. The objective is not to learn a particular framework, but to stay as close as possible to “core” Java while using essential additional libraries.
Topics covered
| Theme | Technology |
|---|---|
| Build Tool | Maven |
| Language Features | Java 17 (records, text blocks, switch expressions, etc.) |
| Unit Testing | JUnit 5 |
| HTTP calls | Java HttpClient (since Java 11) |
| JSON Binding | Jackson |
| Relational database | H2 + JDBC |
| REST API | JAX-RS / Jersey 3 |
Target audience
This course is aimed at beginner to intermediate Java developers who already know the basics of the language (collections, streams, exception handling, OOP) but have not yet worked on a real application codebase.
Recommended prerequisite courses:
- Java SE 17: The Big Picture
- Java SE 17 Fundamentals
- Object-oriented Programming in Java SE 17
2. Configuring a Java application
Module duration: 44m 42s
2.1 Introduction and prerequisites
This course builds a complete Java application from scratch. We spend the majority of time in the IDE actually coding. Here is what is assumed:
- Knowledge of Java fundamentals (OOP, collections, streams, exceptions)
- Prior experience with an IDE (not necessarily IntelliJ)
- No prior knowledge of Maven, JUnit, HttpClient, JDBC, JAX-RS is required
The end goal is to be comfortable understanding and contributing to large, real-world Java codebases.
2.2 Tools used
| Tool | Recommended version | URL |
|---|---|---|
| JDK | Java 17 | https://adoptium.net |
| Maven | 3.8.x | https://maven.apache.org |
| SDKMAN (optional) | — | https://sdkman.io |
Verifying the installation from the terminal:
java --version
# openjdk 17.x.x ...
mvn --version
# Apache Maven 3.8.x ...
SDKMAN is a convenient alternative for installing and switching between different versions of Java or Maven:
# Installation SDKMAN
curl -s "https://get.sdkman.io" | bash
# Installation Java 17 via SDKMAN
sdk install java 17.x.x-tem
# Installation Maven
sdk install maven 3.8.6
2.3 What we will build
The project is called Course Info and consists of three components:
┌─────────────────────────────────────────────────────────────────┐
│ Système Course Info │
│ │
│ ┌───────────────┐ stocke ┌──────────────┐ │
│ │ course-info │ ──────────► │ Base de │ │
│ │ -cli │ │ données H2 │ │
│ │ (CLI tool) │ │ (courses.db)│ │
│ └───────────────┘ └──────┬───────┘ │
│ │ │ lit │
│ │ appel API │ │
│ ▼ ▼ │
│ ┌───────────────┐ ┌──────────────┐ │
│ │ Pluralsight │ │ course-info │ │
│ │ API (HTTP) │ │ -server │ │
│ └───────────────┘ │ (REST API) │ │
│ └──────────────┘ │
│ │ │
│ ▼ │
│ GET /courses → JSON │
│ POST /courses/{id}/notes │
└─────────────────────────────────────────────────────────────────┘
Key Features:
- CLI tool: calls the Pluralsight API, parses the JSON, stores the courses in the database
- REST server: expose courses stored in JSON via
GET /courses - Notes: allows you to add notes to a course via
POST /courses/{id}/notes
A course in the system has the following structure:
{
"id": "b6e31e25-ed0b-4bd1-8d1a-4854f63a268c",
"name": "What's New in Java 15",
"length": 68,
"url": "https://app.pluralsight.com/library/courses/java-15-whats-new",
"notes": null
}
Choice not to use a framework: Frameworks like Spring Boot, Jakarta EE, Quarkus or Micronaut are valid but impose specific architecture and practices. This course teaches Java and not a framework, in order to understand the foundations before tackling these layers of abstraction.
2.4 Setting up a Maven project
Maven is chosen as the build tool for several reasons:
- Separation of build responsibility from IDE
- Standardized and universally recognized project structure (convention over configuration)
- Compiling and running automatic tests
- Dependency Management (download from Maven Central)
- Build reproducible on all environments
Maven directory structure
course-info/
├── pom.xml ← Configuration Maven
└── src/
├── main/
│ └── java/ ← Code de production
│ └── com/pluralsight/courseinfo/cli/
└── test/
└── java/ ← Code de test
└── com/pluralsight/courseinfo/cli/
Configuring maven.compiler.release vs source/target
The --release flag (available since Java 9) is preferred to the source and target properties because it ensures that the code does not refer to APIs that do not exist in the target version:
<properties>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<maven.compiler.release>17</maven.compiler.release>
</properties>
You must also configure a recent version of the Maven Compiler Plugin (≥ 3.6) so that the release flag is recognized:
<build>
<plugins>
<plugin>
<artifactId>maven-compiler-plugin</artifactId>
<version>3.10.1</version>
</plugin>
</plugins>
</build>
Essential Maven Commands
# Nettoyer les artéfacts de compilation
mvn clean
# Compiler, tester, et packager
mvn clean verify
# Exécuter seulement les tests
mvn test
Note: On first launch, Maven downloads plugins and dependencies to a local cache (~/.m2/repository). Subsequent launches reuse this cache.
2.5 Compiling and running the first class
Package naming convention in a large Java project:
com.pluralsight.courseinfo.cli
│── com.pluralsight → GroupId Maven (identifie l'entreprise/organisation)
│── courseinfo → ArtifactId Maven (nom du projet)
└── cli → Composant (CLI, repository, server, etc.)
First class — application entry point:
package com.pluralsight.courseinfo.cli;
public class CourseRetriever {
public static void main(String... args) {
if (args.length == 0) {
System.out.println("Please provide an author name as first argument.");
return;
}
try {
retrieveCourses(args[0]);
} catch (Exception e) {
System.out.println("Unexpected error: " + e.getMessage());
e.printStackTrace();
}
}
private static void retrieveCourses(String authorId) {
System.out.println("Retrieving courses for author: " + authorId);
}
}
Important points:
- The signature
main(String... args)uses Java varargs, equivalent tomain(String[] args) - A high-level exception handler is introduced from the start to catch any unhandled exceptions in lower layers
- IntelliJ allows configuring program arguments via Run → Edit Configurations → Program arguments
2.6 Introduction of a first dependency (SLF4J)
Why use a logging library?
System.out.println is insufficient for a real application because:
- No log levels (DEBUG, INFO, WARN, ERROR)
- No configuration without recompiling
- No possibility of directing logs to different destinations (file, centralized system)
SLF4J: Simple Logging Facade for Java
The Java logging ecosystem is fragmented (Log4j, Logback, JDK logging). SLF4J is a facade that abstracts the underlying implementation. This allows:
- Write code against a single API (SLF4J API)
- To change the implementation without modifying the application code
Code applicatif → SLF4J API → [slf4j-simple | Logback | Log4j | JDK logging]
Checking for a Maven dependency
To find Maven coordinates for a library: https://search.maven.org
Search for slf4j → select org.slf4j:slf4j-api
Added SLF4J dependencies in pom.xml
<dependencies>
<dependency>
<groupId>org.slf4j</groupId>
<artifactId>slf4j-api</artifactId>
<version>1.7.36</version>
</dependency>
<dependency>
<groupId>org.slf4j</groupId>
<artifactId>slf4j-simple</artifactId>
<version>1.7.36</version>
<scope>runtime</scope>
</dependency>
</dependencies>
slf4j-api: the logging API that the code uses directlyslf4j-simple: simple logging implementation (scoperuntimebecause we do not code against it)
Usage in code
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
public class CourseRetriever {
private static Logger LOG = LoggerFactory.getLogger(CourseRetriever.class);
public static void main(String... args) {
LOG.info("CourseRetriever starting");
if (args.length == 0) {
LOG.warn("Please provide an author name as first argument.");
return;
}
// ...
}
}
SLF4J parameterized messages avoid unnecessary string concatenation:
// À éviter
LOG.info("Retrieving courses for author '" + authorId + "'");
// Recommandé — la string n'est construite que si le niveau de log est actif
LOG.info("Retrieving courses for author '{}'", authorId);
Log levels:
| Method | Usage |
|---|---|
LOG.trace(...) | Very detailed information (development only) |
LOG.debug(...) | Debug Info |
LOG.info(...) | Informative progress messages |
LOG.warn(...) | Non-blocking warnings |
LOG.error(...) | Important errors |
2.7 Module 2 Summary
- Creating a Java Maven project from scratch in IntelliJ
- Correct configuration of Java 17 via
maven.compiler.release - Understanding standard Maven directory structure
- Introduction of essential Maven commands (
mvn clean,mvn verify) - Adding an external dependency via Maven Central
- Using SLF4J for logging instead of
System.out.println
3. Calling an external web API
Module duration: 46m 54s
3.1 Overview
This module extends the CLI so that it can make HTTP calls to the Pluralsight API which returns courses for a given author. Concepts covered:
- Java HttpClient (standard API since Java 11) for HTTP calls
- Java Records (Java 16+) to model data returned by the API
- Jackson to deserialize JSON into Java objects
- JUnit 5 to write the first unit tests
3.2 Using the Java HttpClient
Separation of concerns: introduction of CourseRetrievalService
Rather than writing the HTTP code directly in CourseRetriever, we create a dedicated class CourseRetrievalService in the service package:
com.pluralsight.courseinfo.cli
├── CourseRetriever.java ← Point d'entrée
└── service/
├── CourseRetrievalService.java ← Logique d'appel HTTP
├── CourseStorageService.java ← Logique de stockage
└── PluralsightCourse.java ← Modèle de données (record)
Java API Javadoc
To explore the Java 17 API:
- Google search: “javadoc 17”
- Oracle URL: https://docs.oracle.com/en/java/javase/17/docs/api/
- Module affected:
java.net.http→ classjava.net.http.HttpClient
HttpClient base code
import java.net.URI;
import java.net.http.HttpClient;
import java.net.http.HttpRequest;
import java.net.http.HttpResponse;
public class CourseRetrievalService {
private static final String PS_URI =
"https://app.pluralsight.com/profile/data/author/%s/all-content";
private static final HttpClient CLIENT = HttpClient.newHttpClient();
public String getCoursesFor(String authorId) {
HttpRequest request = HttpRequest
.newBuilder(URI.create(PS_URI.formatted(authorId)))
.GET()
.build();
try {
HttpResponse<String> response =
CLIENT.send(request, HttpResponse.BodyHandlers.ofString());
return response.body();
} catch (IOException | InterruptedException e) {
throw new RuntimeException("Could not call Pluralsight API", e);
}
}
}
Important points:
URI.create(PS_URI.formatted(authorId))usesString.formatted()(Java 15+) instead ofString.format()HttpResponse.BodyHandlers.ofString()indicates that the response body should be treated as aString- The
HttpClientinstance is declaredstatic finalbecause it is thread-safe and reusable
3.3 Improvement of HttpClient
Pattern Builder to configure the client
private static final HttpClient CLIENT = HttpClient
.newBuilder()
.followRedirects(HttpClient.Redirect.ALWAYS) // Suivre les redirections HTTP
.build();
Managing HTTP status codes with a switch expression
Java 14 introduced switch expressions, a form of switch that returns a value:
public List<PluralsightCourse> getCoursesFor(String authorId) {
HttpRequest request = HttpRequest
.newBuilder(URI.create(PS_URI.formatted(authorId)))
.GET()
.build();
try {
HttpResponse<String> response =
CLIENT.send(request, HttpResponse.BodyHandlers.ofString());
return switch (response.statusCode()) {
case 200 -> toPluralsightCourses(response);
case 404 -> List.of();
default -> throw new RuntimeException(
"Pluralsight API call failed with status code " + response.statusCode()
);
};
} catch (IOException | InterruptedException e) {
throw new RuntimeException("Could not call Pluralsight API", e);
}
}
The switch expression is more concise than a chained if-else and allows the different cases to be clearly expressed.
3.4 Introduction of a Java Record
What is a Java Record?
Records (introduced in Java 16) are a type of class designed to model immutable data. They automatically generate:
- A constructor with all parameters
- Access methods (getters) for each component
equals(),hashCode(),toString()
Syntax of a record:
public record PluralsightCourse(String id, String title, String duration,
String contentUrl, boolean isRetired) {
}
This is equivalent to a classic Java class with all these elements defined, but in a single line.
Access to record components
Unlike JavaBeans, a record’s access methods have the same name as the component (without a get prefix):
PluralsightCourse course = new PluralsightCourse("id", "Title", "01:30:00", "/url", false);
String title = course.title(); // "Title" — pas getTitlte()
boolean retired = course.isRetired(); // false
Adding a behavior method to a record
A record can contain normal methods. Here, we convert the duration (string) into minutes (long):
@JsonIgnoreProperties(ignoreUnknown = true)
public record PluralsightCourse(String id, String title, String duration,
String contentUrl, boolean isRetired) {
public long durationInMinutes() {
return Duration.between(
LocalTime.MIN,
LocalTime.parse(duration())
).toMinutes();
}
}
The java.time API:
LocalTime.parse(duration())parses the string"01:30:00.123"intoLocalTimeDuration.between(LocalTime.MIN, ...)calculates the duration since midnight.toMinutes()converts to integer minutes
@JsonIgnoreProperties(ignoreUnknown = true)
The Jackson annotation indicates that properties present in the JSON but absent in the record should be ignored (rather than causing an error).
3.5 JSON binding with Jackson
Why Jackson?
There is no API in the Java Standard Library to map JSON to objects. Jackson is the most used library in the Java ecosystem for this task.
Added Jackson dependencies in pom.xml
<properties>
<jackson.version>2.13.3</jackson.version>
</properties>
<dependencies>
<dependency>
<groupId>com.fasterxml.jackson.core</groupId>
<artifactId>jackson-databind</artifactId>
<version>${jackson.version}</version>
</dependency>
<dependency>
<groupId>com.fasterxml.jackson.core</groupId>
<artifactId>jackson-annotations</artifactId>
<version>${jackson.version}</version>
</dependency>
</dependencies>
jackson-databind: the JSON mapping engine ↔ Java objectsjackson-annotations: annotations (@JsonIgnoreProperties`, etc.)jackson-core: transitive dependency ofjackson-databind, automatically downloaded by Maven
Important: Never depend on a transitive dependency in your own code. If we use jackson-core directly, we must explicitly add it as a dependency.
Using ObjectMapper
private static final ObjectMapper OBJECT_MAPPER = new ObjectMapper();
private List<PluralsightCourse> toPluralsightCourses(HttpResponse<String> response)
throws JsonProcessingException {
JavaType returnType = OBJECT_MAPPER.getTypeFactory()
.constructCollectionType(List.class, PluralsightCourse.class);
return OBJECT_MAPPER.readValue(response.body(), returnType);
}
ObjectMapperis thread-safe and shareable → declaredstatic finalJavaTypeis needed for generic typeList<PluralsightCourse>due to type erasure in Java (type erasure)
Exploring Jackson source code in IntelliJ
IntelliJ offers a Download Sources button that downloads the source code of the libraries from Maven Central. This allows you to:
- Read the Javadoc directly in the IDE
- Navigating Libraries Implementation
- Understand how APIs work
3.6 Course filtering
Once the PluralsightCourse list is available, you can filter the withdrawn courses with the Stream API:
With a lambda
List<PluralsightCourse> activeCourses = courses.stream()
.filter(course -> !course.isRetired())
.toList();
With a method reference and Predicate.not
import static java.util.function.Predicate.not;
List<PluralsightCourse> activeCourses = courses.stream()
.filter(not(PluralsightCourse::isRetired))
.toList();
PluralsightCourse::isRetiredis a method reference that acts like aPredicate<PluralsightCourse>Predicate.not(...)reverses this predicate.toList()(Java 16+) creates an unmodifiable list from the stream
3.7 Writing the first unit tests (JUnit)
Why write tests?
- Verify that the code does what it should do
- Tests remain even if implementation changes
- Allows you to refactor with confidence
Added JUnit 5 to pom.xml
<dependency>
<groupId>org.junit.jupiter</groupId>
<artifactId>junit-jupiter</artifactId>
<version>5.8.2</version>
<scope>test</scope>
</dependency>
The test scope means that this dependency is only available in the test code and is not included in the final JAR.
You must also configure the Maven Surefire Plugin for Maven to correctly execute JUnit 5 tests:
<plugin>
<artifactId>maven-surefire-plugin</artifactId>
<version>2.22.2</version>
</plugin>
Without this configuration, mvn test will return 0 tests executed because the default version of the plugin does not support JUnit 5.
First unit test
We test the durationInMinutes() method of the PluralsightCourse record:
import org.junit.jupiter.api.Test;
import static org.junit.jupiter.api.Assertions.*;
class PluralsightCourseTest {
@Test
void durationInMinutes() {
PluralsightCourse course =
new PluralsightCourse("id", "Test course", "01:08:54.9613330", "url", false);
assertEquals(68, course.durationInMinutes());
}
}
JUnit 5 conventions:
@Testmarks a method as test- Test method is
package-private(nopublic) assertEquals(expected, actual)— argument order is important for error messages
3.8 Setting up the unit test
Rather than duplicating the test for each case, we use @ParameterizedTest with @CsvSource:
import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.CsvSource;
import static org.junit.jupiter.api.Assertions.*;
class PluralsightCourseTest {
@ParameterizedTest
@CsvSource(textBlock = """
01:08:54.9613330, 68
00:05:37, 5
00:00:00.0, 0
""")
void durationInMinutes(String input, long expected) {
PluralsightCourse course =
new PluralsightCourse("id", "Test course", input, "url", false);
assertEquals(expected, course.durationInMinutes());
}
}
Important points:
@ParameterizedTestreplaces@Testand indicates that the test will be run multiple times@CsvSourceprovides input data in CSV form- Text block (Java 15): a multiline text with
"""..."""which preserves the formatting - JUnit 5 automatically converts CSV strings to
longfor theexpectedparameter - The name of each test displayed matches the CSV line, making it easier to identify failures
3.9 Module 3 Summary
- Using the standard Java HttpClient to call an external HTTP API
- Managing HTTP status codes with a switch expression
- Data modeling with an immutable Java Record
- Parsing JSON with Jackson (
ObjectMapper,@JsonIgnoreProperties) - Filtering a collection with the Stream API and
Predicate.not - Writing unit tests with JUnit 5 (
@Test,@ParameterizedTest,@CsvSource) - Using Java text blocks for multiline test data
- Configuring Maven Surefire Plugin for running JUnit 5 tests
4. Storing data in a database
Module duration: 36m 21s
4.1 Overview
This module adds data persistence to a relational database. We introduce:
- Maven modularization (multi-module project)
- The Repository Pattern to abstract data access
- The H2 database (embedded, written in Java)
- The standard JDBC API for interacting with the database
4.2 Refactoring to multiple Maven modules
Why multiple Maven modules?
As the application grows, it is useful to separate related parts into separate Maven modules. Advantages:
- Each module produces its own
.jar - Dependencies between modules are declared explicitly in the POM
- Avoids a “spaghetti” code base where everything depends on everything
- Sharing configuration via parent POM
Multi-module structure
course-info/ ← Parent POM
├── pom.xml ← POM parent (packaging = pom)
├── course-info-repository/ ← Module partagé (domain + JDBC)
│ ├── pom.xml
│ └── src/
├── course-info-cli/ ← Module CLI
│ ├── pom.xml
│ └── src/
└── course-info-server/ ← Module serveur REST
├── pom.xml
└── src/
Parent POM — module declaration
<project>
<groupId>com.pluralsight</groupId>
<artifactId>course-info</artifactId>
<packaging>pom</packaging> ← Important : pas de code Java dans le parent
<version>1.0-SNAPSHOT</version>
<modules>
<module>course-info-repository</module>
<module>course-info-cli</module>
<module>course-info-server</module>
</modules>
<!-- ... -->
</project>
Dependencies between modules
The course-info-cli module declares a dependency on course-info-repository:
<dependency>
<groupId>com.pluralsight</groupId>
<artifactId>course-info-repository</artifactId>
<version>1.0-SNAPSHOT</version>
</dependency>
Maven builds modules in the correct order automatically.
4.3 Introduction of the Repository abstraction
The Pattern Repository
The goal is to introduce an abstraction (interface) that hides the details of the persistence implementation. The advantages:
- Consumer code is written in terms of domain objects, not SQL
- We can change the underlying database without affecting consumers
- The code is more testable (we can pass an in-memory implementation in the tests)
Object of domain Race
Before creating the CourseRepository interface, we create a Course record representing a course in our system (separate from PluralsightCourse which is linked to the external API):
package com.pluralsight.courseinfo.domain;
import java.util.Optional;
public record Course(String id, String name, long length, String url, Optional<String> notes) {
// Bloc de validation compact du constructeur
public Course {
filled(id);
filled(name);
filled(url);
notes.ifPresent(Course::filled);
}
private static void filled(String s) {
if (s == null || s.isBlank()) {
throw new IllegalArgumentException("No value present!");
}
}
}
Important points:
- The compact constructor (
public Course { ... }) is specific to records - It allows adding validation without re-declaring all parameters
Optional<String> notes: notes are optional. We useOptionalto make explicit the possible absence of value
Interface CourseRepository
package com.pluralsight.courseinfo.repository;
import com.pluralsight.courseinfo.domain.Course;
import java.util.List;
public interface CourseRepository {
void saveCourse(Course course);
List<Course> getAllCourses();
void addNotes(String id, String notes);
// Factory method statique pour créer une instance du repository
static CourseRepository openCourseRepository(String databaseFile) {
return new CourseJdbcRepository(databaseFile);
}
}
The static factory method openCourseRepository allows consumers to obtain an instance without knowing the concrete implementation class CourseJdbcRepository.
4.4 Configuring the Repository with H2 and JDBC
H2 Database
H2 is an embedded SQL database, written in Java:
- No separate server installation (Postgres, SQL Server, etc.)
- Included as a library in the application (Maven dependency)
- Storing data in a file on disk
- AUTO_SERVER mode: allows several processes (CLI + server) to access the same database
JDBC (Java Database Connectivity)
JDBC is a standard Java library API for interacting with SQL databases:
- Standardized: identical code regardless of the base vendor
- Almost all databases provide a JDBC driver
- High level libraries (Hibernate, Jooq) rely on JDBC
Adding H2 to the POM of the repository module
<dependency>
<groupId>com.h2database</groupId>
<artifactId>h2</artifactId>
<version>2.1.214</version>
</dependency>
Database initialization script
CREATE TABLE IF NOT EXISTS COURSES(
ID VARCHAR PRIMARY KEY NOT NULL,
NAME VARCHAR NOT NULL,
LENGTH INT NOT NULL,
URL VARCHAR NOT NULL,
NOTES VARCHAR
);
This db_init.sql file is referenced in the JDBC URL with the parameter INIT=RUNSCRIPT FROM './db_init.sql', which ensures that the table is created if it does not already exist.
4.5 Implementation of the JDBC Repository
The CourseJdbcRepository class is package-private (no public) because consumers use the CourseRepository interface. The implementation is a detail.
H2 login URL
private static final String H2_DATABASE_URL =
"jdbc:h2:file:%s;AUTO_SERVER=TRUE;INIT=RUNSCRIPT FROM './db_init.sql'";
file:%s: storage on disk, the%sis replaced by the file pathAUTO_SERVER=TRUE: mode which allows several concurrent connectionsINIT=RUNSCRIPT: executes the initialization SQL script at startup
SQL queries with text blocks
private static final String INSERT_COURSE = """
MERGE INTO Courses (id, name, length, url)
VALUES (?, ?, ?, ?)
""";
private static final String ADD_NOTES = """
UPDATE Courses SET notes = ?
WHERE id = ?
""";
MERGE INTOoffers upsert behavior: creates the course if it does not exist, updates it if it exists (based on primary keyid)- The
?are placeholders ofPreparedStatement
Importance of PreparedStatements
PreparedStatements (as opposed to simple Statements) are essential for security:
- Protect against SQL injections (SQL injection)
- Allow the database engine to cache the execution plan
Implementation of saveCourse
@Override
public void saveCourse(Course course) {
executeStatement(INSERT_COURSE, statement -> {
statement.setString(1, course.id());
statement.setString(2, course.name());
statement.setLong(3, course.length());
statement.setString(4, course.url());
statement.execute();
}, "Failed to insert " + course);
}
Utility method to execute a statement
private void executeStatement(String sql, PreparedStatementConfigurer configurer, String errorMsg) {
try (Connection connection = dataSource.getConnection()) { // try-with-resources
PreparedStatement statement = connection.prepareStatement(sql);
configurer.configure(statement);
statement.execute();
} catch (SQLException e) {
throw new RepositoryException(errorMsg, e);
}
}
@FunctionalInterface
interface PreparedStatementConfigurer {
void configure(PreparedStatement statement) throws SQLException;
}
Try-with-resources
JDBC connection implements AutoCloseable. We use try-with-resources to guarantee that the connection is always closed, even in the event of an exception:
try (Connection connection = dataSource.getConnection()) {
// La connexion est automatiquement fermée à la fin du bloc,
// qu'il y ait une exception ou non.
}
RepositoryException — domain exception
We do not propagate SQLException directly because this would expose the SQL implementation details to consumers of the interface:
package com.pluralsight.courseinfo.repository;
import java.sql.SQLException;
public class RepositoryException extends RuntimeException {
public RepositoryException(String message, SQLException e) {
super(message, e);
}
}
By using a RuntimeException, we do not force consumers to have a try-catch around each call to the repository.
Implementation of getAllCourses
@Override
public List<Course> getAllCourses() {
try (Connection connection = dataSource.getConnection()) {
Statement statement = connection.createStatement();
ResultSet resultSet = statement.executeQuery("SELECT * FROM COURSES");
List<Course> courses = new ArrayList<>();
while (resultSet.next()) {
Course course = new Course(
resultSet.getString(1), // id
resultSet.getString(2), // name
resultSet.getLong(3), // length
resultSet.getString(4), // url
Optional.ofNullable(resultSet.getString(5)) // notes
);
courses.add(course);
}
return Collections.unmodifiableList(courses);
} catch (SQLException e) {
throw new RepositoryException("Failed to retrieve courses", e);
}
}
4.6 Using the Repository from the CLI
CourseStorageService
package com.pluralsight.courseinfo.cli.service;
import com.pluralsight.courseinfo.domain.Course;
import com.pluralsight.courseinfo.repository.CourseRepository;
import java.util.List;
import java.util.Optional;
public class CourseStorageService {
private static final String PS_BASE_URL = "https://app.pluralsight.com";
private final CourseRepository courseRepository;
public CourseStorageService(CourseRepository courseRepository) {
this.courseRepository = courseRepository;
}
public void storePluralsightCourses(List<PluralsightCourse> psCourses) {
for (PluralsightCourse psCourse : psCourses) {
Course course = new Course(
psCourse.id(),
psCourse.title(),
psCourse.durationInMinutes(),
PS_BASE_URL + psCourse.contentUrl(),
Optional.empty() // Pas de notes lors du stockage initial
);
courseRepository.saveCourse(course);
}
}
}
CourseRetriever updated
private static void retrieveCourses(String authorId) {
LOG.info("Retrieving courses for author '{}'", authorId);
CourseRetrievalService courseRetrievalService = new CourseRetrievalService();
CourseRepository courseRepository =
CourseRepository.openCourseRepository("./courses.db");
CourseStorageService courseStorageService =
new CourseStorageService(courseRepository);
List<PluralsightCourse> coursesToStore = courseRetrievalService
.getCoursesFor(authorId)
.stream()
.filter(not(PluralsightCourse::isRetired))
.toList();
LOG.info("Retrieved {} courses: {}", coursesToStore.size(), coursesToStore);
courseStorageService.storePluralsightCourses(coursesToStore);
LOG.info("Courses successfully stored");
}
Testing CourseStorageService
To test CourseStorageService without a real database, we use an in-memory implementation of the CourseRepository interface (stub):
class CourseStorageServiceTest {
@Test
void storePluralsightCourses() {
CourseRepository repository = new InMemoryCourseRepository();
CourseStorageService courseStorageService = new CourseStorageService(repository);
PluralsightCourse ps1 = new PluralsightCourse(
"1", "Title 1", "01:40:00.123", "/url-1", false
);
courseStorageService.storePluralsightCourses(List.of(ps1));
Course expected = new Course(
"1", "Title 1", 100, "https://app.pluralsight.com/url-1", Optional.empty()
);
assertEquals(List.of(expected), repository.getAllCourses());
}
// Implémentation en mémoire de CourseRepository pour les tests
static class InMemoryCourseRepository implements CourseRepository {
private final List<Course> courses = new ArrayList<>();
@Override public void saveCourse(Course course) { courses.add(course); }
@Override public List<Course> getAllCourses() { return courses; }
@Override public void addNotes(String id, String notes) {
throw new UnsupportedOperationException();
}
}
}
This illustrates how the Repository Pattern improves testability: one can test service logic without starting an actual database.
4.7 Summary of Module 4
- Maven modularization: transformation of a single-module project into multi-module with parent POM
- Repository Pattern: abstraction of the persistence layer behind an interface
- Record
Course: immutable domain object with validation in the compact constructor - H2: embedded SQL database, convenient for development and testing
- JDBC: Standard API to interact with any SQL database
- PreparedStatement: protection against SQL injections and better performance
- Try-with-resources: automatic management of connection closure
- RepositoryException: encapsulation of technical exceptions behind a domain exception
5. Creating a REST API
Module duration: 28m 45s
5.1 Overview
This module adds an HTTP server which exposes the courses stored in the database via a REST API. Technologies used:
- JAX-RS: Jakarta EE specification for creating REST APIs in Java
- Jersey 3: JAX-RS reference implementation
- Grizzly: lightweight HTTP server integrated into Jersey
JAX-RS is an annotations-oriented API: we configure endpoints declaratively by annotating Java methods.
5.2 Creating a JAX-RS Resource
A JAX-RS resource is a Java class that describes and implements a REST endpoint.
JAX-RS dependencies in pom.xml of the server module
<dependency>
<groupId>jakarta.ws.rs</groupId>
<artifactId>jakarta.ws.rs-api</artifactId>
<version>3.1.0</version>
</dependency>
Annotation @Path
@Path("courses")
public class CourseResource {
// Tous les endpoints de cette classe seront sous /courses
}
Endpoint GET /courses
@GET
@Produces(MediaType.APPLICATION_JSON)
public Stream<Course> getCourses() {
return courseRepository.getAllCourses()
.stream()
.sorted(comparing(Course::id));
}
@GET: HTTP GET method@Produces: returned content type (application/json)- Sorting on
Course::idguarantees a stable order in the responses
Injection via constructor
The CourseResource receives the CourseRepository from its constructor:
public class CourseResource {
private final CourseRepository courseRepository;
public CourseResource(CourseRepository courseRepository) {
this.courseRepository = courseRepository;
}
// ...
}
This is an approach close to dependency injection but without a framework. We instantiate and wire the objects manually in the CourseServer class.
5.3 Exposing the resource via Jersey (HTTP)
Jersey dependencies in pom.xml
<properties>
<jersey.version>3.0.4</jersey.version>
</properties>
<dependencies>
<!-- Coeur de Jersey -->
<dependency>
<groupId>org.glassfish.jersey.core</groupId>
<artifactId>jersey-server</artifactId>
<version>${jersey.version}</version>
</dependency>
<!-- Serveur HTTP Grizzly pour Jersey -->
<dependency>
<groupId>org.glassfish.jersey.containers</groupId>
<artifactId>jersey-container-grizzly2-http</artifactId>
<version>${jersey.version}</version>
</dependency>
<!-- Framework d'injection HK2 (requis à runtime) -->
<dependency>
<groupId>org.glassfish.jersey.inject</groupId>
<artifactId>jersey-hk2</artifactId>
<version>${jersey.version}</version>
<scope>runtime</scope>
</dependency>
</dependencies>
CourseServer — server entry point
public class CourseServer {
private static final Logger LOG = LoggerFactory.getLogger(CourseServer.class);
private static final String BASE_URI = "http://localhost:8080/";
public static void main(String... args) {
String databaseFilename = loadDatabaseFilename();
LOG.info("Starting HTTP server with database {}", databaseFilename);
CourseRepository courseRepository =
CourseRepository.openCourseRepository("./courses.db");
ResourceConfig config = new ResourceConfig()
.register(new CourseResource(courseRepository));
GrizzlyHttpServerFactory.createHttpServer(URI.create(BASE_URI), config);
}
// ...
}
ResourceConfig is the Jersey entry point for registering JAX-RS resources. We pass the new CourseResource(courseRepository) instance directly instead of letting Jersey instantiate it (which would require an injection framework).
5.4 Returning JSON with JAX-RS and Jersey
Problem: MessageBodyWriter not found
When changing the return type from String to Stream<Course> with @Produces(APPLICATION_JSON), Jersey generates an error:
MessageBodyWriter not found for media type=application/json
Jersey cannot serialize a Java object to JSON without an additional module.
Solution: jersey-media-json-jackson
<dependency>
<groupId>org.glassfish.jersey.media</groupId>
<artifactId>jersey-media-json-jackson</artifactId>
<version>${jersey.version}</version>
<scope>runtime</scope>
</dependency>
This module integrates Jackson into Jersey for JSON serialization/deserialization. It is in runtime scope because we do not write code that references it directly.
Additional Jackson dependency for Java 8+ types
To get Jackson to properly serialize Optional<String>:
<dependency>
<groupId>com.fasterxml.jackson.datatype</groupId>
<artifactId>jackson-datatype-jdk8</artifactId>
<version>${jackson.version}</version>
<scope>runtime</scope>
</dependency>
5.5 Added storage of notes in the Repository
Modification of the Race record
We add Optional<String> notes as the 5th component:
public record Course(String id, String name, long length, String url, Optional<String> notes) {
public Course {
filled(id);
filled(name);
filled(url);
notes.ifPresent(Course::filled); // Validation si notes présentes
}
// ...
}
This change is not backwards compatible and causes compilation errors in:
CourseStorageService→ addOptional.empty()to instantiation ofCourseCourseStorageServiceTest→ updateCourseconstructs in testsCourseJdbcRepository.getAllCourses()→ read the 5thnotescolumn from theResultSet
addNotes method in CourseJdbcRepository
@Override
public void addNotes(String id, String notes) {
executeStatement(ADD_NOTES, statement -> {
statement.setString(1, notes);
statement.setString(2, id);
}, "Failed to add notes to " + id);
}
5.6 Adding notes via REST API
Endpoint POST /courses/{id}/notes
@POST
@Path("/{id}/notes")
@Consumes(MediaType.TEXT_PLAIN)
public void addNotes(@PathParam("id") String id, String notes) {
courseRepository.addNotes(id, notes);
}
@POST: HTTP POST method@Path("/{id}/notes"): relative path with placeholder{id}@Consumes(TEXT_PLAIN): request body is plain text@PathParam("id"): binds the placeholder{id}to the parameterString id- Return
void→ HTTP response 204 No Content by default
Full endpoint URL
POST http://localhost:8080/courses/{courseId}/notes
Content-Type: text/plain
Voici mes notes sur ce cours.
Error handling in getCourses
@GET
@Produces(MediaType.APPLICATION_JSON)
public Stream<Course> getCourses() {
try {
return courseRepository
.getAllCourses()
.stream()
.sorted(comparing(Course::id));
} catch (RepositoryException e) {
LOG.error("Could not retrieve courses from the database", e);
throw new NotFoundException(); // HTTP 404
}
}
5.7 Module 5 Summary
- JAX-RS: Annotation-based API to create REST APIs
- Jersey 3: JAX-RS implementation with integrated Grizzly HTTP server
- ResourceConfig: saving JAX-RS resources in Jersey
@GET,@POST,@Path,@PathParam,@Produces,@Consumes: essential JAX-RS annotationsjersey-media-json-jackson: Jackson integration for JSON serialization in JerseyOptional<String>in theCourserecord: explicit representation of optional values- Annotations enable a declarative programming style, very common in Java frameworks
6. Towards production
Module duration: 28m 9s
6.1 Unification of application logging
Problem: Two logging systems coexist
When the server starts, we observe two different log formats:
- SLF4J (our code) → a single line
- JDK logging (Internal Jersey) → two lines with different format
Jersey internally uses the java.util.logging API (JDK logging), different from SLF4J.
Solution: jul-to-slf4j bridge
Library jul-to-slf4j redirects all JDK logging API calls to SLF4J:
<dependency>
<groupId>org.slf4j</groupId>
<artifactId>jul-to-slf4j</artifactId>
<version>${slf4j.version}</version>
</dependency>
Activation of the bridge in CourseServer
The bridge must be installed before any log, using a static block:
static {
LogManager.getLogManager().reset(); // Supprime les handlers JDK logging par défaut
SLF4JBridgeHandler.install(); // Installe le bridge vers SLF4J
}
The static block executes when loading the class, ensuring that the bridge is active before any logging is used.
Result
All application logs (our code + Jersey) now use the same SLF4J format.
6.2 External configuration of the application
Why outsource configuration?
We do not want to hardcode values which:
- Change between environments (dev, test, prod)
- Should be controlled by the installer, not the developer
- Would require recompilation to change
Possible approaches
| Approach | Usage |
|---|---|
| Environment Variables | Simple, universal |
| Command line arguments | Already used in the CLI tool |
| Configuration files | More user-friendly for many settings |
Here we use the Java properties files.
Creation of the server.properties file
Location: src/main/resources/server.properties (will be included in the JAR)
course-info.database=./courses.db
Loading into CourseServer
private static String loadDatabaseFilename() {
try (InputStream propertiesStream =
CourseServer.class.getResourceAsStream("/server.properties")) {
Properties properties = new Properties();
properties.load(propertiesStream);
return properties.getProperty("course-info.database");
} catch (IOException e) {
throw new IllegalStateException("Could not load database filename");
}
}
getResourceAsStream("/server.properties")loads the file from the classpath (absolute path with/)Properties.load()parses the properties filegetProperty("course-info.database")gets the value
6.3 Maven dependency management (dependencyManagement)
Problem: Duplicating versions
SLF4J and Jackson versions are duplicated in multiple POM files. If you want to update them, you have to modify several files.
Solution 1: Properties in parent POM
Move version properties to parent POM:
<!-- Dans le pom.xml parent -->
<properties>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<maven.compiler.release>17</maven.compiler.release>
<slf4j.version>1.7.36</slf4j.version>
<jackson.version>2.13.3</jackson.version>
</properties>
All submodules inherit these properties and can refer to them with ${slf4j.version}.
Solution 2: dependencyManagement section
The dependencyManagement section in the parent POM allows dependency versions and scopes to be declared only once. The submodules declare the dependency without version:
<!-- Dans le pom.xml parent -->
<dependencyManagement>
<dependencies>
<dependency>
<groupId>org.slf4j</groupId>
<artifactId>slf4j-api</artifactId>
<version>${slf4j.version}</version>
</dependency>
<dependency>
<groupId>org.slf4j</groupId>
<artifactId>slf4j-simple</artifactId>
<version>${slf4j.version}</version>
<scope>runtime</scope>
</dependency>
<dependency>
<groupId>org.junit.jupiter</groupId>
<artifactId>junit-jupiter</artifactId>
<version>5.8.2</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>com.fasterxml.jackson.core</groupId>
<artifactId>jackson-databind</artifactId>
<version>${jackson.version}</version>
</dependency>
<!-- ... -->
</dependencies>
</dependencyManagement>
In the submodules, we declare the dependency without version:
<!-- Dans course-info-cli/pom.xml -->
<dependency>
<groupId>org.slf4j</groupId>
<artifactId>slf4j-api</artifactId>
<!-- Pas de version : héritée du parent via dependencyManagement -->
</dependency>
Benefit: To update SLF4J, change slf4j.version in a single file.
6.4 Creating a standalone executable JAR
Problem
The .jar produced by Maven only contains our code. To run the application, you would need to provide all dependencies on the classpath:
# Ingérable en production
java -cp target/course-info-server.jar:lib/slf4j-api.jar:lib/jersey-server.jar:... \
com.pluralsight.courseinfo.server.CourseServer
Solution: Maven Shade Plugin
The Maven Shade Plugin creates a fat JAR (also called uber JAR) that contains our code and all dependencies in a single file:
<build>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-shade-plugin</artifactId>
<version>3.3.0</version>
<executions>
<execution>
<phase>package</phase>
<goals>
<goal>shade</goal>
</goals>
<configuration>
<transformers>
<!-- Définit la Main-Class dans le manifeste MANIFEST.MF -->
<transformer implementation="org.apache.maven.plugins.shade.resource.ManifestResourceTransformer">
<mainClass>com.pluralsight.courseinfo.server.CourseServer</mainClass>
</transformer>
<!-- Fusionne les fichiers META-INF/services (nécessaire pour Jersey) -->
<transformer implementation="org.apache.maven.plugins.shade.resource.ServicesResourceTransformer" />
</transformers>
</configuration>
</execution>
</executions>
</plugin>
</plugins>
</build>
Build and run
# Construire le fat JAR
mvn clean package
# Exécuter le serveur — une seule commande
java -jar target/course-info-server-1.0-SNAPSHOT.jar
The ServicesResourceTransformer is necessary because Jersey uses Java’s SPI (Service Provider Interface) mechanism to find its implementations, via files in META-INF/services/. This transformer correctly merges these files from the different JARs.
6.5 Next steps for the project
Ideas for extending the course-info system:
Functional improvements:
- Add a new course source (other than Pluralsight) in the CLI tool
- Add an endpoint
DELETE /courses/{id}(requires repository method +@DELETEinCourseResource) - Add update endpoint (
PUT /courses/{id}) - Create a course via REST API (
POST /courses)
Technical improvements:
- Replace H2 with PostgreSQL (JDBC driver available, schema management with Flyway or Liquibase)
- Introduce integration tests for the repository layer with a real H2 database
- Set up a CI/CD pipeline (GitHub Actions for example) which launches the tests automatically
- Create a Docker image to distribute the application
- Add pagination to the
GET /coursesendpoint
6.6 Next steps for the learner
Practical:
The best way to progress is to code. Extend the course-info project or create a new application.
Java frameworks to explore:
- Spring Framework / Spring Boot: the most popular Java framework, learning path available on Pluralsight
- Jakarta EE Web Profile: Java Enterprise, preview available with Jakarta EE Web Profile: The Big Picture
- Quarkus / Micronaut: modern cloud and microservices-oriented frameworks
Specific areas for further investigation:
- Maven → Maven Fundamentals (Pluralsight)
- Java HttpClient → Java Fundamentals: HttpClient (Pluralsight)
- Java Records → Java SE 17 Advanced Language Features (Pluralsight)
- JUnit 5 → Java SE 17 Unit Testing with JUnit (Pluralsight)
- JDBC → Java Core Libraries: JDBC 4 (Pluralsight)
- JAX-RS / Jersey → Jersey 3 Fundamentals (Pluralsight)
7. Complete project architecture
File structure
course-info/
├── pom.xml ← POM parent (multi-module)
├── db_init.sql ← Script SQL d'initialisation
├── sander-mak.json ← Données JSON de test
│
├── course-info-repository/ ← Module partagé (domain + JDBC)
│ ├── pom.xml
│ └── src/
│ ├── main/java/com/pluralsight/courseinfo/
│ │ ├── domain/
│ │ │ └── Course.java ← Record domaine
│ │ └── repository/
│ │ ├── CourseRepository.java ← Interface
│ │ ├── CourseJdbcRepository.java ← Implémentation JDBC (package-private)
│ │ └── RepositoryException.java ← Exception domaine
│ └── test/java/com/pluralsight/courseinfo/
│ └── domain/
│ └── CourseTest.java
│
├── course-info-cli/ ← Module CLI tool
│ ├── pom.xml
│ └── src/
│ ├── main/java/com/pluralsight/courseinfo/cli/
│ │ ├── CourseRetriever.java ← Main class
│ │ └── service/
│ │ ├── CourseRetrievalService.java ← Appels API HTTP
│ │ ├── CourseStorageService.java ← Stockage via Repository
│ │ └── PluralsightCourse.java ← Record (données API Pluralsight)
│ └── test/java/com/pluralsight/courseinfo/cli/service/
│ ├── CourseStorageServiceTest.java
│ └── PluralsightCourseTest.java
│
└── course-info-server/ ← Module serveur REST
├── pom.xml
└── src/
└── main/
├── java/com/pluralsight/courseinfo/server/
│ ├── CourseServer.java ← Main class (Grizzly + Jersey)
│ └── CourseResource.java ← Resource JAX-RS
└── resources/
└── server.properties ← Configuration externe
Module dependency graph
course-info-cli ──────────────────────────────────► course-info-repository
│ │
│ (utilise CourseRepository, Course) │ (définit Course,
│ │ CourseRepository,
│ │ CourseJdbcRepository)
course-info-server ────────────────────────────────► │
│ │
│ (utilise CourseRepository, Course) ▼
│ H2 Database (JDBC)
▼
JAX-RS / Jersey (REST API HTTP)
CLI tool execution flow
main(args) → retrieveCourses(authorId)
│
├─► CourseRetrievalService.getCoursesFor(authorId)
│ │
│ ├─► HttpClient.send(request) → API Pluralsight
│ ├─► ObjectMapper.readValue() → List<PluralsightCourse>
│ └─► return List<PluralsightCourse>
│
└─► CourseStorageService.storePluralsightCourses(courses)
│
└─► CourseRepository.saveCourse(course)
│
└─► JDBC MERGE INTO Courses ... → H2 (courses.db)
REST server execution flow
main() → CourseServer.main()
│
├─► loadDatabaseFilename() (depuis server.properties)
├─► CourseRepository.openCourseRepository("./courses.db")
├─► new CourseResource(courseRepository)
├─► ResourceConfig.register(courseResource)
└─► GrizzlyHttpServerFactory.createHttpServer(BASE_URI, config)
│
▼
GET /courses
│
└─► CourseResource.getCourses()
│
└─► courseRepository.getAllCourses() → H2 → List<Course>
stream().sorted(comparing(Course::id)).return JSON
POST /courses/{id}/notes (body: text plain)
│
└─► CourseResource.addNotes(id, notes)
│
└─► courseRepository.addNotes(id, notes) → H2 UPDATE
8. Full Source Code — Configuration Files
8.1 parent pom.xml
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0
http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>com.pluralsight</groupId>
<artifactId>course-info</artifactId>
<packaging>pom</packaging>
<version>1.0-SNAPSHOT</version>
<modules>
<module>course-info-repository</module>
<module>course-info-cli</module>
<module>course-info-server</module>
</modules>
<properties>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<maven.compiler.release>17</maven.compiler.release>
<slf4j.version>1.7.36</slf4j.version>
<jackson.version>2.13.3</jackson.version>
</properties>
<dependencyManagement>
<dependencies>
<dependency>
<groupId>org.slf4j</groupId>
<artifactId>slf4j-api</artifactId>
<version>${slf4j.version}</version>
</dependency>
<dependency>
<groupId>org.slf4j</groupId>
<artifactId>slf4j-simple</artifactId>
<version>${slf4j.version}</version>
<scope>runtime</scope>
</dependency>
<dependency>
<groupId>org.junit.jupiter</groupId>
<artifactId>junit-jupiter</artifactId>
<version>5.8.2</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>com.fasterxml.jackson.core</groupId>
<artifactId>jackson-databind</artifactId>
<version>${jackson.version}</version>
</dependency>
<dependency>
<groupId>com.fasterxml.jackson.core</groupId>
<artifactId>jackson-annotations</artifactId>
<version>${jackson.version}</version>
</dependency>
</dependencies>
</dependencyManagement>
<build>
<plugins>
<plugin>
<artifactId>maven-compiler-plugin</artifactId>
<version>3.10.1</version>
</plugin>
<plugin>
<artifactId>maven-surefire-plugin</artifactId>
<version>2.22.2</version>
</plugin>
</plugins>
</build>
</project>
8.2 pom.xml — course-info-repository
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0
http://maven.apache.org/maven-v4_0_0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>com.pluralsight</groupId>
<artifactId>course-info</artifactId>
<version>1.0-SNAPSHOT</version>
</parent>
<groupId>com.pluralsight</groupId>
<artifactId>course-info-repository</artifactId>
<packaging>jar</packaging>
<version>1.0-SNAPSHOT</version>
<name>Course Info Storage</name>
<dependencies>
<dependency>
<groupId>com.h2database</groupId>
<artifactId>h2</artifactId>
<version>2.1.214</version>
</dependency>
<dependency>
<groupId>org.junit.jupiter</groupId>
<artifactId>junit-jupiter</artifactId>
</dependency>
</dependencies>
</project>
8.3 pom.xml — course-info-cli
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0
http://maven.apache.org/xsd/maven-4.0.0.xsd">
<parent>
<artifactId>course-info</artifactId>
<groupId>com.pluralsight</groupId>
<version>1.0-SNAPSHOT</version>
</parent>
<modelVersion>4.0.0</modelVersion>
<artifactId>course-info-cli</artifactId>
<dependencies>
<dependency>
<groupId>com.pluralsight</groupId>
<artifactId>course-info-repository</artifactId>
<version>1.0-SNAPSHOT</version>
</dependency>
<dependency>
<groupId>org.slf4j</groupId>
<artifactId>slf4j-api</artifactId>
</dependency>
<dependency>
<groupId>org.slf4j</groupId>
<artifactId>slf4j-simple</artifactId>
</dependency>
<dependency>
<groupId>com.fasterxml.jackson.core</groupId>
<artifactId>jackson-databind</artifactId>
</dependency>
<dependency>
<groupId>com.fasterxml.jackson.core</groupId>
<artifactId>jackson-annotations</artifactId>
</dependency>
<dependency>
<groupId>org.junit.jupiter</groupId>
<artifactId>junit-jupiter</artifactId>
</dependency>
</dependencies>
</project>
8.4 pom.xml — course-info-server
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0
http://maven.apache.org/maven-v4_0_0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>com.pluralsight</groupId>
<artifactId>course-info</artifactId>
<version>1.0-SNAPSHOT</version>
</parent>
<groupId>com.pluralsight</groupId>
<artifactId>course-info-server</artifactId>
<packaging>jar</packaging>
<version>1.0-SNAPSHOT</version>
<name>Course Info API server</name>
<properties>
<jersey.version>3.0.4</jersey.version>
</properties>
<dependencies>
<dependency>
<groupId>com.pluralsight</groupId>
<artifactId>course-info-repository</artifactId>
<version>1.0-SNAPSHOT</version>
</dependency>
<dependency>
<groupId>org.slf4j</groupId>
<artifactId>slf4j-api</artifactId>
</dependency>
<dependency>
<groupId>org.slf4j</groupId>
<artifactId>slf4j-simple</artifactId>
</dependency>
<dependency>
<groupId>org.slf4j</groupId>
<artifactId>jul-to-slf4j</artifactId>
<version>${slf4j.version}</version>
</dependency>
<dependency>
<groupId>com.fasterxml.jackson.datatype</groupId>
<artifactId>jackson-datatype-jdk8</artifactId>
<version>${jackson.version}</version>
<scope>runtime</scope>
</dependency>
<dependency>
<groupId>jakarta.ws.rs</groupId>
<artifactId>jakarta.ws.rs-api</artifactId>
<version>3.1.0</version>
</dependency>
<dependency>
<groupId>org.glassfish.jersey.core</groupId>
<artifactId>jersey-server</artifactId>
<version>${jersey.version}</version>
</dependency>
<dependency>
<groupId>org.glassfish.jersey.inject</groupId>
<artifactId>jersey-hk2</artifactId>
<version>${jersey.version}</version>
<scope>runtime</scope>
</dependency>
<dependency>
<groupId>org.glassfish.jersey.media</groupId>
<artifactId>jersey-media-json-jackson</artifactId>
<version>${jersey.version}</version>
<scope>runtime</scope>
</dependency>
<dependency>
<groupId>org.glassfish.jersey.containers</groupId>
<artifactId>jersey-container-grizzly2-http</artifactId>
<version>${jersey.version}</version>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-shade-plugin</artifactId>
<version>3.3.0</version>
<executions>
<execution>
<phase>package</phase>
<goals>
<goal>shade</goal>
</goals>
<configuration>
<transformers>
<transformer implementation="org.apache.maven.plugins.shade.resource.ManifestResourceTransformer">
<mainClass>com.pluralsight.courseinfo.server.CourseServer</mainClass>
</transformer>
<transformer implementation="org.apache.maven.plugins.shade.resource.ServicesResourceTransformer" />
</transformers>
</configuration>
</execution>
</executions>
</plugin>
</plugins>
</build>
</project>
9. Full source code — Course info repository module
9.1 Course.java (domain)
package com.pluralsight.courseinfo.domain;
import java.util.Optional;
public record Course(String id, String name, long length, String url, Optional<String> notes) {
// Constructeur compact : validation des champs obligatoires
public Course {
filled(id);
filled(name);
filled(url);
notes.ifPresent(Course::filled);
}
private static void filled(String s) {
if (s == null || s.isBlank()) {
throw new IllegalArgumentException("No value present!");
}
}
}
Explanations:
record: Java 16+ immutable data class. Automatically generate constructor, getters,equals,hashCode,toString- Compact constructor (
public Course { ... }): special form without re-declaring parameters, executed after component assignment Optional<String> notes: explicit representation of a potentially absent value. Preferable tonullto force the consumer code to handle the absent casenotes.ifPresent(Course::filled): callsfilledonly if notes are present
9.2 CourseRepository.java (interface)
package com.pluralsight.courseinfo.repository;
import com.pluralsight.courseinfo.domain.Course;
import java.util.List;
public interface CourseRepository {
void saveCourse(Course course);
List<Course> getAllCourses();
void addNotes(String id, String notes);
// Factory method statique — masque l'implémentation concrète
static CourseRepository openCourseRepository(String databaseFile) {
return new CourseJdbcRepository(databaseFile);
}
}
Explanations:
- Pure interface: defines the contract without implementation details
- The static factory method
openCourseRepositoryallows consumers to obtain an instance without knowingCourseJdbcRepository CourseJdbcRepositoryis package-private (nopublic) → external consumers cannot instantiate it directly
9.3 CourseJdbcRepository.java (JDBC implementation)
package com.pluralsight.courseinfo.repository;
import com.pluralsight.courseinfo.domain.Course;
import org.h2.jdbcx.JdbcDataSource;
import javax.sql.DataSource;
import java.sql.*;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.Optional;
class CourseJdbcRepository implements CourseRepository {
private static final String H2_DATABASE_URL =
"jdbc:h2:file:%s;AUTO_SERVER=TRUE;INIT=RUNSCRIPT FROM './db_init.sql'";
// SQL avec text blocks Java 15+
private static final String INSERT_COURSE = """
MERGE INTO Courses (id, name, length, url)
VALUES (?, ?, ?, ?)
""";
private static final String ADD_NOTES = """
UPDATE Courses SET notes = ?
WHERE id = ?
""";
private final DataSource dataSource;
CourseJdbcRepository(String databaseFile) {
JdbcDataSource jdbcDataSource = new JdbcDataSource();
jdbcDataSource.setURL(H2_DATABASE_URL.formatted(databaseFile));
this.dataSource = jdbcDataSource;
}
@Override
public List<Course> getAllCourses() {
try (Connection connection = dataSource.getConnection()) {
Statement statement = connection.createStatement();
ResultSet resultSet = statement.executeQuery("SELECT * FROM COURSES");
List<Course> courses = new ArrayList<>();
while (resultSet.next()) {
Course course = new Course(
resultSet.getString(1), // id
resultSet.getString(2), // name
resultSet.getLong(3), // length
resultSet.getString(4), // url
Optional.ofNullable(resultSet.getString(5)) // notes
);
courses.add(course);
}
return Collections.unmodifiableList(courses);
} catch (SQLException e) {
throw new RepositoryException("Failed to retrieve courses", e);
}
}
@Override
public void saveCourse(Course course) {
executeStatement(INSERT_COURSE, statement -> {
statement.setString(1, course.id());
statement.setString(2, course.name());
statement.setLong(3, course.length());
statement.setString(4, course.url());
statement.execute();
}, "Failed to insert " + course);
}
@Override
public void addNotes(String id, String notes) {
executeStatement(ADD_NOTES, statement -> {
statement.setString(1, notes);
statement.setString(2, id);
}, "Failed to add notes to " + id);
}
private void executeStatement(String sql,
PreparedStatementConfigurer configurer,
String errorMsg) {
try (Connection connection = dataSource.getConnection()) {
PreparedStatement statement = connection.prepareStatement(sql);
configurer.configure(statement);
statement.execute();
} catch (SQLException e) {
throw new RepositoryException(errorMsg, e);
}
}
// Interface fonctionnelle personnalisée pour configurer un PreparedStatement
@FunctionalInterface
interface PreparedStatementConfigurer {
void configure(PreparedStatement statement) throws SQLException;
}
}
Key explanations:
- Package-private class (
classwithoutpublic): implementation encapsulation MERGE INTO: upsert behavior — create if absent, update if present (based on primary keyid)PreparedStatementwith?: prevent SQL injections. Values are never interpolated in SQL query- Try-with-resources:
try (Connection conn = ...)guarantees the connection is closed even in the event of an exception @FunctionalInterface PreparedStatementConfigurer: custom functional interface allowing you to passPreparedStatementconfiguration code as lambdaCollections.unmodifiableList(): returns an unmodifiable list to protect internal state
9.4 RepositoryException.java
package com.pluralsight.courseinfo.repository;
import java.sql.SQLException;
public class RepositoryException extends RuntimeException {
public RepositoryException(String message, SQLException e) {
super(message, e);
}
}
Explanations:
- Inherits from
RuntimeException: consumers are not required to catch it (unchecked exception) - Encapsulate
SQLException: SQL detail is not exposed in the signature of theCourseRepositoryinterface - Original exception is kept as cause (exception chaining) for debugging
9.5 db_init.sql
CREATE TABLE IF NOT EXISTS COURSES(
ID VARCHAR PRIMARY KEY NOT NULL,
NAME VARCHAR NOT NULL,
LENGTH INT NOT NULL,
URL VARCHAR NOT NULL,
NOTES VARCHAR
);
Explanations:
IF NOT EXISTS: idempotent — can be executed multiple times without errorNOTES VARCHARwithoutNOT NULL: the column can containNULL(optional notes)- This script is run automatically via the
INIT=RUNSCRIPT FROM './db_init.sql'parameter in the JDBC H2 URL
9.6 CourseTest.java
package com.pluralsight.courseinfo.domain;
import org.junit.jupiter.api.Test;
import java.util.Optional;
import static org.junit.jupiter.api.Assertions.assertThrows;
public class CourseTest {
@Test
void rejectNullComponents() {
assertThrows(IllegalArgumentException.class, () ->
new Course(null, null, 1, null, Optional.empty()));
}
@Test
void rejectBlankNotes() {
assertThrows(IllegalArgumentException.class, () ->
new Course("1", "title", 1, "url", Optional.of("")));
}
}
Explanations:
assertThrows(ExceptionClass, () -> ...): checks that an exception is thrown- These tests validate the compact constructor of the
Courserecord Optional.of(""): notes present but empty — must be rejected by validation
10. Complete source code — Course info cli module
10.1 CourseRetriever.java (main)
package com.pluralsight.courseinfo.cli;
import com.pluralsight.courseinfo.cli.service.CourseRetrievalService;
import com.pluralsight.courseinfo.cli.service.CourseStorageService;
import com.pluralsight.courseinfo.cli.service.PluralsightCourse;
import com.pluralsight.courseinfo.repository.CourseRepository;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.util.List;
import static java.util.function.Predicate.not;
public class CourseRetriever {
private static Logger LOG = LoggerFactory.getLogger(CourseRetriever.class);
public static void main(String... args) {
LOG.info("CourseRetriever starting");
if (args.length == 0) {
LOG.warn("Please provide an author name as first argument.");
return;
}
try {
retrieveCourses(args[0]);
} catch (Exception e) {
LOG.error("Unexpected error", e);
}
}
private static void retrieveCourses(String authorId) {
LOG.info("Retrieving courses for author '{}'", authorId);
CourseRetrievalService courseRetrievalService = new CourseRetrievalService();
CourseRepository courseRepository =
CourseRepository.openCourseRepository("./courses.db");
CourseStorageService courseStorageService =
new CourseStorageService(courseRepository);
List<PluralsightCourse> coursesToStore = courseRetrievalService
.getCoursesFor(authorId)
.stream()
.filter(not(PluralsightCourse::isRetired))
.toList();
LOG.info("Retrieved the following {} courses {}",
coursesToStore.size(), coursesToStore);
courseStorageService.storePluralsightCourses(coursesToStore);
LOG.info("Courses successfully stored");
}
}
Explanations:
main(String... args): Java varargs, equivalent tomain(String[] args)- High-level exception handler (
try-catch Exception): centralizes the handling of unexpected errors not(PluralsightCourse::isRetired):Predicate.notwith method reference (static import).toList(): Java 16, creates an unmodifiable list from a stream
10.2 CourseRetrievalService.java
package com.pluralsight.courseinfo.cli.service;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.JavaType;
import com.fasterxml.jackson.databind.ObjectMapper;
import java.io.IOException;
import java.net.URI;
import java.net.http.HttpClient;
import java.net.http.HttpRequest;
import java.net.http.HttpResponse;
import java.util.List;
public class CourseRetrievalService {
// URL de l'API Pluralsight — %s sera remplacé par l'authorId
// Fallback local (pour les cas où l'API n'est pas accessible) :
// private static final String PS_URI =
// "https://raw.githubusercontent.com/sandermak-ps/course-info-java-17/master/sander-mak.json";
private static final String PS_URI =
"https://app.pluralsight.com/profile/data/author/%s/all-content";
// HttpClient partagé, thread-safe, configuré une seule fois
private static final HttpClient CLIENT = HttpClient
.newBuilder()
.followRedirects(HttpClient.Redirect.ALWAYS) // Suivre les redirections
.build();
private static final ObjectMapper OBJECT_MAPPER = new ObjectMapper();
public List<PluralsightCourse> getCoursesFor(String authorId) {
HttpRequest request = HttpRequest
.newBuilder(URI.create(PS_URI.formatted(authorId)))
.GET()
.build();
try {
HttpResponse<String> response =
CLIENT.send(request, HttpResponse.BodyHandlers.ofString());
// Switch expression — retourne une valeur selon le code de statut HTTP
return switch (response.statusCode()) {
case 200 -> toPluralsightCourses(response);
case 404 -> List.of();
default -> throw new RuntimeException(
"Pluralsight API call failed with status code "
+ response.statusCode());
};
} catch (IOException | InterruptedException e) {
throw new RuntimeException("Could not call Pluralsight API", e);
}
}
private List<PluralsightCourse> toPluralsightCourses(HttpResponse<String> response)
throws JsonProcessingException {
// JavaType est nécessaire à cause de l'effacement de type (type erasure)
// pour le type générique List<PluralsightCourse>
JavaType returnType = OBJECT_MAPPER.getTypeFactory()
.constructCollectionType(List.class, PluralsightCourse.class);
return OBJECT_MAPPER.readValue(response.body(), returnType);
}
}
Explanations:
HttpClient.Redirect.ALWAYS: automatically follow HTTP redirects (301, 302, etc.)- Switch expression (Java 14+):
switch (expr) { case x -> value; ... }returns a value catch (IOException | InterruptedException e): multi-catch with|JavaType+constructCollectionType: bypassing Java type erasure to deserialize aList<PluralsightCourse>with Jackson
10.3 PluralsightCourse.java (record)
package com.pluralsight.courseinfo.cli.service;
import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
import java.time.Duration;
import java.time.LocalTime;
// Ignorer les propriétés JSON qui ne correspondent à aucun composant du record
@JsonIgnoreProperties(ignoreUnknown = true)
public record PluralsightCourse(
String id,
String title,
String duration, // Format: "HH:mm:ss.SSSSSSS" (ex: "01:08:54.9613330")
String contentUrl,
boolean isRetired) {
public long durationInMinutes() {
// LocalTime.parse() parse la durée comme une heure (HH:mm:ss)
// Duration.between(LocalTime.MIN, ...) calcule la durée depuis minuit
return Duration.between(
LocalTime.MIN,
LocalTime.parse(duration())
).toMinutes();
}
}
Explanations:
- Java Record (Java 16): immutable data class. Components are declared in parentheses
@JsonIgnoreProperties(ignoreUnknown = true): Jackson ignores unmapped JSON fields (robustness in the face of API evolution)duration(): access method automatically generated by the record (nogetprefix)LocalTime.parse(): parse a string like"01:08:54.9613330"into aLocalTimeobjectLocalTime.MIN: represents00:00:00— used as a starting point to calculate the duration
10.4 CourseStorageService.java
package com.pluralsight.courseinfo.cli.service;
import com.pluralsight.courseinfo.domain.Course;
import com.pluralsight.courseinfo.repository.CourseRepository;
import java.util.List;
import java.util.Optional;
public class CourseStorageService {
private static final String PS_BASE_URL = "https://app.pluralsight.com";
private final CourseRepository courseRepository;
public CourseStorageService(CourseRepository courseRepository) {
this.courseRepository = courseRepository;
}
public void storePluralsightCourses(List<PluralsightCourse> psCourses) {
for (PluralsightCourse psCourse : psCourses) {
Course course = new Course(
psCourse.id(),
psCourse.title(),
psCourse.durationInMinutes(),
PS_BASE_URL + psCourse.contentUrl(), // URL partielle → URL complète
Optional.empty() // Pas de notes à l'insertion
);
courseRepository.saveCourse(course);
}
}
}
Explanations:
- Translate
PluralsightCourse(external API DTO) toCourse(domain object) PS_BASE_URL + psCourse.contentUrl(): the Pluralsight API returns relative paths (ex:/library/courses/...), we prefix them with the base domainOptional.empty(): notes are missing on initial insertion
10.5 CourseStorageServiceTest.java
package com.pluralsight.courseinfo.cli.service;
import com.pluralsight.courseinfo.domain.Course;
import com.pluralsight.courseinfo.repository.CourseRepository;
import org.junit.jupiter.api.Test;
import java.util.ArrayList;
import java.util.List;
import java.util.Optional;
import static org.junit.jupiter.api.Assertions.*;
class CourseStorageServiceTest {
@Test
void storePluralsightCourses() {
// Utilisation d'un repository en mémoire (stub) — pas de vraie base de données
CourseRepository repository = new InMemoryCourseRepository();
CourseStorageService courseStorageService = new CourseStorageService(repository);
PluralsightCourse ps1 = new PluralsightCourse(
"1", "Title 1", "01:40:00.123", "/url-1", false);
courseStorageService.storePluralsightCourses(List.of(ps1));
Course expected = new Course(
"1", "Title 1", 100,
"https://app.pluralsight.com/url-1",
Optional.empty());
assertEquals(List.of(expected), repository.getAllCourses());
}
// Stub — implémentation en mémoire de CourseRepository pour les tests
static class InMemoryCourseRepository implements CourseRepository {
private final List<Course> courses = new ArrayList<>();
@Override
public void saveCourse(Course course) {
courses.add(course);
}
@Override
public List<Course> getAllCourses() {
return courses;
}
@Override
public void addNotes(String id, String notes) {
throw new UnsupportedOperationException();
}
}
}
Explanations:
- End-to-end testing of the service without real database
InMemoryCourseRepositoryimplementsCourseRepositoryby storing courses in memory (stub)- The duration
"01:40:00.123"must be converted to100minutes (1h40) - URL
"/url-1"must be prefixed with"https://app.pluralsight.com"→"https://app.pluralsight.com/url-1" - Illustrates how the
CourseRepositoryinterface improves testability
10.6 PluralsightCourseTest.java
package com.pluralsight.courseinfo.cli.service;
import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.CsvSource;
import static org.junit.jupiter.api.Assertions.*;
class PluralsightCourseTest {
@ParameterizedTest
@CsvSource(textBlock = """
01:08:54.9613330, 68
00:05:37, 5
00:00:00.0, 0
""")
void durationInMinutes(String input, long expected) {
PluralsightCourse course =
new PluralsightCourse("id", "Test course", input, "url", false);
assertEquals(expected, course.durationInMinutes());
}
}
Explanations:
@ParameterizedTest: JUnit 5 runs the test once per line of the CSV@CsvSource(textBlock = """..."""): Java 15+ text block for multiline data- JUnit 5 automatically converts CSV values to parameter types (
String,long) - Three test cases: normal duration with milliseconds, short duration, zero duration
- The name of each test run in the results matches the CSV line
11. Full source code — Course info server module
11.1 CourseServer.java (main)
package com.pluralsight.courseinfo.server;
import com.pluralsight.courseinfo.repository.CourseRepository;
import org.glassfish.jersey.grizzly2.httpserver.GrizzlyHttpServerFactory;
import org.glassfish.jersey.server.ResourceConfig;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.slf4j.bridge.SLF4JBridgeHandler;
import java.io.IOException;
import java.io.InputStream;
import java.net.URI;
import java.util.Properties;
import java.util.logging.LogManager;
public class CourseServer {
// Bloc statique : exécuté au chargement de la classe, avant main()
// Redirige le JDK logging (java.util.logging) vers SLF4J
static {
LogManager.getLogManager().reset(); // Supprime les handlers JDK logging par défaut
SLF4JBridgeHandler.install(); // Installe le bridge JUL → SLF4J
}
private static final Logger LOG = LoggerFactory.getLogger(CourseServer.class);
private static final String BASE_URI = "http://localhost:8080/";
public static void main(String... args) {
String databaseFilename = loadDatabaseFilename();
LOG.info("Starting HTTP server with database {}", databaseFilename);
CourseRepository courseRepository =
CourseRepository.openCourseRepository("./courses.db");
// Configuration Jersey : enregistrement de la resource JAX-RS
ResourceConfig config = new ResourceConfig()
.register(new CourseResource(courseRepository));
// Démarrage du serveur HTTP Grizzly
GrizzlyHttpServerFactory.createHttpServer(URI.create(BASE_URI), config);
}
private static String loadDatabaseFilename() {
try (InputStream propertiesStream =
CourseServer.class.getResourceAsStream("/server.properties")) {
Properties properties = new Properties();
properties.load(propertiesStream);
return properties.getProperty("course-info.database");
} catch (IOException e) {
throw new IllegalStateException("Could not load database filename");
}
}
}
Explanations:
- Static block (
static { ... }): executed only once when the class is first loaded, beforemain() SLF4JBridgeHandler: bridge of thejul-to-slf4jlibrary which redirects all calls to JDK logging (used by Jersey) to SLF4JLogManager.getLogManager().reset(): removes the default console handler from JDK logging to avoid duplicatesResourceConfig.register(new CourseResource(...)): we instantiateCourseResourcemanually (rather than letting Jersey do it via reflection) to pass theCourseRepositoryby constructorgetResourceAsStream("/server.properties"): load the file from the classpath (the initial/indicates the root of the classpath)
11.2 CourseResource.java (JAX-RS)
package com.pluralsight.courseinfo.server;
import com.pluralsight.courseinfo.domain.Course;
import com.pluralsight.courseinfo.repository.CourseRepository;
import com.pluralsight.courseinfo.repository.RepositoryException;
import jakarta.ws.rs.*;
import jakarta.ws.rs.core.MediaType;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.util.stream.Stream;
import static java.util.Comparator.comparing;
@Path("courses") // Tous les endpoints sont sous /courses
public class CourseResource {
private static final Logger LOG = LoggerFactory.getLogger(CourseResource.class);
private CourseRepository courseRepository;
public CourseResource(CourseRepository courseRepository) {
this.courseRepository = courseRepository;
}
@GET
@Produces(MediaType.APPLICATION_JSON) // Produit du JSON
public Stream<Course> getCourses() {
try {
return courseRepository
.getAllCourses()
.stream()
.sorted(comparing(Course::id)); // Tri stable par ID
} catch (RepositoryException e) {
LOG.error("Could not retrieve courses from the database", e);
throw new NotFoundException(); // HTTP 404
}
}
@POST
@Path("/{id}/notes") // Chemin : /courses/{id}/notes
@Consumes(MediaType.TEXT_PLAIN) // Corps de la requête : texte brut
public void addNotes(@PathParam("id") String id, String notes) {
courseRepository.addNotes(id, notes);
// Retour void → HTTP 204 No Content
}
}
Explanations of JAX-RS annotations:
| Annotation | Role |
|---|---|
@Path("courses") | Prefix the path of all endpoints in the class |
@GET | Responds to HTTP GET requests |
@POST | Responds to HTTP POST requests |
@Produces(APPLICATION_JSON) | The response body is JSON |
@Consumes(TEXT_PLAIN) | The expected request body is plain text |
@Path("/{id}/notes") | Relative path with dynamic placeholder {id} |
@PathParam("id") | Binds the placeholder {id} to the Java parameter String id |
Stream<Course>in return: Jersey (via jackson) serializes the stream into a JSON arrayNotFoundException(): JAX-RS exception which automatically generates an HTTP 404 response
11.3 server.properties
course-info.database=./courses.db
This file is placed in src/main/resources/ and will be included in the JAR during the Maven build. It is loaded from the classpath via getResourceAsStream("/server.properties").
12. Example data — sander mak.json
File sander-mak.json contains Pluralsight API sample data for author sander-mak. It is used as a local fallback if the Pluralsight API is not accessible.
Structure of a course object in this JSON:
{
"title": "What's New in Java 15",
"id": "b6e31e25-ed0b-4bd1-8d1a-4854f63a268c",
"status": "published",
"level": "Intermediate",
"duration": "01:08:54.9613330",
"displayDate": "2020-10-28T00:00:00+00:00",
"contentRating": {
"averageRating": 4.94444,
"numberOfRaters": 36
},
"authors": [
{
"handle": "sander-mak",
"firstName": "Sander",
"lastName": "Mak"
}
],
"type": "course",
"contentUrl": "/library/courses/java-15-whats-new",
"isRetired": false,
"isNew": false
}
Matching record PluralsightCourse:
| JSON field | Record component | Java type |
|---|---|---|
id | id | String |
title | title | String |
duration | duration | String (converted to minutes via durationInMinutes()) |
contentUrl | contentUrl | String (prefixed with https://app.pluralsight.com) |
isRetired | isRetired | boolean |
| All other fields | — | Ignored by @JsonIgnoreProperties(ignoreUnknown = true) |
Search Terms
application · java · se · backend · architecture · full-stack · web · pom.xml · maven · repository · record · source · dependencies · dependency · jackson · added · api · database · jax-rs · jdbc · logging · method · server · cli