JBoss.orgCommunity Documentation
Planner's input and output data (the planning problem and the best solution) are plain old JavaBeans (POJO's), so integration with other Java technologies is straightforward. For example:
To read a planning problem from the database (and store the best solution in it), annotate the domain POJO's with JPA annotations.
To read a planning problem from an XML file (and store the best solution in it), annotate the domain POJO's with XStream or JAXB annotations.
To expose the Solver as a REST Service that reads the planning problem and responds with the best
solution, annotate the domain POJO's with XStream, JAXB or Jackson annotations and hook the Solver
in
Camel or RESTEasy.
Enrich the domain POJO's (solution, entities and problem facts) with JPA annotations to store them in a database.
Do not confuse JPA's @Entity
annotation with Planner's
@PlanningEntity
annotation. They can appear both on the same class:
@PlanningEntity // OptaPlanner annotation
@Entity // JPA annotation
public class Talk {...}
Add a dependency to the optaplanner-persistence-jpa
jar to take advantage of these extra
integration features:
When a Score
is persisted into a relational database, JPA and Hibernate will default to
Java serializing it to a BLOB
column. This has several disadvantages:
The Java serialization format of Score
classes is currently not backwards
compatible. Upgrading to a newer Planner version can break reading an existing database.
The score is not easily readable for a query executed in the database console. This is annoying during development.
The score cannot be used in a SQL or JPA-QL query to efficiently filter the results: for example to query all infeasible schedules.
To avoid these issues, configure it to instead use INTEGER (or other) columns, by using the appropriate
*ScoreHibernateType
for your Score
type, for example for a
HardSoftScore
:
@PlanningSolution
@Entity
@TypeDef(defaultForType = HardSoftScore.class, typeClass = HardSoftScoreHibernateType.class)
public class CloudBalance {
@PlanningScore
@Columns(columns = {@Column(name = "initScore"), @Column(name = "hardScore"), @Column(name = "softScore")})
protected HardSoftScore score;
...
}
Configure the same number of @Column
annotations as the number of score levels in the
score plus one (for the initScore
), otherwise Hibernate will fail fast because a property
mapping has the wrong number of columns.
In this case, the DDL will look like this:
CREATE TABLE CloudBalance(
...
initScore INTEGER,
hardScore INTEGER,
softScore INTEGER
);
When using a BigDecimal
based Score
, specify the precision and scale
of the columns to avoid silent rounding:
@PlanningSolution
@Entity
@TypeDef(defaultForType = HardSoftBigDecimalScore.class, typeClass = HardSoftBigDecimalScoreHibernateType.class)
public class CloudBalance{
@PlanningScore
@Columns(columns = {
@Column(name = "initScore")
@Column(name = "hardScore", precision = 10, scale = 5),
@Column(name = "softScore", precision = 10, scale = 5)})
protected HardSoftBigDecimalScore score;
...
}
In this case, the DDL will look like this:
CREATE TABLE CloudBalance(
...
initScore INTEGER,
hardScore DECIMAL(10, 5),
softScore DECIMAL(10, 5)
);
When using any type of bendable Score
, specify the hard and soft level sizes as
parameters:
@PlanningSolution
@Entity
@TypeDef(defaultForType = BendableScore.class, typeClass = BendableScoreHibernateType.class, parameters = {
@Parameter(name = "hardLevelsSize", value = "3"),
@Parameter(name = "softLevelsSize", value = "2")})
public class Schedule {
@PlanningScore
@Columns(columns = {
@Column(name = "initScore")
@Column(name = "hard0Score"),
@Column(name = "hard1Score"),
@Column(name = "hard2Score"),
@Column(name = "soft0Score"),
@Column(name = "soft1Score")})
protected BendableScore score;
...
}
All this support is Hibernate specific because currently JPA 2.1's converters do not support converting to multiple columns.
In JPA and Hibernate, there is usually a @ManyToOne
relationship from most problem fact
classes to the planning solution class. Therefore, the problem fact classes reference the planning solution
class, which implies that when the solution is planning cloned, they
need to be cloned too. Use an @DeepPlanningClone
on each such problem fact class to enforce
that:
@PlanningSolution // OptaPlanner annotation
@Entity // JPA annotation
public class Conference {
@OneToMany(mappedBy="conference")
private List<Room> roomList;
...
}
@DeepPlanningClone // OptaPlanner annotation: Force the default planning cloner to planning clone this class too
@Entity // JPA annotation
public class Room {
@ManyToOne
private Conference conference; // Because of this reference, this problem fact needs to be planning cloned too
}
Neglecting to do this can lead to persisting duplicate solutions, JPA exceptions or other side effects.
Enrich the domain POJO's (solution, entities and problem facts) with XStream annotations to serialize them to/from XML or JSON.
Add a dependency to the optaplanner-persistence-xstream
jar to take advantage of these
extra integration features:
When a Score
is marshalled to XML or JSON by the default XStream configuration, it's
verbose and ugly. To fix that, configure the appropriate ScoreXStreamConverter
:
@PlanningSolution
@XStreamAlias("CloudBalance")
public class CloudBalance {
@PlanningScore
@XStreamConverter(HardSoftScoreXStreamConverter.class)
private HardSoftScore score;
...
}
For example, this will generate pretty XML:
<CloudBalance>
...
<score>0hard/-200soft</score>
</CloudBalance>
The same applies for a bendable score:
@PlanningSolution
@XStreamAlias("Schedule")
public class Schedule {
@PlanningScore
@XStreamConverter(BendableScoreXStreamConverter.class)
private BendableScore score;
...
}
For example, this will generate:
<Schedule>
...
<score>[0/0]hard/[-100/-20/-3]soft</score>
</Schedule>
The hardLevelsSize
and softLevelsSize
implied, when reading a
bendable score from an XML element, must always be in sync with those in the solver.
Enrich the domain POJO's (solution, entities and problem facts) with JAXB annotations to serialize them to/from XML or JSON.
Add a dependency to the optaplanner-persistence-jaxb
jar to take advantage of these extra
integration features:
When a Score
is marshalled to XML or JSON by the default JAXB configuration, it's
corrupted. To fix that, configure the appropriate ScoreJaxbXmlAdapter
:
@PlanningSolution
@XmlRootElement @XmlAccessorType(XmlAccessType.FIELD)
public class CloudBalance {
@PlanningScore
@XmlJavaTypeAdapter(HardSoftScoreJaxbXmlAdapter.class)
private HardSoftScore score;
...
}
For example, this will generate pretty XML:
<cloudBalance>
...
<score>0hard/-200soft</score>
</cloudBalance>
The same applies for a bendable score:
@PlanningSolution
@XmlRootElement @XmlAccessorType(XmlAccessType.FIELD)
public class Schedule {
@PlanningScore
@XmlJavaTypeAdapter(BendableScoreJaxbXmlAdapter.class)
private BendableScore score;
...
}
For example, with a hardLevelsSize
of 2
and a
softLevelsSize
of 3
, that will generate:
<schedule>
...
<score>[0/0]hard/[-100/-20/-3]soft</score>
</schedule>
The hardLevelsSize
and softLevelsSize
implied, when reading a
bendable score from an XML element, must always be in sync with those in the solver.
Enrich the domain POJO's (solution, entities and problem facts) with Jackson annotations to serialize them to/from JSON.
Add a dependency to the optaplanner-persistence-jackson
jar to take advantage of these extra
integration features:
When a Score
is marshalled to JSON by the default Jackson configuration, it
fails. To fix that, configure a ScoreJacksonJsonSerializer
and the appropriate
ScoreJacksonJsonDeserializer
:
@PlanningSolution
public class CloudBalance {
@PlanningScore
@JsonSerialize(using = ScoreJacksonJsonSerializer.class)
@JsonDeserialize(using = HardSoftScoreJacksonJsonDeserializer.class)
private HardSoftScore score;
...
}
For example, this will generate pretty JSON:
{
...
"score":"0hard/-200soft"
}
The same applies for a bendable score:
@PlanningSolution
public class Schedule {
@PlanningScore
@JsonSerialize(using = ScoreJacksonJsonSerializer.class)
@JsonDeserialize(using = BendableScoreJacksonXmlAdapter.class)
private BendableScore score;
...
}
For example, with a hardLevelsSize
of 2
and a
softLevelsSize
of 3
, that will generate:
{
...
"score":"[0/0]hard/[-100/-20/-3]soft"
}
The hardLevelsSize
and softLevelsSize
implied, when reading a
bendable score from a JSON element, must always be in sync with those in the solver.
Camel is an enterprise integration framework which includes support for Planner (starting from Camel 2.13). It can expose a use case as a REST service, a SOAP service, a JMS service, ...
Read the documentation for the camel-optaplanner component. That component works in Karaf too.
To deploy an Planner web application on WildFly, simply include the optaplanner dependency jars in the
war
file's WEB-INF/lib
directory (just like any other dependency) as shown
in the optaplanner-webexamples-*.war
. However, in this approach the war file can easily grow to
several MB in size, which is fine for a one-time deployment, but too heavyweight for frequent redeployments
(especially over a slow network connection).
The remedy is to use deliver the optaplanner jars in a JBoss module to WildFly and create a skinny war. Let's create an module called org.optaplanner:
Navigate to the directory ${WILDFLY_HOME}/modules/system/layers/base/
.
This directory contains the JBoss modules of WildFly. Create directory structure
org/optaplanner/main
for our new module.
Copy optaplanner-core-${version}.jar
and all its direct and transitive dependency
jars into that new directory. Use "mvn dependency:tree" on each optaplanner artifact to discover all
dependencies.
Create the file module.xml
in that new directory. Give it this content:
<?xml version="1.0" encoding="UTF-8"?>
<module xmlns="urn:jboss:module:1.3" name="org.optaplanner">
<resources>
...
<resource-root path="kie-api-${version}.jar"/>
...
<resource-root path="optaplanner-core-${version}.jar"/>
...
<resource-root path="."/>
</resources>
<dependencies>
<module name="javaee.api"/>
</dependencies>
</module>
Navigate to the deployed war
file.
Remove optaplanner-core-${version}.jar
and all its direct and transitive
dependency jars from the WEB-INF/lib
directory in the war
file.
Create the file jboss-deployment-structure.xml
in the
WEB-INF/lib
directory. Give it this content:
<?xml version="1.0" encoding="UTF-8" ?>
<jboss-deployment-structure>
<deployment>
<dependencies>
<module name="org.optaplanner" export="true"/>
</dependencies>
</deployment>
</jboss-deployment-structure>
Because of JBoss Modules' ClassLoader
magic, you'll likely need to provide the
ClassLoader
of your classes during the SolverFactory
creation, so it can find the classpath resources (such as the solver config, score DRL's and domain
classes) in your jars.
The optaplanner-core
jar includes OSGi metadata in its MANIFEST.MF
file to function properly in an OSGi environment too. Furthermore, the maven artifact
drools-karaf-features
(which will be renamed to kie-karaf-features
) contains
a features.xml
file that supports the OSGi-feature
optaplanner-engine
.
Because of the OSGi's ClassLoader
magic, you'll likely need to provide the
ClassLoader
of your classes during the SolverFactory
creation, so it can find the classpath resources (such as the solver config, score DRL's and domain
classes) in your jars.
Planner does not require OSGi. It works perfectly fine in a normal Java environment too.
Android is not a complete JVM (because some JDK libraries are missing), but Planner works on Android with easy Java or incremental Java score calculation. The Drools rule engine does not work on Android yet, so Drools score calculation doesn't work on Android and its dependencies need to be excluded.
Workaround to use Planner on Android:
Add a dependency to the build.gradle
file in your Android project to exclude
org.drools
and xmlpull
dependencies:
dependencies {
...
compile('org.optaplanner:optaplanner-core:...') {
exclude group: 'xmlpull'
exclude group: 'org.drools'
}
...
}
A good Planner implementation beats any good human planner for non-trivial datasets. Many human planners fail to accept this, often because they feel threatened by an automated system.
But despite that, both can benefit if the human planner acts as supervisor to Planner:
The human planner defines and validates the score function.
Some examples expose a *Parametrization
object, which defines the weight for each
score constraint. The human planner can then tweak those weights at runtime.
When the business changes, the score function often needs to change too. The human planner can notify the developers to add, change or remove score constraints.
The human planner is always in control of Planner.
As shown in the course scheduling example, the human planner can lock 1 or more planning variables to a specific planning value and make those immovable. Because they are immovable, Planner does not change them: it optimizes the planning around the enforcements made by the human. If the human planner locks all planning variables, he/she sidelines Planner completely.
In a prototype implementation, the human planner might use this occasionally. But as the implementation matures, it must become obsolete. But do keep the feature alive: as a reassurance for the humans. Or in case that one day the business changes dramatically before the score constraints can be adjusted.
Therefore, it's often a good idea to involve the human planner in your project.