[[helloWorldJavaQuickStart]]
= Hello world Java quick start
:doctype: book
:imagesdir: ../..
:sectnums:
:icons: font
include::../../_attributes.adoc[]

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

== What you will build

You will build a command-line application that optimizes a school timetable for students and teachers:

----
...
INFO  Solving ended: time spent (5000), best score (0hard/9soft), ...
INFO
INFO  |            | Room A     | Room B     | Room C     |
INFO  |------------|------------|------------|------------|
INFO  | MON 08:30  | English    | Math       |            |
INFO  |            | I. Jones   | A. Turing  |            |
INFO  |            | 9th grade  | 10th grade |            |
INFO  |------------|------------|------------|------------|
INFO  | MON 09:30  | History    | Physics    |            |
INFO  |            | I. Jones   | M. Curie   |            |
INFO  |            | 9th grade  | 10th grade |            |
INFO  |------------|------------|------------|------------|
INFO  | MON 10:30  | History    | Physics    |            |
INFO  |            | I. Jones   | M. Curie   |            |
INFO  |            | 10th grade | 9th grade  |            |
INFO  |------------|------------|------------|------------|
...
INFO  |------------|------------|------------|------------|
----

Your application will assign `Lesson` instances to `Timeslot` and `Room` instances automatically
by using AI to adhere to hard and soft scheduling _constraints_, for example:

* 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.
Fortunately, 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, review the completed example:

. Complete one of the following tasks:
.. Clone the Git repository:
+
[source,shell,subs=attributes+]
----
$ git clone {quickstarts-clone-url}
----
+
.. Download an {quickstarts-archive-url}[archive].

. Find the solution in {hello-world-java-quickstart-url}[the `hello-world` directory].
. Follow the instructions in the README file to run the application.

== 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 Maven or Gradle build file and add these dependencies:

* `optaplanner-core` (compile scope) to solve the school timetable problem.
* `optaplanner-test` (test scope) to JUnit test the school timetabling constraints.
* A xref:planner-configuration/planner-configuration.adoc#logging[logging] implementation, such as `logback-classic` (runtime scope), to see what OptaPlanner is doing.

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 https://maven.apache.org/xsd/maven-4.0.0.xsd">
  <modelVersion>4.0.0</modelVersion>

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

  <properties>
    <maven.compiler.release>{java-version}</maven.compiler.release>
    <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
  </properties>

  <dependencyManagement>
    <dependencies>
      <dependency>
        <groupId>org.optaplanner</groupId>
        <artifactId>optaplanner-bom</artifactId>
        <version>{optaplanner-version}</version>
        <type>pom</type>
        <scope>import</scope>
      </dependency>
      <dependency>
        <groupId>ch.qos.logback</groupId>
        <artifactId>logback-classic</artifactId>
        <version>1.2.3</version>
      </dependency>
    </dependencies>
  </dependencyManagement>

  <dependencies>
    <dependency>
      <groupId>org.optaplanner</groupId>
      <artifactId>optaplanner-core</artifactId>
    </dependency>
    <dependency>
      <groupId>ch.qos.logback</groupId>
      <artifactId>logback-classic</artifactId>
      <scope>runtime</scope>
    </dependency>

    <!-- Testing -->
    <dependency>
      <groupId>org.optaplanner</groupId>
      <artifactId>optaplanner-test</artifactId>
      <scope>test</scope>
    </dependency>
  </dependencies>

  <build>
    <plugins>
      <plugin>
        <groupId>org.codehaus.mojo</groupId>
        <artifactId>exec-maven-plugin</artifactId>
        <version>3.0.0</version>
        <configuration>
          <mainClass>org.acme.schooltimetabling.TimeTableApp</mainClass>
        </configuration>
      </plugin>
    </plugins>
  </build>
</project>
----

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

[source,groovy,subs=attributes+]
----
plugins {
    id "java"
    id "application"
}

def optaplannerVersion = "{optaplanner-version}"
def logbackVersion = "1.2.9"

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

repositories {
    mavenCentral()
}

