////
Licensed to the Apache Software Foundation (ASF) under one
or more contributor license agreements.  See the NOTICE file
distributed with this work for additional information
regarding copyright ownership.  The ASF licenses this file
to you under the Apache License, Version 2.0 (the
"License"); you may not use this file except in compliance
with the License.  You may obtain a copy of the License at

  http://www.apache.org/licenses/LICENSE-2.0

Unless required by applicable law or agreed to in writing,
software distributed under the License is distributed on an
"AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
KIND, either express or implied.  See the License for the
specific language governing permissions and limitations
under the License.
////

[[springBootJavaQuickStart]]
= Spring Boot Java quick start
:doctype: book
:imagesdir: ../..
:sectnums:
:icons: font
include::../../_attributes.adoc[]


// Keep this in sync with quarkus-quickstart.adoc where applicable

This guide walks you through the process of creating a Spring Boot application
with https://www.optaplanner.org/[OptaPlanner]'s constraint solving Artificial Intelligence (AI).

== What you will build

You will build a REST application that optimizes a school timetable for students and teachers:

image::quickstart/school-timetabling/schoolTimetablingScreenshot.png[]

Your service will assign `Lesson` instances to `Timeslot` and `Room` instances automatically
by using AI to adhere to hard and soft scheduling _constraints_, such as the following examples:

* A room can have at most one lesson at the same time.
* A teacher can teach at most one lesson at the same time.
* A student can attend at most one lesson at the same time.
* A teacher prefers to teach all lessons in the same room.
* A teacher prefers to teach sequential lessons and dislikes gaps between lessons.
* A student dislikes sequential lessons on the same subject.

Mathematically speaking, school timetabling is an _NP-hard_ problem.
This means it is difficult to scale.
Simply brute force iterating through all possible combinations takes millions of years for a non-trivial data set, even on a supercomputer.
Luckily, AI constraint solvers such as OptaPlanner have advanced algorithms
that deliver a near-optimal solution in a reasonable amount of time.

== Solution source code

Follow the instructions in the next sections to create the application step by step (recommended).

Alternatively, you can also skip right to the completed example:

. Clone the Git repository:
+
[source,shell,subs=attributes+]
----
$ git clone {quickstarts-clone-url}
----
+
or download an {quickstarts-archive-url}[archive].

. Find the solution in {spring-boot-quickstart-url}[the `technology` directory]
and run it (see its README file).

== Prerequisites

To complete this guide, you need:

* https://adoptopenjdk.net/[JDK] {java-version}+ with `JAVA_HOME` configured appropriately
* https://maven.apache.org/download.html[Apache Maven] {maven-version}+ or https://gradle.org/install/[Gradle] 4+
* An IDE, such as https://www.jetbrains.com/idea[IntelliJ IDEA], VSCode or Eclipse

== The build file and the dependencies

Create a Spring Boot application with the following dependencies:

* Spring Web (`spring-boot-starter-web`)
* OptaPlanner (`optaplanner-spring-boot-starter`)

If you choose Maven, your `pom.xml` file has the following content:

[source,xml,subs=attributes+]
----
<?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>
    <parent>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-parent</artifactId>
        <version>{spring-boot-version}</version>
    </parent>

    <groupId>org.acme</groupId>
    <artifactId>optaplanner-spring-boot-school-timetabling-quickstart</artifactId>
    <version>1.0-SNAPSHOT</version>

    <properties>
        <java.version>{java-version}</java.version>
        <version.org.optaplanner>{optaplanner-version}</version.org.optaplanner>
    </properties>

    <dependencyManagement>
        <dependencies>
            <dependency>
                <groupId>org.optaplanner</groupId>
                <artifactId>optaplanner-bom</artifactId>
                <version>${version.org.optaplanner}</version>
                <type>pom</type>
                <scope>import</scope>
            </dependency>
        </dependencies>
    </dependencyManagement>
    <dependencies>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-data-rest</artifactId>
        </dependency>
        <dependency>
            <groupId>org.optaplanner</groupId>
            <artifactId>optaplanner-spring-boot-starter</artifactId>
        </dependency>

        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-test</artifactId>
            <scope>test</scope>
        </dependency>
        <dependency>
            <groupId>org.optaplanner</groupId>
            <artifactId>optaplanner-test</artifactId>
            <scope>test</scope>
        </dependency>
    </dependencies>

    <build>
        <plugins>
            <plugin>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-maven-plugin</artifactId>
            </plugin>
        </plugins>
    </build>
