diff --git a/docs/src/main/asciidoc/optaplanner.adoc b/docs/src/main/asciidoc/optaplanner.adoc
deleted file mode 100644
index 07bdf01fba911..0000000000000
--- a/docs/src/main/asciidoc/optaplanner.adoc
+++ /dev/null
@@ -1,1100 +0,0 @@
-////
-This guide is maintained in the main Quarkus repository
-and pull requests should be submitted there:
-https://github.com/quarkusio/quarkus/tree/main/docs/src/main/asciidoc
-////
-= OptaPlanner - Using AI to optimize a schedule with OptaPlanner
-include::_attributes.adoc[]
-:categories: business-automation
-:summary: This guide walks you through the process of creating a Quarkus application with OptaPlanner's constraint solving Artificial Intelligence (AI).
-:config-file: application.properties
-
-This guide walks you through the process of creating a Quarkus 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::optaplanner-time-table-app-screenshot.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 dataset, 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]]
-== Solution
-
-We recommend that you follow the instructions in the next sections and create the application step by step.
-However, you can go right to the completed example.
-
-Clone the Git repository: `git clone {quickstarts-clone-url}`, or download an {quickstarts-archive-url}[archive].
-
-The solution is located in link:{quickstarts-tree-url}/optaplanner-quickstart[the `optaplanner-quickstart` directory].
-
-== Prerequisites
-
-:prerequisites-time: 30 minutes
-:prerequisites-no-graalvm:
-include::{includes}/prerequisites.adoc[]
-
-== The build file and the dependencies
-
-Use https://code.quarkus.io/[code.quarkus.io] to generate an application
-with the following extensions, for Maven or Gradle:
-
-* RESTEasy Reactive (`quarkus-resteasy-reactive`)
-* RESTEasy Reactive Jackson (`quarkus-resteasy-reactive-jackson`)
-* OptaPlanner (`optaplanner-quarkus`)
-* OptaPlanner Jackson (`optaplanner-quarkus-jackson`)
-
-Alternatively, generate it from the command line:
-
-:create-app-artifact-id: optaplanner-quickstart
-:create-app-extensions: resteasy-reactive,resteasy-reactive-jackson,optaplanner-quarkus,optaplanner-quarkus-jackson
-include::{includes}/devtools/create-app.adoc[]
-
-This will include the following dependencies in your build file:
-
-[source,xml,subs=attributes+,role="primary asciidoc-tabs-target-sync-cli asciidoc-tabs-target-sync-maven"]
-.pom.xml
-----
-
-
-
- {quarkus-platform-groupid}
- quarkus-bom
- {quarkus-version}
- pom
- import
-
-
- {quarkus-platform-groupid}
- quarkus-optaplanner-bom
- {quarkus-version}
- pom
- import
-
-
-
-
-
- io.quarkus
- quarkus-resteasy-reactive
-
-
- io.quarkus
- quarkus-resteasy-reactive-jackson
-
-
- org.optaplanner
- optaplanner-quarkus
-
-
- org.optaplanner
- optaplanner-quarkus-jackson
-
-
-
- io.quarkus
- quarkus-junit5
- test
-
-
-----
-
-[source,gradle,subs=attributes+,role="secondary asciidoc-tabs-target-sync-gradle"]
-.build.gradle
-----
-dependencies {
- implementation enforcedPlatform("{quarkus-platform-groupid}:quarkus-bom:{quarkus-version}")
- implementation enforcedPlatform("{quarkus-platform-groupid}:quarkus-optaplanner-bom:{quarkus-version}")
- implementation 'io.quarkus:quarkus-resteasy-reactive'
- implementation 'io.quarkus:quarkus-resteasy-reactive-jackson'
- implementation 'org.optaplanner:optaplanner-quarkus'
- implementation 'org.optaplanner:optaplanner-quarkus-jackson'
-
- testImplementation 'io.quarkus:quarkus-junit5'
-}
-----
-
-== Model the domain objects
-
-Your goal is to assign each lesson to a time slot and a room.
-You will create these classes:
-
-image::optaplanner-time-table-class-diagram-pure.png[]
-
-=== Timeslot
-
-The `Timeslot` class represents a time interval when lessons are taught,
-for example, `Monday 10:30 - 11:30` or `Tuesday 13:30 - 14:30`.
-For simplicity's sake, all time slots have the same duration
-and there are no time slots during lunch or other breaks.
-
-A time slot has no date, because a high school schedule just repeats every week.
-So there is no need for https://docs.optaplanner.org/latestFinal/optaplanner-docs/html_single/index.html#continuousPlanning[continuous planning].
-
-Create the `src/main/java/org/acme/optaplanner/domain/Timeslot.java` class:
-
-[source,java]
-----
-package org.acme.optaplanner.domain;
-
-import java.time.DayOfWeek;
-import java.time.LocalTime;
-
-public class Timeslot {
-
- private DayOfWeek dayOfWeek;
- private LocalTime startTime;
- private LocalTime endTime;
-
- public Timeslot() {
- }
-
- public Timeslot(DayOfWeek dayOfWeek, LocalTime startTime, LocalTime endTime) {
- this.dayOfWeek = dayOfWeek;
- this.startTime = startTime;
- this.endTime = endTime;
- }
-
- public DayOfWeek getDayOfWeek() {
- return dayOfWeek;
- }
-
- public LocalTime getStartTime() {
- return startTime;
- }
-
- public LocalTime getEndTime() {
- return endTime;
- }
-
- @Override
- public String toString() {
- return dayOfWeek + " " + startTime;
- }
-
-}
-----
-
-Because no `Timeslot` instances change during solving, a `Timeslot` is called a _problem fact_.
-Such classes do not require any OptaPlanner specific annotations.
-
-Notice the `toString()` method keeps the output short,
-so it is easier to read OptaPlanner's `DEBUG` or `TRACE` log, as shown later.
-
-=== Room
-
-The `Room` class represents a location where lessons are taught,
-for example, `Room A` or `Room B`.
-For simplicity's sake, all rooms are without capacity limits
-and they can accommodate all lessons.
-
-Create the `src/main/java/org/acme/optaplanner/domain/Room.java` class:
-
-[source,java]
-----
-package org.acme.optaplanner.domain;
-
-public class Room {
-
- private String name;
-
- public Room() {
- }
-
- public Room(String name) {
- this.name = name;
- }
-
- public String getName() {
- return name;
- }
-
- @Override
- public String toString() {
- return name;
- }
-
-}
-----
-
-`Room` instances do not change during solving, so `Room` is also a _problem fact_.
-
-=== Lesson
-
-During a lesson, represented by the `Lesson` class,
-a teacher teaches a subject to a group of students,
-for example, `Math by A.Turing for 9th grade` or `Chemistry by M.Curie for 10th grade`.
-If a subject is taught multiple times per week by the same teacher to the same student group,
-there are multiple `Lesson` instances that are only distinguishable by `id`.
-For example, the 9th grade has six math lessons a week.
-
-During solving, OptaPlanner changes the `timeslot` and `room` fields of the `Lesson` class,
-to assign each lesson to a time slot and a room.
-Because OptaPlanner changes these fields, `Lesson` is a _planning entity_:
-
-image::optaplanner-time-table-class-diagram-annotated.png[]
-
-Most of the fields in the previous diagram contain input data, except for the orange fields:
-A lesson's `timeslot` and `room` fields are unassigned (`null`) in the input data
-and assigned (not `null`) in the output data.
-OptaPlanner changes these fields during solving.
-Such fields are called planning variables.
-In order for OptaPlanner to recognize them,
-both the `timeslot` and `room` fields require an `@PlanningVariable` annotation.
-Their containing class, `Lesson`, requires an `@PlanningEntity` annotation.
-
-Create the `src/main/java/org/acme/optaplanner/domain/Lesson.java` class:
-
-[source,java]
-----
-package org.acme.optaplanner.domain;
-
-import org.optaplanner.core.api.domain.entity.PlanningEntity;
-import org.optaplanner.core.api.domain.lookup.PlanningId;
-import org.optaplanner.core.api.domain.variable.PlanningVariable;
-
-@PlanningEntity
-public class Lesson {
-
- @PlanningId
- private Long id;
-
- private String subject;
- private String teacher;
- private String studentGroup;
-
- @PlanningVariable
- private Timeslot timeslot;
- @PlanningVariable
- private Room room;
-
- public Lesson() {
- }
-
- public Lesson(Long id, String subject, String teacher, String studentGroup) {
- this.id = id;
- this.subject = subject;
- this.teacher = teacher;
- this.studentGroup = studentGroup;
- }
-
- public Long getId() {
- return id;
- }
-
- public String getSubject() {
- return subject;
- }
-
- public String getTeacher() {
- return teacher;
- }
-
- public String getStudentGroup() {
- return studentGroup;
- }
-
- public Timeslot getTimeslot() {
- return timeslot;
- }
-
- public void setTimeslot(Timeslot timeslot) {
- this.timeslot = timeslot;
- }
-
- public Room getRoom() {
- return room;
- }
-
- public void setRoom(Room room) {
- this.room = room;
- }
-
- @Override
- public String toString() {
- return subject + "(" + id + ")";
- }
-
-}
-----
-
-The `Lesson` class has an `@PlanningEntity` annotation,
-so OptaPlanner knows that this class changes during solving
-because it contains one or more planning variables.
-
-The `timeslot` field has an `@PlanningVariable` annotation, so OptaPlanner knows that it can change its value.
-In order to find potential Timeslot instances to assign to this field, OptaPlanner uses the variable type to connect to a value range provider that provides a List to pick from.
-
-The `room` field also has an `@PlanningVariable` annotation, for the same reasons.
-
-[NOTE]
-====
-Determining the `@PlanningVariable` fields for an arbitrary constraint solving use case
-is often challenging the first time.
-Read https://docs.optaplanner.org/latestFinal/optaplanner-docs/html_single/index.html#domainModelingGuide[the domain modeling guidelines]
-to avoid common pitfalls.
-====
-
-== Define the constraints and calculate the score
-
-A _score_ represents the quality of a specific solution.
-The higher, the better.
-OptaPlanner looks for the best solution, which is the solution with the highest score found in the available time.
-It might be the _optimal_ solution.
-
-Because this use case has hard and soft constraints,
-use the `HardSoftScore` class to represent the score:
-
-* Hard constraints must not be broken. For example: _A room can have at most one lesson at the same time._
-* Soft constraints should not be broken. For example: _A teacher prefers to teach in a single room._
-
-Hard constraints are weighted against other hard constraints.
-Soft constraints are weighted too, against other soft constraints.
-*Hard constraints always outweigh soft constraints*, regardless of their respective weights.
-
-To calculate the score, you could implement an `EasyScoreCalculator` class:
-
-[source,java]
-----
-public class TimeTableEasyScoreCalculator implements EasyScoreCalculator {
-
- @Override
- public HardSoftScore calculateScore(TimeTable timeTable) {
- List lessonList = timeTable.getLessonList();
- int hardScore = 0;
- for (Lesson a : lessonList) {
- for (Lesson b : lessonList) {
- if (a.getTimeslot() != null && a.getTimeslot().equals(b.getTimeslot())
- && a.getId() < b.getId()) {
- // A room can accommodate at most one lesson at the same time.
- if (a.getRoom() != null && a.getRoom().equals(b.getRoom())) {
- hardScore--;
- }
- // A teacher can teach at most one lesson at the same time.
- if (a.getTeacher().equals(b.getTeacher())) {
- hardScore--;
- }
- // A student can attend at most one lesson at the same time.
- if (a.getStudentGroup().equals(b.getStudentGroup())) {
- hardScore--;
- }
- }
- }
- }
- int softScore = 0;
- // Soft constraints are only implemented in the optaplanner-quickstarts code
- return HardSoftScore.of(hardScore, softScore);
- }
-
-}
-----
-
-Unfortunately **that does not scale well**, because it is non-incremental:
-every time a lesson is assigned to a different time slot or room,
-all lessons are re-evaluated to calculate the new score.
-
-Instead, create a `src/main/java/org/acme/optaplanner/solver/TimeTableConstraintProvider.java` class
-to perform incremental score calculation.
-It uses OptaPlanner's ConstraintStream API which is inspired by Java Streams and SQL:
-
-[source,java]
-----
-package org.acme.optaplanner.solver;
-
-import org.acme.optaplanner.domain.Lesson;
-import org.optaplanner.core.api.score.buildin.hardsoft.HardSoftScore;
-import org.optaplanner.core.api.score.stream.Constraint;
-import org.optaplanner.core.api.score.stream.ConstraintFactory;
-import org.optaplanner.core.api.score.stream.ConstraintProvider;
-import org.optaplanner.core.api.score.stream.Joiners;
-
-public class TimeTableConstraintProvider implements ConstraintProvider {
-
- @Override
- public Constraint[] defineConstraints(ConstraintFactory constraintFactory) {
- return new Constraint[] {
- // Hard constraints
- roomConflict(constraintFactory),
- teacherConflict(constraintFactory),
- studentGroupConflict(constraintFactory),
- // Soft constraints are only implemented in the optaplanner-quickstarts code
- };
- }
-
- Constraint roomConflict(ConstraintFactory constraintFactory) {
- // A room can accommodate at most one lesson at the same time.
-
- // Select a lesson ...
- return constraintFactory
- .forEach(Lesson.class)
- // ... and pair it with another lesson ...
- .join(Lesson.class,
- // ... in the same timeslot ...
- Joiners.equal(Lesson::getTimeslot),
- // ... in the same room ...
- Joiners.equal(Lesson::getRoom),
- // ... and the pair is unique (different id, no reverse pairs) ...
- Joiners.lessThan(Lesson::getId))
- // ... then penalize each pair with a hard weight.
- .penalize(HardSoftScore.ONE_HARD)
- .asConstraint("Room conflict");
- }
-
- Constraint teacherConflict(ConstraintFactory constraintFactory) {
- // A teacher can teach at most one lesson at the same time.
- return constraintFactory.forEach(Lesson.class)
- .join(Lesson.class,
- Joiners.equal(Lesson::getTimeslot),
- Joiners.equal(Lesson::getTeacher),
- Joiners.lessThan(Lesson::getId))
- .penalize(HardSoftScore.ONE_HARD)
- .asConstraint("Teacher conflict");
- }
-
- Constraint studentGroupConflict(ConstraintFactory constraintFactory) {
- // A student can attend at most one lesson at the same time.
- return constraintFactory.forEach(Lesson.class)
- .join(Lesson.class,
- Joiners.equal(Lesson::getTimeslot),
- Joiners.equal(Lesson::getStudentGroup),
- Joiners.lessThan(Lesson::getId))
- .penalize(HardSoftScore.ONE_HARD)
- .asConstraint("Student group conflict");
- }
-
-}
-----
-
-The `ConstraintProvider` scales an order of magnitude better than the `EasyScoreCalculator`: __O__(n) instead of __O__(n²).
-
-== Gather the domain objects in a planning solution
-
-A `TimeTable` wraps all `Timeslot`, `Room`, and `Lesson` instances of a single dataset.
-Furthermore, because it contains all lessons, each with a specific planning variable state,
-it is a _planning solution_ and it has a score:
-
-* If lessons are still unassigned, then it is an _uninitialized_ solution,
-for example, a solution with the score `-4init/0hard/0soft`.
-* If it breaks hard constraints, then it is an _infeasible_ solution,
-for example, a solution with the score `-2hard/-3soft`.
-* If it adheres to all hard constraints, then it is a _feasible_ solution,
-for example, a solution with the score `0hard/-7soft`.
-
-Create the `src/main/java/org/acme/optaplanner/domain/TimeTable.java` class:
-
-[source,java]
-----
-package org.acme.optaplanner.domain;
-
-import java.util.List;
-
-import org.optaplanner.core.api.domain.solution.PlanningEntityCollectionProperty;
-import org.optaplanner.core.api.domain.solution.PlanningScore;
-import org.optaplanner.core.api.domain.solution.PlanningSolution;
-import org.optaplanner.core.api.domain.solution.ProblemFactCollectionProperty;
-import org.optaplanner.core.api.domain.valuerange.ValueRangeProvider;
-import org.optaplanner.core.api.score.buildin.hardsoft.HardSoftScore;
-
-@PlanningSolution
-public class TimeTable {
-
- @ValueRangeProvider
- @ProblemFactCollectionProperty
- private List timeslotList;
- @ValueRangeProvider
- @ProblemFactCollectionProperty
- private List roomList;
- @PlanningEntityCollectionProperty
- private List lessonList;
-
- @PlanningScore
- private HardSoftScore score;
-
- public TimeTable() {
- }
-
- public TimeTable(List timeslotList, List roomList, List lessonList) {
- this.timeslotList = timeslotList;
- this.roomList = roomList;
- this.lessonList = lessonList;
- }
-
- public List getTimeslotList() {
- return timeslotList;
- }
-
- public List getRoomList() {
- return roomList;
- }
-
- public List getLessonList() {
- return lessonList;
- }
-
- public HardSoftScore getScore() {
- return score;
- }
-
-}
-----
-
-The `TimeTable` class has an `@PlanningSolution` annotation,
-so OptaPlanner knows that this class contains all the input and output data.
-
-Specifically, this class is the input of the problem:
-
-* A `timeslotList` field with all time slots
-** This is a list of problem facts, because they do not change during solving.
-* A `roomList` field with all rooms
-** This is a list of problem facts, because they do not change during solving.
-* A `lessonList` field with all lessons
-** This is a list of planning entities, because they change during solving.
-** Of each `Lesson`:
-*** The values of the `timeslot` and `room` fields are typically still `null`, so unassigned.
-They are planning variables.
-*** The other fields, such as `subject`, `teacher` and `studentGroup`, are filled in.
-These fields are problem properties.
-
-However, this class is also the output of the solution:
-
-* A `lessonList` field for which each `Lesson` instance has non-null `timeslot` and `room` fields after solving
-* A `score` field that represents the quality of the output solution, for example, `0hard/-5soft`
-
-=== The value range providers
-
-The `timeslotList` field is a value range provider.
-It holds the `Timeslot` instances which OptaPlanner can pick from to assign to the `timeslot` field of `Lesson` instances.
-The `timeslotList` field has an `@ValueRangeProvider` annotation to connect the `@PlanningVariable` with the `@ValueRangeProvider`,
-by matching the type of the planning variable with the type returned by the value range provider.
-
-Following the same logic, the `roomList` field also has an `@ValueRangeProvider` annotation.
-
-=== The problem fact and planning entity properties
-
-Furthermore, OptaPlanner needs to know which `Lesson` instances it can change
-as well as how to retrieve the `Timeslot` and `Room` instances used for score calculation
-by your `TimeTableConstraintProvider`.
-
-The `timeslotList` and `roomList` fields have an `@ProblemFactCollectionProperty` annotation,
-so your `TimeTableConstraintProvider` can select _from_ those instances.
-
-The `lessonList` has an `@PlanningEntityCollectionProperty` annotation,
-so OptaPlanner can change them during solving
-and your `TimeTableConstraintProvider` can select _from_ those too.
-
-== 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 Quarkus extension 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/optaplanner/rest/TimeTableResource.java` class:
-
-[source,java]
-----
-package org.acme.optaplanner.rest;
-
-import java.util.UUID;
-import java.util.concurrent.ExecutionException;
-import jakarta.inject.Inject;
-import jakarta.ws.rs.POST;
-import jakarta.ws.rs.Path;
-
-import org.acme.optaplanner.domain.TimeTable;
-import org.optaplanner.core.api.solver.SolverJob;
-import org.optaplanner.core.api.solver.SolverManager;
-
-@Path("/timeTable")
-public class TimeTableResource {
-
- @Inject
- SolverManager solverManager;
-
- @POST
- @Path("/solve")
- public TimeTable solve(TimeTable problem) {
- UUID problemId = UUID.randomUUID();
- // Submit the problem to start solving
- SolverJob 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 termination 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 an HTTP timeout in this simple implementation.
-# It's recommended to run for at least 5 minutes ("5m") otherwise.
-quarkus.optaplanner.solver.termination.spent-limit=5s
-----
-
-
-== Run the application
-
-First start the application:
-
-include::{includes}/devtools/dev.adoc[]
-
-=== 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 show 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 build file:
-
-[source,xml,role="primary asciidoc-tabs-target-sync-cli asciidoc-tabs-target-sync-maven"]
-.pom.xml
-----
-
- org.optaplanner
- optaplanner-test
- test
-
-----
-
-[source,gradle,role="secondary asciidoc-tabs-target-sync-gradle"]
-.build.gradle
-----
-testImplementation("org.optaplanner:optaplanner-test")
-----
-
-Create the `src/test/java/org/acme/optaplanner/solver/TimeTableConstraintProviderTest.java` class:
-
-[source,java]
-----
-package org.acme.optaplanner.solver;
-
-import java.time.DayOfWeek;
-import java.time.LocalTime;
-
-import jakarta.inject.Inject;
-
-import io.quarkus.test.junit.QuarkusTest;
-import org.acme.optaplanner.domain.Lesson;
-import org.acme.optaplanner.domain.Room;
-import org.acme.optaplanner.domain.TimeTable;
-import org.acme.optaplanner.domain.Timeslot;
-import org.junit.jupiter.api.Test;
-import org.optaplanner.test.api.score.stream.ConstraintVerifier;
-
-@QuarkusTest
-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);
-
- @Inject
- ConstraintVerifier 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 `TimeTableResource` to solve.
-
-Create the `src/test/java/org/acme/optaplanner/rest/TimeTableResourceTest.java` class:
-
-[source,java]
-----
-package org.acme.optaplanner.rest;
-
-import java.time.DayOfWeek;
-import java.time.LocalTime;
-import java.util.ArrayList;
-import java.util.List;
-
-import jakarta.inject.Inject;
-
-import io.quarkus.test.junit.QuarkusTest;
-import org.acme.optaplanner.domain.Room;
-import org.acme.optaplanner.domain.Timeslot;
-import org.acme.optaplanner.domain.Lesson;
-import org.acme.optaplanner.domain.TimeTable;
-import org.acme.optaplanner.rest.TimeTableResource;
-import org.junit.jupiter.api.Test;
-import org.junit.jupiter.api.Timeout;
-
-import static org.junit.jupiter.api.Assertions.assertFalse;
-import static org.junit.jupiter.api.Assertions.assertNotNull;
-import static org.junit.jupiter.api.Assertions.assertTrue;
-
-@QuarkusTest
-public class TimeTableResourceTest {
-
- @Inject
- TimeTableResource timeTableResource;
-
- @Test
- @Timeout(600_000)
- public void solve() {
- TimeTable problem = generateProblem();
- TimeTable solution = timeTableResource.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 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 roomList = new ArrayList<>();
- roomList.add(new Room("Room A"));
- roomList.add(new Room("Room B"));
- roomList.add(new Room("Room C"));
-
- List 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).
-
-Add test properties to the `src/main/resources/application.properties` file:
-
-[source,properties]
-----
-quarkus.optaplanner.solver.termination.spent-limit=5s
-
-# Effectively disable spent-time termination in favor of the best-score-limit
-%test.quarkus.optaplanner.solver.termination.spent-limit=1h
-%test.quarkus.optaplanner.solver.termination.best-score-limit=0hard/*soft
-----
-
-Normally, the solver finds a feasible solution in less than 200 milliseconds.
-Notice how the `application.properties` 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]
-----
-quarkus.log.category."org.optaplanner".level=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 Quarkus application with https://www.optaplanner.org/[OptaPlanner]!
-
-== Further improvements: Database and UI integration
-
-Now try adding database and UI integration:
-
-. Store `Timeslot`, `Room`, and `Lesson` in the database with xref:hibernate-orm-panache.adoc[Hibernate and Panache].
-
-. xref:rest-json.adoc[Expose them through REST].
-
-. Adjust the `TimeTableResource` to read and write a `TimeTable` instance in a single transaction
-and use those accordingly:
-+
-[source,java]
-----
-package org.acme.optaplanner.rest;
-
-import jakarta.inject.Inject;
-import jakarta.transaction.Transactional;
-import jakarta.ws.rs.GET;
-import jakarta.ws.rs.POST;
-import jakarta.ws.rs.Path;
-
-import io.quarkus.panache.common.Sort;
-import org.acme.optaplanner.domain.Lesson;
-import org.acme.optaplanner.domain.Room;
-import org.acme.optaplanner.domain.TimeTable;
-import org.acme.optaplanner.domain.Timeslot;
-import org.optaplanner.core.api.score.ScoreManager;
-import org.optaplanner.core.api.score.buildin.hardsoft.HardSoftScore;
-import org.optaplanner.core.api.solver.SolverManager;
-import org.optaplanner.core.api.solver.SolverStatus;
-
-@Path("/timeTable")
-public class TimeTableResource {
-
- public static final Long SINGLETON_TIME_TABLE_ID = 1L;
-
- @Inject
- SolverManager solverManager;
- @Inject
- ScoreManager scoreManager;
-
- // To try, open http://localhost:8080/timeTable
- @GET
- 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 = findById(SINGLETON_TIME_TABLE_ID);
- scoreManager.updateScore(solution); // Sets the score
- solution.setSolverStatus(solverStatus);
- return solution;
- }
-
- @POST
- @Path("/solve")
- public void solve() {
- solverManager.solveAndListen(SINGLETON_TIME_TABLE_ID,
- this::findById,
- this::save);
- }
-
- public SolverStatus getSolverStatus() {
- return solverManager.getSolverStatus(SINGLETON_TIME_TABLE_ID);
- }
-
- @POST
- @Path("/stopSolving")
- public void stopSolving() {
- solverManager.terminateEarly(SINGLETON_TIME_TABLE_ID);
- }
-
- @Transactional
- protected TimeTable findById(Long id) {
- if (!SINGLETON_TIME_TABLE_ID.equals(id)) {
- throw new IllegalStateException("There is no timeTable with id (" + id + ").");
- }
- // Occurs in a single transaction, so each initialized lesson references the same timeslot/room instance
- // that is contained by the timeTable's timeslotList/roomList.
- return new TimeTable(
- Timeslot.listAll(Sort.by("dayOfWeek").and("startTime").and("endTime").and("id")),
- Room.listAll(Sort.by("name").and("id")),
- Lesson.listAll(Sort.by("subject").and("teacher").and("studentGroup").and("id")));
- }
-
- @Transactional
- protected void save(TimeTable timeTable) {
- for (Lesson lesson : timeTable.getLessonList()) {
- // TODO this is awfully naive: optimistic locking causes issues if called by the SolverManager
- Lesson attachedLesson = Lesson.findById(lesson.getId());
- attachedLesson.setTimeslot(lesson.getTimeslot());
- attachedLesson.setRoom(lesson.getRoom());
- }
- }
-
-}
-----
-+
-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 `ScoreManager` (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 `TimeTableResourceTest` instance accordingly, now that the `solve()` method returns immediately.
-Poll for the latest solution until the solver finishes solving:
-+
-[source,java]
-----
-package org.acme.optaplanner.rest;
-
-import jakarta.inject.Inject;
-
-import io.quarkus.test.junit.QuarkusTest;
-import org.acme.optaplanner.domain.Lesson;
-import org.acme.optaplanner.domain.TimeTable;
-import org.junit.jupiter.api.Test;
-import org.junit.jupiter.api.Timeout;
-import org.optaplanner.core.api.solver.SolverStatus;
-
-import static org.junit.jupiter.api.Assertions.assertFalse;
-import static org.junit.jupiter.api.Assertions.assertNotNull;
-import static org.junit.jupiter.api.Assertions.assertTrue;
-
-@QuarkusTest
-public class TimeTableResourceTest {
-
- @Inject
- TimeTableResource timeTableResource;
-
- @Test
- @Timeout(600_000)
- public void solveDemoDataUntilFeasible() throws InterruptedException {
- timeTableResource.solve();
- TimeTable timeTable = timeTableResource.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 = timeTableResource.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 link:{quickstarts-tree-url}/optaplanner-quickstart[the quickstart source code] to see how this all turns out.