dependencies {
    implementation platform("org.optaplanner:optaplanner-bom:${optaplannerVersion}")
    implementation "org.optaplanner:optaplanner-core"
    testImplementation "org.optaplanner:optaplanner-test"

    runtimeOnly "ch.qos.logback:logback-classic:${logbackVersion}"
}

java {
    sourceCompatibility = JavaVersion.VERSION_11
    targetCompatibility = JavaVersion.VERSION_11
}

compileJava {
    options.encoding = "UTF-8"
    options.compilerArgs << "-parameters"
}

compileTestJava {
    options.encoding = "UTF-8"
}

application {
    mainClass = "org.acme.schooltimetabling.TimeTableApp"
}

test {
    // Log the test execution results.
    testLogging {
        events "passed", "skipped", "failed"
    }
}
----

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 application

Now you are ready to put everything together and create a Java application.
The `main()` method performs the following tasks:

. Creates the `SolverFactory` to build a `Solver` per dataset.
. Loads a dataset.
. Solves it with `Solver.solve()`.
. Visualizes the solution for that dataset.

Typically, an application has a single `SolverFactory`
to build a new `Solver` instance for each problem dataset to solve.
A `SolverFactory` is thread-safe, but a `Solver` is not.
In this case, there is only one dataset, so only one `Solver` instance.

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

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

import java.time.DayOfWeek;
import java.time.Duration;
import java.time.LocalTime;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.Map;
import java.util.stream.Collectors;

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.acme.schooltimetabling.solver.TimeTableConstraintProvider;
import org.optaplanner.core.api.solver.Solver;
import org.optaplanner.core.api.solver.SolverFactory;
import org.optaplanner.core.config.solver.SolverConfig;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

public class TimeTableApp {

    private static final Logger LOGGER = LoggerFactory.getLogger(TimeTableApp.class);

    public static void main(String[] args) {
        SolverFactory<TimeTable> solverFactory = SolverFactory.create(new SolverConfig()
                .withSolutionClass(TimeTable.class)
                .withEntityClasses(Lesson.class)
                .withConstraintProviderClass(TimeTableConstraintProvider.class)
                // The solver runs only for 5 seconds on this small dataset.
                // It's recommended to run for at least 5 minutes ("5m") otherwise.
                .withTerminationSpentLimit(Duration.ofSeconds(5)));

        // Load the problem
        TimeTable problem = generateDemoData();

        // Solve the problem
        Solver<TimeTable> solver = solverFactory.buildSolver();
        TimeTable solution = solver.solve(problem);

        // Visualize the solution
        printTimetable(solution);
    }