</project>
----

On the other hand, in Gradle, your `build.gradle` file has this content:

[source,groovy,subs=attributes+]
----
plugins {
    id "org.springframework.boot" version "{spring-boot-version}"
    id "io.spring.dependency-management" version "1.0.11.RELEASE"
    id "java"
}

def optaplannerVersion = "{optaplanner-version}"

group = "org.acme"
version = "1.0-SNAPSHOT"
sourceCompatibility = "11"

repositories {
    mavenCentral()
}

dependencies {
    implementation "org.springframework.boot:spring-boot-starter-web"
    implementation "org.springframework.boot:spring-boot-starter-data-rest"
    testImplementation("org.springframework.boot:spring-boot-starter-test")

    implementation platform("org.optaplanner:optaplanner-bom:${optaplannerVersion}")
    implementation "org.optaplanner:optaplanner-spring-boot-starter"
    testImplementation("org.optaplanner:optaplanner-test")
}

test {
    useJUnitPlatform()
}
----

include::../school-timetabling/school-timetabling-model.adoc[leveloffset=+1]
include::../school-timetabling/school-timetabling-constraints.adoc[leveloffset=+1]
include::../school-timetabling/school-timetabling-solution.adoc[leveloffset=+1]

== Create the solver service

Now you are ready to put everything together and create a REST service.
But solving planning problems on REST threads causes HTTP timeout issues.
Therefore, the Spring Boot starter injects a `SolverManager` instance,
which runs solvers in a separate thread pool
and can solve multiple datasets in parallel.

Create the `src/main/java/org/acme/schooltimetabling/rest/TimeTableController.java` class:

[source,java]
----
package org.acme.schooltimetabling.rest;

import java.util.UUID;
import java.util.concurrent.ExecutionException;

import org.acme.schooltimetabling.domain.TimeTable;
import org.optaplanner.core.api.solver.SolverJob;
import org.optaplanner.core.api.solver.SolverManager;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

@RestController
@RequestMapping("/timeTable")
public class TimeTableController {

    @Autowired
    private SolverManager<TimeTable, UUID> solverManager;

    @PostMapping("/solve")
    public TimeTable solve(@RequestBody TimeTable problem) {
        UUID problemId = UUID.randomUUID();
        // Submit the problem to start solving
        SolverJob<TimeTable, UUID> solverJob = solverManager.solve(problemId, problem);
        TimeTable solution;
        try {
            // Wait until the solving ends
            solution = solverJob.getFinalBestSolution();
        } catch (InterruptedException | ExecutionException e) {
            throw new IllegalStateException("Solving failed.", e);
        }
        return solution;
    }

}
----

For simplicity's sake, this initial implementation waits for the solver to finish,
which can still cause an HTTP timeout.
The _complete_ implementation avoids HTTP timeouts much more elegantly.

== Set the termination time

Without a termination setting or a `terminationEarly()` event, the solver runs forever.
To avoid that, limit the solving time to five seconds.
That is short enough to avoid the HTTP timeout.

Create the `src/main/resources/application.properties` file:

[source,properties]
----
# The solver runs only for 5 seconds to avoid a HTTP timeout in this simple implementation.
# It's recommended to run for at least 5 minutes ("5m") otherwise.
optaplanner.solver.termination.spent-limit=5s
----

OptaPlanner returns _the best solution_ found in the available termination time.
Due to xref:optimization-algorithms/optimization-algorithms.adoc#doesPlannerFindTheOptimalSolution[the nature of NP-hard problems],
the best solution might not be optimal, especially for larger datasets.
Increase the termination time to potentially find a better solution.

== Make the application executable

Package everything into a single executable JAR file driven by a standard Java `main()` method:

Replace the `DemoApplication.java` class created by Spring Initializr
with the `src/main/java/org/acme/schooltimetabling/TimeTableSpringBootApp.java` class:

[source,java]
----
package org.acme.schooltimetabling;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;

