I am working off of the optaplanner-spring-boot-starter cloud-balancing project and I am trying to assign shifts to employees based on their skill level. However when I pass a JSON object to my roster controller I get an error saying:
java.lang.IllegalArgumentException: The solutionClass (class com.redhat.optaplannersbs.domain.Roster)'s entityCollectionProperty (bean property shiftList on class com.redhat.optaplannersbs.domain.Roster) should never return null.
I do not understand what the problem is as I am basically doing the same thing as the cloud-balancing problem and that runs and solves fine.
Here is my code for the employee class:
public class Employee {
private int eid;
private String name;
private int skillLevel;
public Employee(){
}
public int getEid() {
return eid;
}
public void setEid(int eid) {
this.eid = eid;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
// constraint getters and setters
public int getSkillLevel() {
return skillLevel;
}
public void setSkillLevel(int skillLevel) {
this.skillLevel = skillLevel;
}
}
Here is my code for the shift class:
@PlanningEntity
public class Shift {
private int sid;
private LocalTime startTime;
private LocalTime endTime;
private int requiredSkillLevel;
@PlanningVariable(valueRangeProviderRefs = "employee")
private Employee employee;
public Shift(){
}
public Shift(Long deptId, Long spotId, LocalTime startTime,
LocalTime endTime, Long employeeId){
this.startTime = startTime;
this.endTime = endTime;
}
public int getSid() {
return sid;
}
public void setSid(int sid) {
this.sid = sid;
}
public LocalTime getStartTime() {
return startTime;
}
public void setStartTime(LocalTime startTime) {
this.startTime = startTime;
}
public LocalTime getEndTime() {
return endTime;
}
public void setEndTime(LocalTime endTime) {
this.endTime = endTime;
}
// planning variable getter and setter
public Employee getEmployee() {
return employee;
}
public void setEmployee(Employee employee) {
this.employee = employee;
}
public int getRequiredSkillLevel() {
return requiredSkillLevel;
}
public void setRequiredSkillLevel(int requiredSkillLevel) {
this.requiredSkillLevel = requiredSkillLevel;
}
}
Here is my Roster class:
@PlanningSolution
public class Roster {
private List<Employee> employeeList;
private List<Shift> shiftList;
private HardSoftScore score;
public Roster(List<Employee> employeeList, List<Shift> shiftList) {
this.employeeList = employeeList;
this.shiftList = shiftList;
}
@ProblemFactCollectionProperty
@ValueRangeProvider(id="employee")
public List<Employee> getEmployeeList() {
return employeeList;
}
public void setEmployeeList(List<Employee> employeeList) {
this.employeeList = employeeList;
}
@PlanningEntityCollectionProperty
public List<Shift> getShiftList() {
return shiftList;
}
public void setShiftList(List<Shift> shiftList) {
this.shiftList = shiftList;
}
@PlanningScore
public HardSoftScore getScore() {
return score;
}
public void setScore(HardSoftScore score) {
this.score = score;
}
}
Here is my constraint provider:
public class ConstraintProvider implements org.optaplanner.core.api.score.stream.ConstraintProvider {
@Override
public Constraint[] defineConstraints(ConstraintFactory constraintFactory) {
return new Constraint[]{
requiredSkillLevelOfEmployeesForShifts(constraintFactory)
};
}
private Constraint requiredSkillLevelOfEmployeesForShifts(ConstraintFactory constraintFactory) {
return constraintFactory.from(Shift.class)
.groupBy(Shift::getEmployee, sum(Shift::getRequiredSkillLevel))
.filter((employee, requiredSkillLevel) -> requiredSkillLevel > employee.getSkillLevel())
.penalize("requiredSkillLevelForShifts",
HardSoftScore.ONE_HARD,
(employee, requiredSkillLevel) -> requiredSkillLevel - employee.getSkillLevel());
}
}
Here is my controller:
@RestController
@RequestMapping("/roster")
public class RosterController {
@Autowired
private SolverManager<Roster, UUID> solverManager;
@PostMapping("/solve")
public Roster solve(@RequestBody Roster problem) {
UUID problemId = UUID.randomUUID();
// Submit the problem to start solving
SolverJob<Roster, UUID> solverJob = solverManager.solve(problemId, problem);
Roster solution;
try {
// Wait until the solving ends
solution = solverJob.getFinalBestSolution();
} catch (InterruptedException | ExecutionException e) {
throw new IllegalStateException("Solving failed.", e);
}
return solution;
}
}
The JSON data I pass via a post request is as follows:
{
"shifts":[
{
"sid":0,
"startTime":"09:00",
"endTime":"18:00",
"requiredSkillLevel": 12
},
{
"sid":1,
"startTime":"12:00",
"endTime":"20:00",
"requiredSkillLevel": 10
},
{
"sid":2,
"startTime":"18:00",
"endTime":"00:00",
"requiredSkillLevel": 10
},
{
"sid": 3,
"startTime":"09:00",
"endTime":"18:00",
"requiredSkillLevel": 12
},
{
"sid":4,
"startTime":"12:00",
"endTime":"20:00",
"requiredSkillLevel": 10
},
{
"sid":5,
"startTime":"18:00",
"endTime":"00:00",
"requiredSkillLevel":10
},
{
"sid":6,
"startTime":"09:00",
"endTime":"18:00",
"requiredSkillLevel": 12
},
{
"sid":7,
"startTime":"12:00",
"endTime":"20:00",
"requiredSkillLevel": 10
},
{
"sid":8,
"startTime":"18:00",
"endTime":"00:00",
"requiredSkillLevel":10
},
{
"sid":9,
"startTime":"09:00",
"endTime":"18:00",
"requiredSkillLevel": 12
},
{
"sid":10,
"startTime":"12:00",
"endTime":"20:00",
"requiredSkillLevel": 10
},
{
"sid":11,
"startTime":"18:00",
"endTime":"00:00",
"requiredSkillLevel":10
},
{
"sid":12,
"startTime":"09:00",
"endTime":"18:00",
"requiredSkillLevel": 12
},
{
"sid":13,
"startTime":"12:00",
"endTime":"20:00",
"requiredSkillLevel": 10
},
{
"sid":14,
"startTime":"18:00",
"endTime":"00:00",
"requiredSkillLevel":10
}
],
"employees":[
{
"eid":0,
"name":"john",
"skillLevel": 10
},
{
"eid":1,
"name":"elaine",
"skillLevel": 2
},
{
"eid":2,
"name":"kieran",
"skillLevel": 11
},
{
"eid":3,
"name":"maeve",
"skillLevel": 10
},
{
"eid":4,
"name":"steve",
"skillLevel": 9
},
{
"eid":5,
"name":"steve",
"skillLevel": 9
},
{
"eid":6,
"name":"steve",
"skillLevel": 15
},
{
"eid":7,
"name":"amy",
"skillLevel": 11
}
]
}
I should not be getting this error as I am doing an even simpler version of the cloud balancing application, if anyone can figure out where I am going wrong that would be a big help
Put a breakpoint in your RosterController
, just before you call solverManager.solve(...)
. You'll see that your Roster
instance has a shiftList
field that is null
.
The problem is in the json unmarshalling of your input data, because of naming mismatch in your input json. Note that by default Jackson in Spring Boot ignores properties that doesn't exist, instead of fail-fasting (a design decision I've never understood). There's a property to change that behavior IIRC.