    public static TimeTable generateDemoData() {
        List<Timeslot> timeslotList = new ArrayList<>(10);
        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)));

        timeslotList.add(new Timeslot(DayOfWeek.TUESDAY, LocalTime.of(8, 30), LocalTime.of(9, 30)));
        timeslotList.add(new Timeslot(DayOfWeek.TUESDAY, LocalTime.of(9, 30), LocalTime.of(10, 30)));
        timeslotList.add(new Timeslot(DayOfWeek.TUESDAY, LocalTime.of(10, 30), LocalTime.of(11, 30)));
        timeslotList.add(new Timeslot(DayOfWeek.TUESDAY, LocalTime.of(13, 30), LocalTime.of(14, 30)));
        timeslotList.add(new Timeslot(DayOfWeek.TUESDAY, LocalTime.of(14, 30), LocalTime.of(15, 30)));

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

        List<Lesson> lessonList = new ArrayList<>();
        long id = 0;
        lessonList.add(new Lesson(id++, "Math", "A. Turing", "9th grade"));
        lessonList.add(new Lesson(id++, "Math", "A. Turing", "9th grade"));
        lessonList.add(new Lesson(id++, "Physics", "M. Curie", "9th grade"));
        lessonList.add(new Lesson(id++, "Chemistry", "M. Curie", "9th grade"));
        lessonList.add(new Lesson(id++, "Biology", "C. Darwin", "9th grade"));
        lessonList.add(new Lesson(id++, "History", "I. Jones", "9th grade"));
        lessonList.add(new Lesson(id++, "English", "I. Jones", "9th grade"));
        lessonList.add(new Lesson(id++, "English", "I. Jones", "9th grade"));
        lessonList.add(new Lesson(id++, "Spanish", "P. Cruz", "9th grade"));
        lessonList.add(new Lesson(id++, "Spanish", "P. Cruz", "9th grade"));

        lessonList.add(new Lesson(id++, "Math", "A. Turing", "10th grade"));
        lessonList.add(new Lesson(id++, "Math", "A. Turing", "10th grade"));
        lessonList.add(new Lesson(id++, "Math", "A. Turing", "10th grade"));
        lessonList.add(new Lesson(id++, "Physics", "M. Curie", "10th grade"));
        lessonList.add(new Lesson(id++, "Chemistry", "M. Curie", "10th grade"));
        lessonList.add(new Lesson(id++, "French", "M. Curie", "10th grade"));
        lessonList.add(new Lesson(id++, "Geography", "C. Darwin", "10th grade"));
        lessonList.add(new Lesson(id++, "History", "I. Jones", "10th grade"));
        lessonList.add(new Lesson(id++, "English", "P. Cruz", "10th grade"));
        lessonList.add(new Lesson(id++, "Spanish", "P. Cruz", "10th grade"));

        return new TimeTable(timeslotList, roomList, lessonList);
    }

    private static void printTimetable(TimeTable timeTable) {
        LOGGER.info("");
        List<Room> roomList = timeTable.getRoomList();
        List<Lesson> lessonList = timeTable.getLessonList();
        Map<Timeslot, Map<Room, List<Lesson>>> lessonMap = lessonList.stream()
                .filter(lesson -> lesson.getTimeslot() != null && lesson.getRoom() != null)
                .collect(Collectors.groupingBy(Lesson::getTimeslot, Collectors.groupingBy(Lesson::getRoom)));
        LOGGER.info("|            | " + roomList.stream()
                .map(room -> String.format("%-10s", room.getName())).collect(Collectors.joining(" | ")) + " |");
        LOGGER.info("|" + "------------|".repeat(roomList.size() + 1));
        for (Timeslot timeslot : timeTable.getTimeslotList()) {
            List<List<Lesson>> cellList = roomList.stream()
                    .map(room -> {
                        Map<Room, List<Lesson>> byRoomMap = lessonMap.get(timeslot);
                        if (byRoomMap == null) {
                            return Collections.<Lesson>emptyList();
                        }
                        List<Lesson> cellLessonList = byRoomMap.get(room);
                        if (cellLessonList == null) {
                            return Collections.<Lesson>emptyList();
                        }
                        return cellLessonList;
                    })
                    .collect(Collectors.toList());

            LOGGER.info("| " + String.format("%-10s",
                    timeslot.getDayOfWeek().toString().substring(0, 3) + " " + timeslot.getStartTime()) + " | "
                    + cellList.stream().map(cellLessonList -> String.format("%-10s",
                            cellLessonList.stream().map(Lesson::getSubject).collect(Collectors.joining(", "))))
                            .collect(Collectors.joining(" | "))
                    + " |");
            LOGGER.info("|            | "
                    + cellList.stream().map(cellLessonList -> String.format("%-10s",
                            cellLessonList.stream().map(Lesson::getTeacher).collect(Collectors.joining(", "))))
                            .collect(Collectors.joining(" | "))
                    + " |");
            LOGGER.info("|            | "
                    + cellList.stream().map(cellLessonList -> String.format("%-10s",
                            cellLessonList.stream().map(Lesson::getStudentGroup).collect(Collectors.joining(", "))))
                            .collect(Collectors.joining(" | "))
                    + " |");
            LOGGER.info("|" + "------------|".repeat(roomList.size() + 1));
        }
        List<Lesson> unassignedLessons = lessonList.stream()
                .filter(lesson -> lesson.getTimeslot() == null || lesson.getRoom() == null)
                .collect(Collectors.toList());
        if (!unassignedLessons.isEmpty()) {
            LOGGER.info("");
            LOGGER.info("Unassigned lessons");
            for (Lesson lesson : unassignedLessons) {
                LOGGER.info("  " + lesson.getSubject() + " - " + lesson.getTeacher() + " - " + lesson.getStudentGroup());
            }
        }
    }

}
----