@SpringBootApplication
public class TimeTableSpringBootApp {

    public static void main(String[] args) {
        SpringApplication.run(TimeTableSpringBootApp.class, args);
    }

}
----

Run that `TimeTableSpringBootApp` class as the main class of a normal Java application.

=== Try the application

Now that the application is running, you can test the REST service.
You can use any REST client you wish.
The following example uses the Linux command `curl` to send a POST request:

[source,shell]
----
$ curl -i -X POST http://localhost:8080/timeTable/solve -H "Content-Type:application/json" -d '{"timeslotList":[{"dayOfWeek":"MONDAY","startTime":"08:30:00","endTime":"09:30:00"},{"dayOfWeek":"MONDAY","startTime":"09:30:00","endTime":"10:30:00"}],"roomList":[{"name":"Room A"},{"name":"Room B"}],"lessonList":[{"id":1,"subject":"Math","teacher":"A. Turing","studentGroup":"9th grade"},{"id":2,"subject":"Chemistry","teacher":"M. Curie","studentGroup":"9th grade"},{"id":3,"subject":"French","teacher":"M. Curie","studentGroup":"10th grade"},{"id":4,"subject":"History","teacher":"I. Jones","studentGroup":"10th grade"}]}'
----

After about five seconds, according to the termination spent time defined in your `application.properties`,
the service returns an output similar to the following example:

[source]
----
HTTP/1.1 200
Content-Type: application/json
...

{"timeslotList":...,"roomList":...,"lessonList":[{"id":1,"subject":"Math","teacher":"A. Turing","studentGroup":"9th grade","timeslot":{"dayOfWeek":"MONDAY","startTime":"08:30:00","endTime":"09:30:00"},"room":{"name":"Room A"}},{"id":2,"subject":"Chemistry","teacher":"M. Curie","studentGroup":"9th grade","timeslot":{"dayOfWeek":"MONDAY","startTime":"09:30:00","endTime":"10:30:00"},"room":{"name":"Room A"}},{"id":3,"subject":"French","teacher":"M. Curie","studentGroup":"10th grade","timeslot":{"dayOfWeek":"MONDAY","startTime":"08:30:00","endTime":"09:30:00"},"room":{"name":"Room B"}},{"id":4,"subject":"History","teacher":"I. Jones","studentGroup":"10th grade","timeslot":{"dayOfWeek":"MONDAY","startTime":"09:30:00","endTime":"10:30:00"},"room":{"name":"Room B"}}],"score":"0hard/0soft"}
----

Notice that your application assigned all four lessons to one of the two time slots and one of the two rooms.
Also notice that it conforms to all hard constraints.
For example, M. Curie's two lessons are in different time slots.

On the server side, the `info` log shows what OptaPlanner did in those five seconds:

[source,options="nowrap"]
----
... Solving started: time spent (33), best score (-8init/0hard/0soft), environment mode (REPRODUCIBLE), random (JDK with seed 0).
... Construction Heuristic phase (0) ended: time spent (73), best score (0hard/0soft), score calculation speed (459/sec), step total (4).
... Local Search phase (1) ended: time spent (5000), best score (0hard/0soft), score calculation speed (28949/sec), step total (28398).
... Solving ended: time spent (5000), best score (0hard/0soft), score calculation speed (28524/sec), phase total (2), environment mode (REPRODUCIBLE).
----

=== Test the application

A good application includes test coverage.

==== Test the constraints

To test each constraint in isolation, use a `ConstraintVerifier` in unit tests.
It tests each constraint's corner cases in isolation from the other tests,
which lowers maintenance when adding a new constraint with proper test coverage.


Add a `optaplanner-test` dependency in your `pom.xml`:
[source,xml]
----
    <dependency>
      <groupId>org.optaplanner</groupId>
      <artifactId>optaplanner-test</artifactId>
      <scope>test</scope>
    </dependency>
----

Create the `src/test/java/org/acme/schooltimetabling/solver/TimeTableConstraintProviderTest.java` class:

[source,java]
----
package org.acme.schooltimetabling.solver;

