////
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.
////

[[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.1.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.