The `main()` method first creates the `SolverFactory`:

[source,java]
----
SolverFactory<TimeTable> solverFactory = SolverFactory.create(new SolverConfig()
        .withSolutionClass(TimeTable.class)
        .withEntityClasses(Lesson.class)
        .withConstraintProviderClass(TimeTableConstraintProvider.class)
        // The solver runs only for 5 seconds on this small dataset.
        // It's recommended to run for at least 5 minutes ("5m") otherwise.
        .withTerminationSpentLimit(Duration.ofSeconds(5)));
----

This registers the `@PlanningSolution` class, the `@PlanningEntity` classes,
and the `ConstraintProvider` class, all of which you created earlier.

Without a termination setting or a `terminationEarly()` event, the solver runs forever.
To avoid that, the solver limits the solving time to five seconds.

After five seconds, the `main()` method loads the problem, solves it, and prints the solution:

[source,java]
----
        // Load the problem
        TimeTable problem = generateDemoData();

        // Solve the problem
        Solver<TimeTable> solver = solverFactory.buildSolver();
        TimeTable solution = solver.solve(problem);

        // Visualize the solution
        printTimetable(solution);
----

The `solve()` method doesn't return instantly.
It runs for five seconds before returning the best solution.

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.

The `generateDemoData()` method generates the school timetable problem to solve.

The `printTimetable()` method pretty prints the timetable to the console,
so it's easy to determine visually whether or not it's a good schedule.

=== Configure logging

To see any output in the console, logging must be configured properly.

Create the `src/main/resource/logback.xml` file:

[source,xml]
----
<?xml version="1.0" encoding="UTF-8"?>
<configuration>

  <appender name="consoleAppender" class="ch.qos.logback.core.ConsoleAppender">
    <encoder>
      <pattern>%d{HH:mm:ss.SSS} [%-12.12t] %-5p %m%n</pattern>
    </encoder>
  </appender>

  <logger name="org.optaplanner" level="info"/>

  <root level="info">
    <appender-ref ref="consoleAppender" />
  </root>

</configuration>
----

== Run the application

=== Run the application in IDE

Run that `TimeTableApp` class as the main class of a normal Java application:

----
...
INFO  |            | Room A     | Room B     | Room C     |
INFO  |------------|------------|------------|------------|
INFO  | MON 08:30  | English    | Math       |            |
INFO  |            | I. Jones   | A. Turing  |            |
INFO  |            | 9th grade  | 10th grade |            |
INFO  |------------|------------|------------|------------|
INFO  | MON 09:30  | History    | Physics    |            |
INFO  |            | I. Jones   | M. Curie   |            |
INFO  |            | 9th grade  | 10th grade |            |
...
----

Verify the console output. Does it conform to all hard constraints?
What happens if you comment out the `roomConflict` constraint in `TimeTableConstraintProvider`?

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.
This tests each constraint's corner cases in isolation from the other tests,
which lowers maintenance when adding a new constraint with proper test coverage.

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 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;

class TimeTableConstraintProviderTest {

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

    ConstraintVerifier<TimeTableConstraintProvider, TimeTable> constraintVerifier = ConstraintVerifier.build(
            new TimeTableConstraintProvider(), TimeTable.class, Lesson.class);

    @Test
    void roomConflict() {
        Lesson firstLesson = new Lesson(1, "Subject1", "Teacher1", "Group1", TIMESLOT1, ROOM1);
        Lesson conflictingLesson = new Lesson(2, "Subject2", "Teacher2", "Group2", TIMESLOT1, ROOM1);
        Lesson nonConflictingLesson = new Lesson(3, "Subject3", "Teacher3", "Group3", TIMESLOT2, ROOM1);
        constraintVerifier.verifyThat(TimeTableConstraintProvider::roomConflict)
                .given(firstLesson, conflictingLesson, nonConflictingLesson)
                .penalizesBy(1);
    }

}
----