import java.time.DayOfWeek;
import java.time.LocalTime;
import javax.inject.Inject;
import org.acme.schooltimetabling.domain.Lesson;
import org.acme.schooltimetabling.domain.Room;
import org.acme.schooltimetabling.domain.TimeTable;
import org.acme.schooltimetabling.domain.Timeslot;
import org.junit.jupiter.api.Test;
import org.optaplanner.test.api.score.stream.ConstraintVerifier;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;

@SpringBootTest
class TimeTableConstraintProviderTest {

    private static final Room ROOM = new Room("Room1");
    private static final Timeslot TIMESLOT1 = new Timeslot(DayOfWeek.MONDAY, LocalTime.of(9,0), LocalTime.NOON);
    private static final Timeslot TIMESLOT2 = new Timeslot(DayOfWeek.TUESDAY, LocalTime.of(9,0), LocalTime.NOON);

    @Autowired
    ConstraintVerifier<TimeTableConstraintProvider, TimeTable> constraintVerifier;

    @Test
    void roomConflict() {
        Lesson firstLesson = new Lesson(1, "Subject1", "Teacher1", "Group1");
        Lesson conflictingLesson = new Lesson(2, "Subject2", "Teacher2", "Group2");
        Lesson nonConflictingLesson = new Lesson(3, "Subject3", "Teacher3", "Group3");

        firstLesson.setRoom(ROOM);
        firstLesson.setTimeslot(TIMESLOT1);

        conflictingLesson.setRoom(ROOM);
        conflictingLesson.setTimeslot(TIMESLOT1);

        nonConflictingLesson.setRoom(ROOM);
        nonConflictingLesson.setTimeslot(TIMESLOT2);

        constraintVerifier.verifyThat(TimeTableConstraintProvider::roomConflict)
                .given(firstLesson, conflictingLesson, nonConflictingLesson)
                .penalizesBy(1);
    }
}
----

This test verifies that the constraint `TimeTableConstraintProvider::roomConflict`,
when given three lessons in the same room, where two lessons have the same timeslot,
it penalizes with a match weight of `1`.
So with a constraint weight of `10hard` it would reduce the score by `-10hard`.

Notice how `ConstraintVerifier` ignores the constraint weight during testing - even
if those constraint weights are hard coded in the `ConstraintProvider` - because
constraints weights change regularly before going into production.
This way, constraint weight tweaking does not break the unit tests.

==== Test the solver

In a JUnit test, generate a test dataset and send it to the `TimeTableController` to solve.

Create the `src/test/java/org/acme/schooltimetabling/rest/TimeTableControllerTest.java` class:

[source,java]
----
package org.acme.schooltimetabling.rest;

import java.time.DayOfWeek;
import java.time.LocalTime;
import java.util.ArrayList;
import java.util.List;

import org.acme.schooltimetabling.domain.Lesson;
import org.acme.schooltimetabling.domain.Room;
import org.acme.schooltimetabling.domain.TimeTable;
import org.acme.schooltimetabling.domain.Timeslot;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.Timeout;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;

import static org.junit.jupiter.api.Assertions.assertFalse;
import static org.junit.jupiter.api.Assertions.assertNotNull;
import static org.junit.jupiter.api.Assertions.assertTrue;

@SpringBootTest(properties = {
        // Effectively disable spent-time termination in favor of the best-score-limit
        "optaplanner.solver.termination.spent-limit=1h",
        "optaplanner.solver.termination.best-score-limit=0hard/*soft"})
public class TimeTableControllerTest {

    @Autowired
    private TimeTableController timeTableController;

    @Test
    @Timeout(600_000)
    public void solve() {
        TimeTable problem = generateProblem();
        TimeTable solution = timeTableController.solve(problem);
        assertFalse(solution.getLessonList().isEmpty());
        for (Lesson lesson : solution.getLessonList()) {
            assertNotNull(lesson.getTimeslot());
            assertNotNull(lesson.getRoom());
        }
        assertTrue(solution.getScore().isFeasible());
    }

    private TimeTable generateProblem() {
        List<Timeslot> timeslotList = new ArrayList<>();
        timeslotList.add(new Timeslot(DayOfWeek.MONDAY, LocalTime.of(8, 30), LocalTime.of(9, 30)));
        timeslotList.add(new Timeslot(DayOfWeek.MONDAY, LocalTime.of(9, 30), LocalTime.of(10, 30)));
        timeslotList.add(new Timeslot(DayOfWeek.MONDAY, LocalTime.of(10, 30), LocalTime.of(11, 30)));
        timeslotList.add(new Timeslot(DayOfWeek.MONDAY, LocalTime.of(13, 30), LocalTime.of(14, 30)));
        timeslotList.add(new Timeslot(DayOfWeek.MONDAY, LocalTime.of(14, 30), LocalTime.of(15, 30)));

        List<Room> roomList = new ArrayList<>();
        roomList.add(new Room("Room A"));
        roomList.add(new Room("Room B"));
        roomList.add(new Room("Room C"));

        List<Lesson> lessonList = new ArrayList<>();
        lessonList.add(new Lesson(101L, "Math", "B. May", "9th grade"));
        lessonList.add(new Lesson(102L, "Physics", "M. Curie", "9th grade"));
        lessonList.add(new Lesson(103L, "Geography", "M. Polo", "9th grade"));
        lessonList.add(new Lesson(104L, "English", "I. Jones", "9th grade"));
        lessonList.add(new Lesson(105L, "Spanish", "P. Cruz", "9th grade"));

        lessonList.add(new Lesson(201L, "Math", "B. May", "10th grade"));
        lessonList.add(new Lesson(202L, "Chemistry", "M. Curie", "10th grade"));
        lessonList.add(new Lesson(203L, "History", "I. Jones", "10th grade"));
        lessonList.add(new Lesson(204L, "English", "P. Cruz", "10th grade"));
        lessonList.add(new Lesson(205L, "French", "M. Curie", "10th grade"));
        return new TimeTable(timeslotList, roomList, lessonList);
    }

}
----

This test verifies that after solving, all lessons are assigned to a time slot and a room.
It also verifies that it found a feasible solution (no hard constraints broken).

Normally, the solver finds a feasible solution in less than 200 milliseconds.
Notice how the `@SpringBootTest` annotation's `properties` property overwrites the solver termination during tests
to terminate as soon as a feasible solution (`0hard/*soft`) is found.
This avoids hard coding a solver time, because the unit test might run on arbitrary hardware.
This approach ensures that the test runs long enough to find a feasible solution, even on slow machines.
But it does not run a millisecond longer than it strictly must, even on fast machines.

=== Logging

When adding constraints in your `ConstraintProvider`,
keep an eye on the _score calculation speed_ in the `info` log,
after solving for the same amount of time, to assess the performance impact:

[source]
----
... Solving ended: ..., score calculation speed (29455/sec), ...
----

To understand how OptaPlanner is solving your problem internally,
change the logging in the `application.properties` file or with a `-D` system property:

[source,properties]
----
logging.level.org.optaplanner=debug
----

Use `debug` logging to show every _step_:

[source,options="nowrap"]
----
... Solving started: time spent (67), best score (-20init/0hard/0soft), environment mode (REPRODUCIBLE), random (JDK with seed 0).
...     CH step (0), time spent (128), score (-18init/0hard/0soft), selected move count (15), picked move ([Math(101) {null -> Room A}, Math(101) {null -> MONDAY 08:30}]).
...     CH step (1), time spent (145), score (-16init/0hard/0soft), selected move count (15), picked move ([Physics(102) {null -> Room A}, Physics(102) {null -> MONDAY 09:30}]).
...
----

Use `trace` logging to show every _step_ and every _move_ per step.

== Summary

Congratulations!
You have just developed a Spring application with https://www.optaplanner.org/[OptaPlanner]!

== Further improvements: Database and UI integration

Now try adding database and UI integration:

. Create https://spring.io/guides/gs/accessing-data-jpa/[JPA repositories] for `Timeslot`, `Room`, and `Lesson`.

. https://spring.io/guides/gs/accessing-data-rest/[Expose them through REST].

. Build a `TimeTableRepository` facade to read and write a `TimeTable` instance in a single transaction.

. Adjust the `TimeTableController` accordingly:
+
[source,java]
----
package org.acme.schooltimetabling.rest;