This test verifies that the constraint `TimeTableConstraintProvider::roomConflict` penalizes with a match weight of `1`
when given three lessons in the same room, where two lessons have the same timeslot.
Therefore, a constraint weight of `10hard` 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.

For more, see xref:constraint-streams/constraint-streams.adoc#constraintStreamsTesting[Testing Constraint Streams].

=== 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 `logback.xml` file:

[source,xml]
----
  <logger name="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.

=== Make a standalone application

In order to run the application outside an IDE easily, you will need to make some changes to the configuration of your build tool.

==== Executable JAR in Maven

In Maven, add the following to your `pom.xml`:

[source,xml,options="nowrap"]
----
  ...
  <build>
    <plugins>
      <plugin>
        <groupId>org.apache.maven.plugins</groupId>
        <artifactId>maven-assembly-plugin</artifactId>
        <version>${version.assembly.plugin}</version>
        <configuration>
          <finalName>hello-world-run</finalName>
          <appendAssemblyId>false</appendAssemblyId>
          <descriptors>
            <descriptor>src/assembly/jar-with-dependencies-and-services.xml</descriptor>
          </descriptors>
          <archive>
            <manifestEntries>
              <Main-Class>org.acme.schooltimetabling.TimeTableApp</Main-Class>
              <Multi-Release>true</Multi-Release>
            </manifestEntries>
          </archive>
        </configuration>
        <executions>
          <execution>
            <id>make-assembly</id>
            <phase>package</phase>
            <goals>
              <goal>single</goal>
            </goals>
          </execution>
        </executions>
      </plugin>
      ...
    </plugins>
    ...
  </build>
  ...
----

Also, create a new file in `src/assembly` directory called `jar-with-dependencies-and-services.xml` with the following contents:

[source,xml,options="nowrap"]
----
  <assembly xmlns="http://maven.apache.org/ASSEMBLY/2.1.0"
            xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
            xsi:schemaLocation="http://maven.apache.org/ASSEMBLY/2.1.0 http://maven.apache.org/xsd/assembly-2.1.0.xsd">
    <id>jar-with-dependencies-and-services</id>
    <formats>
      <format>jar</format>
    </formats>
    <containerDescriptorHandlers>
      <containerDescriptorHandler>
        <handlerName>metaInf-services</handlerName>
      </containerDescriptorHandler>
    </containerDescriptorHandlers>
    <includeBaseDirectory>false</includeBaseDirectory>
    <dependencySets>
      <dependencySet>
        <outputDirectory>/</outputDirectory>
        <useProjectArtifact>true</useProjectArtifact>
        <unpack>true</unpack>
        <scope>runtime</scope>
      </dependencySet>
    </dependencySets>
  </assembly>
----

This enables the https://maven.apache.org/plugins/maven-assembly-plugin/[Maven Assembly Plugin] and tells it to do the following:

* Take all dependencies of your project and put their classes and resources into a new JAR.
** If any of the dependencies use https://docs.oracle.com/javase/tutorial/ext/basics/spi.html[Java SPI], it properly bundles all the service descriptors.
** If any of the dependencies are https://openjdk.org/jeps/238[multi-release JARs], it takes that into account.
* Set that JAR's main class to be `org.acme.schooltimetabling.TimeTableApp`.
* Make that JAR available as `hello-world-run.jar` in your project's build directory, most likely `target/`.

This executable JAR can be run like any other JAR:

[source,options="nowrap"]
----
$ mvn clean install
...
$ java -jar target/hello-world-run.jar
----

==== Executable application in Gradle

In Gradle, add the following to your `build.gradle`:

[source,gradle,options="nowrap"]
----
application {
    mainClass = "org.acme.schooltimetabling.TimeTableApp"
}
----

After building the project, you can find an archive with a runnable application inside the `build/distributions/` directory.

== Summary

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

If you ran into any issues,
take a look at {hello-world-java-quickstart-url}[the quickstart source code].

Read the next guide to build a pretty web application for school timetabling
with a REST service and database integration, by leveraging Quarkus.