import org.acme.schooltimetabling.domain.TimeTable;
import org.acme.schooltimetabling.persistence.TimeTableRepository;
import org.optaplanner.core.api.solver.SolutionManager;
import org.optaplanner.core.api.solver.SolverManager;
import org.optaplanner.core.api.solver.SolverStatus;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

@RestController
@RequestMapping("/timeTable")
public class TimeTableController {

    @Autowired
    private TimeTableRepository timeTableRepository;
    @Autowired
    private SolverManager<TimeTable, Long> solverManager;
    @Autowired
    private SolutionManager<TimeTable, HardSoftScore> solutionManager;

    // To try, GET http://localhost:8080/timeTable
    @GetMapping()
    public TimeTable getTimeTable() {
        // Get the solver status before loading the solution
        // to avoid the race condition that the solver terminates between them
        SolverStatus solverStatus = getSolverStatus();
        TimeTable solution = timeTableRepository.findById(TimeTableRepository.SINGLETON_TIME_TABLE_ID);
        solutionManager.update(solution); // Sets the score
        solution.setSolverStatus(solverStatus);
        return solution;
    }

    @PostMapping("/solve")
    public void solve() {
        solverManager.solveAndListen(TimeTableRepository.SINGLETON_TIME_TABLE_ID,
                timeTableRepository::findById,
                timeTableRepository::save);
    }

    public SolverStatus getSolverStatus() {
        return solverManager.getSolverStatus(TimeTableRepository.SINGLETON_TIME_TABLE_ID);
    }

    @PostMapping("/stopSolving")
    public void stopSolving() {
        solverManager.terminateEarly(TimeTableRepository.SINGLETON_TIME_TABLE_ID);
    }

}
----
+
For simplicity's sake, this code handles only one `TimeTable` instance,
but it is straightforward to enable multi-tenancy and handle multiple `TimeTable` instances of different high schools in parallel.
+
The `getTimeTable()` method returns the latest timetable from the database.
It uses the `SolutionManager` (which is automatically injected)
to calculate the score of that timetable, so the UI can show the score.
+
The `solve()` method starts a job to solve the current timetable and store the time slot and room assignments in the database.
It uses the `SolverManager.solveAndListen()` method to listen to intermediate best solutions
and update the database accordingly.
This enables the UI to show progress while the backend is still solving.

. Adjust the `TimeTableControllerTest` instance accordingly, now that the `solve()` method returns immediately.
Poll for the latest solution until the solver finishes solving:
+
[source,java]
----
package org.acme.schooltimetabling.rest;

import org.acme.schooltimetabling.domain.Lesson;
import org.acme.schooltimetabling.domain.TimeTable;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.Timeout;
import org.optaplanner.core.api.solver.SolverStatus;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;

import static org.junit.jupiter.api.Assertions.assertFalse;
import static org.junit.jupiter.api.Assertions.assertNotNull;
import static org.junit.jupiter.api.Assertions.assertTrue;

@SpringBootTest(properties = {
        "optaplanner.solver.termination.spent-limit=1h", // Effectively disable this termination in favor of the best-score-limit
        "optaplanner.solver.termination.best-score-limit=0hard/*soft"})
public class TimeTableControllerTest {

    @Autowired
    private TimeTableController timeTableController;

    @Test
    @Timeout(600_000)
    public void solveDemoDataUntilFeasible() throws InterruptedException {
        timeTableController.solve();
        TimeTable timeTable = timeTableController.getTimeTable();
        while (timeTable.getSolverStatus() != SolverStatus.NOT_SOLVING) {
            // Quick polling (not a Test Thread Sleep anti-pattern)
            // Test is still fast on fast machines and doesn't randomly fail on slow machines.
            Thread.sleep(20L);
            timeTable = timeTableController.getTimeTable();
        }
        assertFalse(timeTable.getLessonList().isEmpty());
        for (Lesson lesson : timeTable.getLessonList()) {
            assertNotNull(lesson.getTimeslot());
            assertNotNull(lesson.getRoom());
        }
        assertTrue(timeTable.getScore().isFeasible());
    }

}
----

. Build an attractive web UI on top of these REST methods to visualize the timetable.

Take a look at {spring-boot-quickstart-url}[the quickstart source code] to see how this all turns out.
