updated coding standards
All checks were successful
Build, test and publish / verify (push) Successful in 4m43s
All checks were successful
Build, test and publish / verify (push) Successful in 4m43s
This commit is contained in:
@@ -3,7 +3,11 @@ package nl.agilecc.agiledancers.model;
|
||||
import java.util.Collections;
|
||||
import java.util.HashSet;
|
||||
import java.util.Set;
|
||||
import lombok.*;
|
||||
import lombok.EqualsAndHashCode;
|
||||
import lombok.Getter;
|
||||
import lombok.NoArgsConstructor;
|
||||
import lombok.Setter;
|
||||
import lombok.ToString;
|
||||
|
||||
@Getter
|
||||
@Setter
|
||||
@@ -13,15 +17,15 @@ import lombok.*;
|
||||
public abstract class AbstractEntity {
|
||||
private Long id;
|
||||
|
||||
protected <T> void addNullSafe(Set<T> set, T t) {
|
||||
protected <T> void addNullSafe(final Set<T> set, final T t) {
|
||||
emptyIfNull(set).add(t);
|
||||
}
|
||||
|
||||
private <T> Set<T> emptyIfNull(Set<T> set) {
|
||||
private <T> Set<T> emptyIfNull(final Set<T> set) {
|
||||
return set == null ? new HashSet<>() : set;
|
||||
}
|
||||
|
||||
protected <T> Set<T> unmodifiableNullSafe(Set<T> set) {
|
||||
protected <T> Set<T> unmodifiableNullSafe(final Set<T> set) {
|
||||
return set == null ? Collections.emptySet() : Collections.unmodifiableSet(set);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,10 @@
|
||||
package nl.agilecc.agiledancers.model;
|
||||
|
||||
import lombok.*;
|
||||
import lombok.AccessLevel;
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.Builder;
|
||||
import lombok.Getter;
|
||||
import lombok.ToString;
|
||||
|
||||
@Getter
|
||||
@AllArgsConstructor(access = AccessLevel.PRIVATE)
|
||||
|
||||
@@ -2,7 +2,12 @@ package nl.agilecc.agiledancers.model;
|
||||
|
||||
import java.util.LinkedHashSet;
|
||||
import java.util.Set;
|
||||
import lombok.*;
|
||||
import lombok.AccessLevel;
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.Builder;
|
||||
import lombok.Getter;
|
||||
import lombok.Setter;
|
||||
import lombok.ToString;
|
||||
import nl.agilecc.agiledancers.model.specifications.DanceGroupStyle;
|
||||
import nl.agilecc.agiledancers.model.specifications.DanceGroupType;
|
||||
|
||||
@@ -42,7 +47,7 @@ public class DanceGroup extends AbstractEntity {
|
||||
return super.unmodifiableNullSafe(dancers);
|
||||
}
|
||||
|
||||
public void addDancer(Dancer dancer) {
|
||||
public void addDancer(final Dancer dancer) {
|
||||
super.addNullSafe(dancers, dancer);
|
||||
}
|
||||
|
||||
@@ -50,15 +55,15 @@ public class DanceGroup extends AbstractEntity {
|
||||
return super.unmodifiableNullSafe(styles);
|
||||
}
|
||||
|
||||
public void addStyle(DanceGroupStyle style) {
|
||||
public void addStyle(final DanceGroupStyle style) {
|
||||
super.addNullSafe(styles, style);
|
||||
}
|
||||
|
||||
public void removeDancer(Dancer dancer) {
|
||||
public void removeDancer(final Dancer dancer) {
|
||||
dancers.remove(dancer);
|
||||
}
|
||||
|
||||
public void replaceStyles(Set<DanceGroupStyle> styles) {
|
||||
public void replaceStyles(final Set<DanceGroupStyle> styles) {
|
||||
this.styles.clear();
|
||||
this.styles.addAll(styles);
|
||||
}
|
||||
|
||||
@@ -2,7 +2,12 @@ package nl.agilecc.agiledancers.model;
|
||||
|
||||
import java.util.LinkedHashSet;
|
||||
import java.util.Set;
|
||||
import lombok.*;
|
||||
import lombok.AccessLevel;
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.Builder;
|
||||
import lombok.Getter;
|
||||
import lombok.Setter;
|
||||
import lombok.ToString;
|
||||
|
||||
@Getter
|
||||
@AllArgsConstructor(access = AccessLevel.PRIVATE)
|
||||
@@ -43,31 +48,31 @@ public class DanceSchool extends AbstractEntity {
|
||||
return super.unmodifiableNullSafe(coaches);
|
||||
}
|
||||
|
||||
public void addDancer(Dancer dancer) {
|
||||
public void addDancer(final Dancer dancer) {
|
||||
super.addNullSafe(dancers, dancer);
|
||||
}
|
||||
|
||||
public void addDanceGroup(DanceGroup danceGroup) {
|
||||
public void addDanceGroup(final DanceGroup danceGroup) {
|
||||
super.addNullSafe(danceGroups, danceGroup);
|
||||
}
|
||||
|
||||
public void addShow(Show show) {
|
||||
public void addShow(final Show show) {
|
||||
super.addNullSafe(shows, show);
|
||||
}
|
||||
|
||||
public void addCoach(Coach coach) {
|
||||
public void addCoach(final Coach coach) {
|
||||
super.addNullSafe(coaches, coach);
|
||||
}
|
||||
|
||||
public void removeDancer(Dancer entity) {
|
||||
public void removeDancer(final Dancer entity) {
|
||||
dancers.remove(entity);
|
||||
}
|
||||
|
||||
public void removeDanceGroup(DanceGroup entity) {
|
||||
public void removeDanceGroup(final DanceGroup entity) {
|
||||
danceGroups.remove(entity);
|
||||
}
|
||||
|
||||
public void removeShow(Show entity) {
|
||||
public void removeShow(final Show entity) {
|
||||
shows.remove(entity);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,7 +2,12 @@ package nl.agilecc.agiledancers.model;
|
||||
|
||||
import java.time.LocalDate;
|
||||
import java.time.Period;
|
||||
import lombok.*;
|
||||
import lombok.AccessLevel;
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.Builder;
|
||||
import lombok.Getter;
|
||||
import lombok.Setter;
|
||||
import lombok.ToString;
|
||||
|
||||
@Getter
|
||||
@AllArgsConstructor(access = AccessLevel.PRIVATE)
|
||||
|
||||
@@ -2,7 +2,12 @@ package nl.agilecc.agiledancers.model;
|
||||
|
||||
import java.time.LocalDate;
|
||||
import java.util.List;
|
||||
import lombok.*;
|
||||
import lombok.AccessLevel;
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.Builder;
|
||||
import lombok.Getter;
|
||||
import lombok.Setter;
|
||||
import lombok.ToString;
|
||||
import nl.agilecc.agiledancers.model.specifications.DayPartType;
|
||||
|
||||
@Getter
|
||||
@@ -19,7 +24,9 @@ public class DayPart extends AbstractEntity {
|
||||
@Setter
|
||||
private List<DanceGroup> danceGroups;
|
||||
|
||||
public void removeDanceGroup(DanceGroup group) {
|
||||
danceGroups = danceGroups.stream().filter(value -> value != group).toList();
|
||||
public void removeDanceGroup(final DanceGroup group) {
|
||||
danceGroups = danceGroups.stream()
|
||||
.filter((final DanceGroup value) -> value != group)
|
||||
.toList();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,7 +2,12 @@ package nl.agilecc.agiledancers.model;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import lombok.*;
|
||||
import lombok.AccessLevel;
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.Builder;
|
||||
import lombok.Getter;
|
||||
import lombok.Setter;
|
||||
import lombok.ToString;
|
||||
import nl.agilecc.agiledancers.model.specifications.DanceGroupType;
|
||||
|
||||
@Getter
|
||||
|
||||
@@ -1,5 +1,16 @@
|
||||
package nl.agilecc.agiledancers.model.scheduling;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
|
||||
public record SchedulingResult(boolean optimal, List<List<Long>> assignments) {}
|
||||
public record SchedulingResult(boolean optimal, List<List<Long>> assignments) {
|
||||
public SchedulingResult {
|
||||
assignments = assignments == null
|
||||
? null
|
||||
: assignments.stream()
|
||||
.map((final List<Long> part) ->
|
||||
part == null ? null : Collections.unmodifiableList(new ArrayList<>(part)))
|
||||
.toList();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,18 +1,20 @@
|
||||
package nl.agilecc.agiledancers.repository;
|
||||
|
||||
import java.util.*;
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import nl.agilecc.agiledancers.model.AbstractEntity;
|
||||
|
||||
public abstract class AbstractRepository<T extends AbstractEntity> {
|
||||
private long sequence = 0L;
|
||||
|
||||
protected Map<Long, T> entities = new LinkedHashMap<>();
|
||||
protected final Map<Long, T> entities = new LinkedHashMap<>();
|
||||
|
||||
public long nextId() {
|
||||
return sequence++;
|
||||
}
|
||||
|
||||
public T add(T entity) {
|
||||
public T add(final T entity) {
|
||||
if (entity.getId() == null) {
|
||||
entity.setId(nextId());
|
||||
}
|
||||
@@ -24,11 +26,11 @@ public abstract class AbstractRepository<T extends AbstractEntity> {
|
||||
return List.copyOf(entities.values());
|
||||
}
|
||||
|
||||
public T findById(long id) {
|
||||
public T findById(final long id) {
|
||||
return entities.get(id);
|
||||
}
|
||||
|
||||
public void delete(long id) {
|
||||
public void delete(final long id) {
|
||||
entities.remove(id);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,22 +3,32 @@ package nl.agilecc.agiledancers.rest;
|
||||
import jakarta.inject.Inject;
|
||||
import jakarta.validation.Valid;
|
||||
import jakarta.validation.constraints.NotNull;
|
||||
import jakarta.ws.rs.*;
|
||||
import jakarta.ws.rs.core.*;
|
||||
import jakarta.ws.rs.Consumes;
|
||||
import jakarta.ws.rs.DELETE;
|
||||
import jakarta.ws.rs.GET;
|
||||
import jakarta.ws.rs.POST;
|
||||
import jakarta.ws.rs.PUT;
|
||||
import jakarta.ws.rs.Path;
|
||||
import jakarta.ws.rs.PathParam;
|
||||
import jakarta.ws.rs.Produces;
|
||||
import jakarta.ws.rs.core.Context;
|
||||
import jakarta.ws.rs.core.MediaType;
|
||||
import jakarta.ws.rs.core.Response;
|
||||
import jakarta.ws.rs.core.UriInfo;
|
||||
import java.util.List;
|
||||
import nl.agilecc.agiledancers.model.DanceGroup;
|
||||
import nl.agilecc.agiledancers.rest.apimodel.GroupRequest;
|
||||
import nl.agilecc.agiledancers.rest.apimodel.GroupView;
|
||||
import nl.agilecc.agiledancers.service.SchoolManagementService;
|
||||
import nl.agilecc.agiledancers.service.model.GroupDetails;
|
||||
|
||||
@Path("/api/dance-schools/{schoolId}/dance-groups")
|
||||
@Produces(MediaType.APPLICATION_JSON)
|
||||
@Consumes(MediaType.APPLICATION_JSON)
|
||||
public class DanceGroupResource {
|
||||
@Inject
|
||||
SchoolManagementService service;
|
||||
private @Inject SchoolManagementService service;
|
||||
|
||||
@PathParam("schoolId")
|
||||
long schoolId;
|
||||
private @PathParam("schoolId") long schoolId;
|
||||
|
||||
@GET
|
||||
public List<GroupView> list() {
|
||||
@@ -27,14 +37,16 @@ public class DanceGroupResource {
|
||||
|
||||
@GET
|
||||
@Path("/{id}")
|
||||
public GroupView get(@PathParam("id") long id) {
|
||||
public GroupView get(final @PathParam("id") long id) {
|
||||
return GroupView.of(service.group(schoolId, id));
|
||||
}
|
||||
|
||||
@POST
|
||||
public Response create(@NotNull @Valid GroupRequest request, @Context UriInfo uriInfo) {
|
||||
var entity = service.createGroup(
|
||||
schoolId, request.category(), request.type(), request.startAge(), request.endAge(), request.styles());
|
||||
public Response create(final @NotNull @Valid GroupRequest request, final @Context UriInfo uriInfo) {
|
||||
final DanceGroup entity = service.createGroup(
|
||||
schoolId,
|
||||
new GroupDetails(
|
||||
request.category(), request.type(), request.startAge(), request.endAge(), request.styles()));
|
||||
return Response.created(uriInfo.getAbsolutePathBuilder()
|
||||
.path(entity.getId().toString())
|
||||
.build())
|
||||
@@ -44,34 +56,31 @@ public class DanceGroupResource {
|
||||
|
||||
@PUT
|
||||
@Path("/{id}")
|
||||
public GroupView update(@PathParam("id") long id, @NotNull @Valid GroupRequest request) {
|
||||
public GroupView update(final @PathParam("id") long id, final @NotNull @Valid GroupRequest request) {
|
||||
return GroupView.of(service.updateGroup(
|
||||
schoolId,
|
||||
id,
|
||||
request.category(),
|
||||
request.type(),
|
||||
request.startAge(),
|
||||
request.endAge(),
|
||||
request.styles()));
|
||||
new GroupDetails(
|
||||
request.category(), request.type(), request.startAge(), request.endAge(), request.styles())));
|
||||
}
|
||||
|
||||
@DELETE
|
||||
@Path("/{id}")
|
||||
public Response delete(@PathParam("id") long id) {
|
||||
public Response delete(final @PathParam("id") long id) {
|
||||
service.deleteGroup(schoolId, id);
|
||||
return Response.noContent().build();
|
||||
}
|
||||
|
||||
@PUT
|
||||
@Path("/{id}/dancers/{dancerId}")
|
||||
public Response addDancer(@PathParam("id") long id, @PathParam("dancerId") long dancerId) {
|
||||
public Response addDancer(final @PathParam("id") long id, final @PathParam("dancerId") long dancerId) {
|
||||
service.addGroupDancer(schoolId, id, dancerId);
|
||||
return Response.noContent().build();
|
||||
}
|
||||
|
||||
@DELETE
|
||||
@Path("/{id}/dancers/{dancerId}")
|
||||
public Response removeDancer(@PathParam("id") long id, @PathParam("dancerId") long dancerId) {
|
||||
public Response removeDancer(final @PathParam("id") long id, final @PathParam("dancerId") long dancerId) {
|
||||
service.removeGroupDancer(schoolId, id, dancerId);
|
||||
return Response.noContent().build();
|
||||
}
|
||||
|
||||
@@ -3,9 +3,20 @@ package nl.agilecc.agiledancers.rest;
|
||||
import jakarta.inject.Inject;
|
||||
import jakarta.validation.Valid;
|
||||
import jakarta.validation.constraints.NotNull;
|
||||
import jakarta.ws.rs.*;
|
||||
import jakarta.ws.rs.core.*;
|
||||
import jakarta.ws.rs.Consumes;
|
||||
import jakarta.ws.rs.DELETE;
|
||||
import jakarta.ws.rs.GET;
|
||||
import jakarta.ws.rs.POST;
|
||||
import jakarta.ws.rs.PUT;
|
||||
import jakarta.ws.rs.Path;
|
||||
import jakarta.ws.rs.PathParam;
|
||||
import jakarta.ws.rs.Produces;
|
||||
import jakarta.ws.rs.core.Context;
|
||||
import jakarta.ws.rs.core.MediaType;
|
||||
import jakarta.ws.rs.core.Response;
|
||||
import jakarta.ws.rs.core.UriInfo;
|
||||
import java.util.List;
|
||||
import nl.agilecc.agiledancers.model.DanceSchool;
|
||||
import nl.agilecc.agiledancers.rest.apimodel.SchoolRequest;
|
||||
import nl.agilecc.agiledancers.rest.apimodel.SchoolView;
|
||||
import nl.agilecc.agiledancers.service.SchoolManagementService;
|
||||
@@ -14,8 +25,7 @@ import nl.agilecc.agiledancers.service.SchoolManagementService;
|
||||
@Produces(MediaType.APPLICATION_JSON)
|
||||
@Consumes(MediaType.APPLICATION_JSON)
|
||||
public class DanceSchoolResource {
|
||||
@Inject
|
||||
SchoolManagementService service;
|
||||
private @Inject SchoolManagementService service;
|
||||
|
||||
@GET
|
||||
public List<SchoolView> list() {
|
||||
@@ -24,13 +34,13 @@ public class DanceSchoolResource {
|
||||
|
||||
@GET
|
||||
@Path("/{id}")
|
||||
public SchoolView get(@PathParam("id") long id) {
|
||||
public SchoolView get(final @PathParam("id") long id) {
|
||||
return SchoolView.of(service.school(id));
|
||||
}
|
||||
|
||||
@POST
|
||||
public Response create(@NotNull @Valid SchoolRequest request, @Context UriInfo uriInfo) {
|
||||
var entity = service.createSchool(request.name());
|
||||
public Response create(final @NotNull @Valid SchoolRequest request, final @Context UriInfo uriInfo) {
|
||||
final DanceSchool entity = service.createSchool(request.name());
|
||||
return Response.created(uriInfo.getAbsolutePathBuilder()
|
||||
.path(entity.getId().toString())
|
||||
.build())
|
||||
@@ -40,13 +50,13 @@ public class DanceSchoolResource {
|
||||
|
||||
@PUT
|
||||
@Path("/{id}")
|
||||
public SchoolView update(@PathParam("id") long id, @NotNull @Valid SchoolRequest request) {
|
||||
public SchoolView update(final @PathParam("id") long id, final @NotNull @Valid SchoolRequest request) {
|
||||
return SchoolView.of(service.updateSchool(id, request.name()));
|
||||
}
|
||||
|
||||
@DELETE
|
||||
@Path("/{id}")
|
||||
public Response delete(@PathParam("id") long id) {
|
||||
public Response delete(final @PathParam("id") long id) {
|
||||
service.deleteSchool(id);
|
||||
return Response.noContent().build();
|
||||
}
|
||||
|
||||
@@ -3,9 +3,20 @@ package nl.agilecc.agiledancers.rest;
|
||||
import jakarta.inject.Inject;
|
||||
import jakarta.validation.Valid;
|
||||
import jakarta.validation.constraints.NotNull;
|
||||
import jakarta.ws.rs.*;
|
||||
import jakarta.ws.rs.core.*;
|
||||
import jakarta.ws.rs.Consumes;
|
||||
import jakarta.ws.rs.DELETE;
|
||||
import jakarta.ws.rs.GET;
|
||||
import jakarta.ws.rs.POST;
|
||||
import jakarta.ws.rs.PUT;
|
||||
import jakarta.ws.rs.Path;
|
||||
import jakarta.ws.rs.PathParam;
|
||||
import jakarta.ws.rs.Produces;
|
||||
import jakarta.ws.rs.core.Context;
|
||||
import jakarta.ws.rs.core.MediaType;
|
||||
import jakarta.ws.rs.core.Response;
|
||||
import jakarta.ws.rs.core.UriInfo;
|
||||
import java.util.List;
|
||||
import nl.agilecc.agiledancers.model.Dancer;
|
||||
import nl.agilecc.agiledancers.rest.apimodel.DancerRequest;
|
||||
import nl.agilecc.agiledancers.rest.apimodel.DancerView;
|
||||
import nl.agilecc.agiledancers.service.SchoolManagementService;
|
||||
@@ -14,11 +25,9 @@ import nl.agilecc.agiledancers.service.SchoolManagementService;
|
||||
@Produces(MediaType.APPLICATION_JSON)
|
||||
@Consumes(MediaType.APPLICATION_JSON)
|
||||
public class DancerResource {
|
||||
@Inject
|
||||
SchoolManagementService service;
|
||||
private @Inject SchoolManagementService service;
|
||||
|
||||
@PathParam("schoolId")
|
||||
long schoolId;
|
||||
private @PathParam("schoolId") long schoolId;
|
||||
|
||||
@GET
|
||||
public List<DancerView> list() {
|
||||
@@ -27,13 +36,13 @@ public class DancerResource {
|
||||
|
||||
@GET
|
||||
@Path("/{id}")
|
||||
public DancerView get(@PathParam("id") long id) {
|
||||
public DancerView get(final @PathParam("id") long id) {
|
||||
return DancerView.of(service.dancer(schoolId, id));
|
||||
}
|
||||
|
||||
@POST
|
||||
public Response create(@NotNull @Valid DancerRequest request, @Context UriInfo uriInfo) {
|
||||
var entity = service.createDancer(schoolId, request.name(), request.birthDate());
|
||||
public Response create(final @NotNull @Valid DancerRequest request, final @Context UriInfo uriInfo) {
|
||||
final Dancer entity = service.createDancer(schoolId, request.name(), request.birthDate());
|
||||
return Response.created(uriInfo.getAbsolutePathBuilder()
|
||||
.path(entity.getId().toString())
|
||||
.build())
|
||||
@@ -43,13 +52,13 @@ public class DancerResource {
|
||||
|
||||
@PUT
|
||||
@Path("/{id}")
|
||||
public DancerView update(@PathParam("id") long id, @NotNull @Valid DancerRequest request) {
|
||||
public DancerView update(final @PathParam("id") long id, final @NotNull @Valid DancerRequest request) {
|
||||
return DancerView.of(service.updateDancer(schoolId, id, request.name(), request.birthDate()));
|
||||
}
|
||||
|
||||
@DELETE
|
||||
@Path("/{id}")
|
||||
public Response delete(@PathParam("id") long id) {
|
||||
public Response delete(final @PathParam("id") long id) {
|
||||
service.deleteDancer(schoolId, id);
|
||||
return Response.noContent().build();
|
||||
}
|
||||
|
||||
@@ -8,7 +8,7 @@ import jakarta.inject.Singleton;
|
||||
@Singleton
|
||||
public class JacksonConfiguration implements ObjectMapperCustomizer {
|
||||
@Override
|
||||
public void customize(ObjectMapper mapper) {
|
||||
public void customize(final ObjectMapper mapper) {
|
||||
mapper.disable(DeserializationFeature.ACCEPT_FLOAT_AS_INT);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -10,8 +10,8 @@ import nl.agilecc.agiledancers.service.ManagementException;
|
||||
@Provider
|
||||
public class ManagementExceptionMapper implements ExceptionMapper<ManagementException> {
|
||||
@Override
|
||||
public Response toResponse(ManagementException exception) {
|
||||
int status = switch (exception.getKind()) {
|
||||
public Response toResponse(final ManagementException exception) {
|
||||
final int status = switch (exception.getKind()) {
|
||||
case NOT_FOUND -> 404;
|
||||
case INVALID_INPUT -> 400;
|
||||
case CONFLICT -> 409;
|
||||
|
||||
@@ -1,27 +1,37 @@
|
||||
package nl.agilecc.agiledancers.rest;
|
||||
|
||||
import com.fasterxml.jackson.core.JsonParser;
|
||||
import com.fasterxml.jackson.databind.*;
|
||||
import com.fasterxml.jackson.databind.DeserializationFeature;
|
||||
import com.fasterxml.jackson.databind.MapperFeature;
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
import jakarta.inject.Inject;
|
||||
import jakarta.ws.rs.*;
|
||||
import jakarta.ws.rs.core.*;
|
||||
import jakarta.ws.rs.Consumes;
|
||||
import jakarta.ws.rs.GET;
|
||||
import jakarta.ws.rs.POST;
|
||||
import jakarta.ws.rs.Path;
|
||||
import jakarta.ws.rs.PathParam;
|
||||
import jakarta.ws.rs.Produces;
|
||||
import jakarta.ws.rs.QueryParam;
|
||||
import jakarta.ws.rs.core.MediaType;
|
||||
import jakarta.ws.rs.core.Response;
|
||||
import nl.agilecc.agiledancers.rest.apimodel.SchoolView;
|
||||
import nl.agilecc.agiledancers.service.*;
|
||||
import nl.agilecc.agiledancers.service.model.*;
|
||||
import nl.agilecc.agiledancers.service.ManagementException;
|
||||
import nl.agilecc.agiledancers.service.ManagementExceptionKind;
|
||||
import nl.agilecc.agiledancers.service.SchoolManagementService;
|
||||
import nl.agilecc.agiledancers.service.model.ImportPreview;
|
||||
import nl.agilecc.agiledancers.service.model.SchoolSnapshot;
|
||||
|
||||
@Path("/api/dance-schools")
|
||||
@Produces(MediaType.APPLICATION_JSON)
|
||||
public class SchoolSnapshotResource {
|
||||
@Inject
|
||||
SchoolManagementService service;
|
||||
private @Inject SchoolManagementService service;
|
||||
|
||||
@Inject
|
||||
ObjectMapper mapper;
|
||||
private @Inject ObjectMapper mapper;
|
||||
|
||||
@GET
|
||||
@Path("/{id}/export")
|
||||
public Response export(@PathParam("id") long id) throws Exception {
|
||||
String json = mapper.writerWithDefaultPrettyPrinter().writeValueAsString(service.exportSchool(id));
|
||||
public Response export(final @PathParam("id") long id) throws Exception {
|
||||
final String json = mapper.writerWithDefaultPrettyPrinter().writeValueAsString(service.exportSchool(id));
|
||||
return Response.ok(json)
|
||||
.header("Content-Disposition", "attachment; filename=school-" + id + ".json")
|
||||
.build();
|
||||
@@ -30,29 +40,31 @@ public class SchoolSnapshotResource {
|
||||
@POST
|
||||
@Path("/import/preview")
|
||||
@Consumes(MediaType.APPLICATION_JSON)
|
||||
public ImportPreview preview(String json) {
|
||||
public ImportPreview preview(final String json) {
|
||||
return service.preview(parse(json));
|
||||
}
|
||||
|
||||
@POST
|
||||
@Path("/import")
|
||||
@Consumes(MediaType.APPLICATION_JSON)
|
||||
public SchoolView apply(String json, @QueryParam("replaceId") Long replaceId) {
|
||||
public SchoolView apply(final String json, final @QueryParam("replaceId") Long replaceId) {
|
||||
return SchoolView.of(service.importSchool(parse(json), replaceId));
|
||||
}
|
||||
|
||||
private SchoolSnapshot parse(String json) {
|
||||
private SchoolSnapshot parse(final String json) {
|
||||
try {
|
||||
SchoolSnapshot snapshot = mapper.copy()
|
||||
final SchoolSnapshot snapshot = mapper.copy()
|
||||
.enable(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES)
|
||||
.enable(DeserializationFeature.FAIL_ON_TRAILING_TOKENS)
|
||||
.disable(MapperFeature.ALLOW_COERCION_OF_SCALARS)
|
||||
.disable(DeserializationFeature.ACCEPT_FLOAT_AS_INT)
|
||||
.enable(JsonParser.Feature.STRICT_DUPLICATE_DETECTION)
|
||||
.readValue(json, SchoolSnapshot.class);
|
||||
if (snapshot == null) throw new IllegalArgumentException("Het bestand bevat geen school");
|
||||
if (snapshot == null) {
|
||||
throw new IllegalArgumentException("Het bestand bevat geen school");
|
||||
}
|
||||
return snapshot;
|
||||
} catch (Exception e) {
|
||||
} catch (final Exception e) {
|
||||
throw new ManagementException(
|
||||
ManagementExceptionKind.INVALID_INPUT, "Ongeldig JSON-bestand: " + e.getMessage());
|
||||
}
|
||||
|
||||
@@ -3,10 +3,23 @@ package nl.agilecc.agiledancers.rest;
|
||||
import jakarta.inject.Inject;
|
||||
import jakarta.validation.Valid;
|
||||
import jakarta.validation.constraints.NotNull;
|
||||
import jakarta.ws.rs.*;
|
||||
import jakarta.ws.rs.core.*;
|
||||
import jakarta.ws.rs.Consumes;
|
||||
import jakarta.ws.rs.DELETE;
|
||||
import jakarta.ws.rs.GET;
|
||||
import jakarta.ws.rs.POST;
|
||||
import jakarta.ws.rs.PUT;
|
||||
import jakarta.ws.rs.Path;
|
||||
import jakarta.ws.rs.PathParam;
|
||||
import jakarta.ws.rs.Produces;
|
||||
import jakarta.ws.rs.core.Context;
|
||||
import jakarta.ws.rs.core.MediaType;
|
||||
import jakarta.ws.rs.core.Response;
|
||||
import jakarta.ws.rs.core.UriInfo;
|
||||
import java.util.List;
|
||||
import nl.agilecc.agiledancers.model.DayPart;
|
||||
import nl.agilecc.agiledancers.model.Show;
|
||||
import nl.agilecc.agiledancers.model.scheduling.SchedulingResult;
|
||||
import nl.agilecc.agiledancers.rest.apimodel.DayPartRequest;
|
||||
import nl.agilecc.agiledancers.rest.apimodel.SchedulingView;
|
||||
import nl.agilecc.agiledancers.rest.apimodel.ShowRequest;
|
||||
import nl.agilecc.agiledancers.rest.apimodel.ShowView;
|
||||
@@ -17,14 +30,11 @@ import nl.agilecc.agiledancers.service.SchoolManagementService;
|
||||
@Produces(MediaType.APPLICATION_JSON)
|
||||
@Consumes(MediaType.APPLICATION_JSON)
|
||||
public class ShowResource {
|
||||
@Inject
|
||||
SchoolManagementService service;
|
||||
private @Inject SchoolManagementService service;
|
||||
|
||||
@Inject
|
||||
SchedulingService scheduling;
|
||||
private @Inject SchedulingService scheduling;
|
||||
|
||||
@PathParam("schoolId")
|
||||
long schoolId;
|
||||
private @PathParam("schoolId") long schoolId;
|
||||
|
||||
@GET
|
||||
public List<ShowView> list() {
|
||||
@@ -33,13 +43,13 @@ public class ShowResource {
|
||||
|
||||
@GET
|
||||
@Path("/{id}")
|
||||
public ShowView get(@PathParam("id") long id) {
|
||||
public ShowView get(final @PathParam("id") long id) {
|
||||
return ShowView.of(service.show(schoolId, id));
|
||||
}
|
||||
|
||||
@POST
|
||||
public Response create(@NotNull @Valid ShowRequest request, @Context UriInfo uriInfo) {
|
||||
var entity = service.createShow(schoolId, request.name(), parts(request), request.appearances());
|
||||
public Response create(final @NotNull @Valid ShowRequest request, final @Context UriInfo uriInfo) {
|
||||
final Show entity = service.createShow(schoolId, request.name(), parts(request), request.appearances());
|
||||
return Response.created(uriInfo.getAbsolutePathBuilder()
|
||||
.path(entity.getId().toString())
|
||||
.build())
|
||||
@@ -49,13 +59,13 @@ public class ShowResource {
|
||||
|
||||
@PUT
|
||||
@Path("/{id}")
|
||||
public ShowView update(@PathParam("id") long id, @NotNull @Valid ShowRequest request) {
|
||||
public ShowView update(final @PathParam("id") long id, final @NotNull @Valid ShowRequest request) {
|
||||
return ShowView.of(service.updateShow(schoolId, id, request.name(), parts(request), request.appearances()));
|
||||
}
|
||||
|
||||
private static List<DayPart> parts(ShowRequest request) {
|
||||
private static List<DayPart> parts(final ShowRequest request) {
|
||||
return request.dayParts().stream()
|
||||
.map(p -> DayPart.builder()
|
||||
.map((final DayPartRequest p) -> DayPart.builder()
|
||||
.date(p.date())
|
||||
.type(p.type())
|
||||
.danceGroups(List.of())
|
||||
@@ -66,15 +76,15 @@ public class ShowResource {
|
||||
@POST
|
||||
@Path("/{id}/schedule")
|
||||
@Consumes(MediaType.WILDCARD)
|
||||
public SchedulingView schedule(@PathParam("id") long id) {
|
||||
var result = scheduling.schedule(schoolId, id);
|
||||
public SchedulingView schedule(final @PathParam("id") long id) {
|
||||
final SchedulingResult result = scheduling.schedule(schoolId, id);
|
||||
return new SchedulingView(
|
||||
result.optimal(), result.optimal() ? "Optimal balance" : "Valid result; optimal balance not proven");
|
||||
}
|
||||
|
||||
@DELETE
|
||||
@Path("/{id}")
|
||||
public Response delete(@PathParam("id") long id) {
|
||||
public Response delete(final @PathParam("id") long id) {
|
||||
service.deleteShow(schoolId, id);
|
||||
return Response.noContent().build();
|
||||
}
|
||||
|
||||
@@ -4,7 +4,7 @@ import java.time.LocalDate;
|
||||
import nl.agilecc.agiledancers.model.Dancer;
|
||||
|
||||
public record DancerView(long id, String name, LocalDate birthDate) {
|
||||
public static DancerView of(Dancer dancer) {
|
||||
public static DancerView of(final Dancer dancer) {
|
||||
return new DancerView(dancer.getId(), dancer.getName(), dancer.getBirthDate());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,7 +1,13 @@
|
||||
package nl.agilecc.agiledancers.rest.apimodel;
|
||||
|
||||
import java.time.LocalDate;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
import nl.agilecc.agiledancers.model.specifications.DayPartType;
|
||||
|
||||
public record DayPartView(LocalDate date, DayPartType type, List<Long> danceGroupIds) {}
|
||||
public record DayPartView(LocalDate date, DayPartType type, List<Long> danceGroupIds) {
|
||||
public DayPartView {
|
||||
danceGroupIds = danceGroupIds == null ? null : Collections.unmodifiableList(new ArrayList<>(danceGroupIds));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,6 +3,8 @@ package nl.agilecc.agiledancers.rest.apimodel;
|
||||
import jakarta.validation.constraints.Min;
|
||||
import jakarta.validation.constraints.NotBlank;
|
||||
import jakarta.validation.constraints.NotNull;
|
||||
import java.util.Collections;
|
||||
import java.util.LinkedHashSet;
|
||||
import java.util.Set;
|
||||
import nl.agilecc.agiledancers.model.specifications.DanceGroupStyle;
|
||||
import nl.agilecc.agiledancers.model.specifications.DanceGroupType;
|
||||
@@ -12,4 +14,8 @@ public record GroupRequest(
|
||||
@NotNull DanceGroupType type,
|
||||
@NotNull @Min(0) Integer startAge,
|
||||
@NotNull @Min(0) Integer endAge,
|
||||
@NotNull Set<@NotNull DanceGroupStyle> styles) {}
|
||||
@NotNull Set<@NotNull DanceGroupStyle> styles) {
|
||||
public GroupRequest {
|
||||
styles = styles == null ? null : Collections.unmodifiableSet(new LinkedHashSet<>(styles));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,5 +1,8 @@
|
||||
package nl.agilecc.agiledancers.rest.apimodel;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collections;
|
||||
import java.util.LinkedHashSet;
|
||||
import java.util.List;
|
||||
import java.util.Set;
|
||||
import nl.agilecc.agiledancers.model.DanceGroup;
|
||||
@@ -15,7 +18,12 @@ public record GroupView(
|
||||
int endAge,
|
||||
Set<DanceGroupStyle> styles,
|
||||
List<Long> dancerIds) {
|
||||
public static GroupView of(DanceGroup group) {
|
||||
public GroupView {
|
||||
styles = styles == null ? null : Collections.unmodifiableSet(new LinkedHashSet<>(styles));
|
||||
dancerIds = dancerIds == null ? null : Collections.unmodifiableList(new ArrayList<>(dancerIds));
|
||||
}
|
||||
|
||||
public static GroupView of(final DanceGroup group) {
|
||||
return new GroupView(
|
||||
group.getId(),
|
||||
group.getCategory(),
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
package nl.agilecc.agiledancers.rest.apimodel;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
import nl.agilecc.agiledancers.model.DanceGroup;
|
||||
import nl.agilecc.agiledancers.model.DanceSchool;
|
||||
@@ -7,7 +9,13 @@ import nl.agilecc.agiledancers.model.Dancer;
|
||||
import nl.agilecc.agiledancers.model.Show;
|
||||
|
||||
public record SchoolView(long id, String name, List<Long> dancerIds, List<Long> danceGroupIds, List<Long> showIds) {
|
||||
public static SchoolView of(DanceSchool school) {
|
||||
public SchoolView {
|
||||
dancerIds = dancerIds == null ? null : Collections.unmodifiableList(new ArrayList<>(dancerIds));
|
||||
danceGroupIds = danceGroupIds == null ? null : Collections.unmodifiableList(new ArrayList<>(danceGroupIds));
|
||||
showIds = showIds == null ? null : Collections.unmodifiableList(new ArrayList<>(showIds));
|
||||
}
|
||||
|
||||
public static SchoolView of(final DanceSchool school) {
|
||||
return new SchoolView(
|
||||
school.getId(),
|
||||
school.getName(),
|
||||
|
||||
@@ -1,11 +1,21 @@
|
||||
package nl.agilecc.agiledancers.rest.apimodel;
|
||||
|
||||
import jakarta.validation.Valid;
|
||||
import jakarta.validation.constraints.*;
|
||||
import java.util.*;
|
||||
import jakarta.validation.constraints.NotBlank;
|
||||
import jakarta.validation.constraints.NotNull;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collections;
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import nl.agilecc.agiledancers.model.specifications.DanceGroupType;
|
||||
|
||||
public record ShowRequest(
|
||||
@NotBlank String name,
|
||||
@NotNull List<@NotNull @Valid DayPartRequest> dayParts,
|
||||
Map<DanceGroupType, Integer> appearances) {}
|
||||
Map<DanceGroupType, Integer> appearances) {
|
||||
public ShowRequest {
|
||||
dayParts = dayParts == null ? null : Collections.unmodifiableList(new ArrayList<>(dayParts));
|
||||
appearances = appearances == null ? null : Collections.unmodifiableMap(new LinkedHashMap<>(appearances));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,16 +1,27 @@
|
||||
package nl.agilecc.agiledancers.rest.apimodel;
|
||||
|
||||
import java.util.*;
|
||||
import nl.agilecc.agiledancers.model.*;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collections;
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import nl.agilecc.agiledancers.model.DanceGroup;
|
||||
import nl.agilecc.agiledancers.model.DayPart;
|
||||
import nl.agilecc.agiledancers.model.Show;
|
||||
import nl.agilecc.agiledancers.model.specifications.DanceGroupType;
|
||||
|
||||
public record ShowView(long id, String name, List<DayPartView> dayParts, Map<DanceGroupType, Integer> appearances) {
|
||||
public static ShowView of(Show show) {
|
||||
public ShowView {
|
||||
dayParts = dayParts == null ? null : Collections.unmodifiableList(new ArrayList<>(dayParts));
|
||||
appearances = appearances == null ? null : Collections.unmodifiableMap(new LinkedHashMap<>(appearances));
|
||||
}
|
||||
|
||||
public static ShowView of(final Show show) {
|
||||
return new ShowView(
|
||||
show.getId(),
|
||||
show.getName(),
|
||||
show.getDayParts().stream()
|
||||
.map(p -> new DayPartView(
|
||||
.map((final DayPart p) -> new DayPartView(
|
||||
p.getDate(),
|
||||
p.getType(),
|
||||
p.getDanceGroups().stream()
|
||||
|
||||
@@ -4,9 +4,9 @@ import nl.agilecc.agiledancers.model.Coach;
|
||||
import nl.agilecc.agiledancers.repository.CoachRepository;
|
||||
|
||||
public class CoachService {
|
||||
private CoachRepository coachRepository = new CoachRepository();
|
||||
private final CoachRepository coachRepository = new CoachRepository();
|
||||
|
||||
public Coach addCoach(String name, Coach coach) {
|
||||
public Coach addCoach(final String name, final Coach coach) {
|
||||
return coachRepository.add(Coach.builder().name(name).build());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,31 +1,24 @@
|
||||
package nl.agilecc.agiledancers.service;
|
||||
|
||||
import java.util.LinkedHashSet;
|
||||
import java.util.Set;
|
||||
import nl.agilecc.agiledancers.model.Coach;
|
||||
import nl.agilecc.agiledancers.model.DanceGroup;
|
||||
import nl.agilecc.agiledancers.model.Dancer;
|
||||
import nl.agilecc.agiledancers.model.specifications.DanceGroupStyle;
|
||||
import nl.agilecc.agiledancers.model.specifications.DanceGroupType;
|
||||
import nl.agilecc.agiledancers.repository.DanceGroupRepository;
|
||||
import nl.agilecc.agiledancers.service.model.GroupDetails;
|
||||
|
||||
public class DanceGroupService {
|
||||
private final DanceGroupRepository danceGroupRepository = new DanceGroupRepository();
|
||||
|
||||
public DanceGroup add(
|
||||
String category,
|
||||
DanceGroupType type,
|
||||
int startAge,
|
||||
int endAge,
|
||||
Set<Dancer> dancers,
|
||||
Set<DanceGroupStyle> styles,
|
||||
Coach coach) {
|
||||
DanceGroup danceGroup = DanceGroup.builder()
|
||||
.category(category)
|
||||
.type(type)
|
||||
.startAge(startAge)
|
||||
.endAge(endAge)
|
||||
public DanceGroup add(final GroupDetails details, final Set<Dancer> dancers, final Coach coach) {
|
||||
final DanceGroup danceGroup = DanceGroup.builder()
|
||||
.category(details.category())
|
||||
.type(details.type())
|
||||
.startAge(details.startAge())
|
||||
.endAge(details.endAge())
|
||||
.dancers(dancers)
|
||||
.styles(styles)
|
||||
.styles(details.styles() == null ? null : new LinkedHashSet<>(details.styles()))
|
||||
.coach(coach)
|
||||
.build();
|
||||
|
||||
|
||||
@@ -1,29 +1,33 @@
|
||||
package nl.agilecc.agiledancers.service;
|
||||
|
||||
import nl.agilecc.agiledancers.model.*;
|
||||
import nl.agilecc.agiledancers.model.Coach;
|
||||
import nl.agilecc.agiledancers.model.DanceGroup;
|
||||
import nl.agilecc.agiledancers.model.DanceSchool;
|
||||
import nl.agilecc.agiledancers.model.Dancer;
|
||||
import nl.agilecc.agiledancers.model.Show;
|
||||
import nl.agilecc.agiledancers.repository.DanceSchoolRepository;
|
||||
|
||||
public class DanceSchoolService {
|
||||
private final DanceSchoolRepository danceSchoolRepository = new DanceSchoolRepository();
|
||||
|
||||
public DanceSchool add(String name) {
|
||||
DanceSchool danceSchool = DanceSchool.builder().name(name).build();
|
||||
public DanceSchool add(final String name) {
|
||||
final DanceSchool danceSchool = DanceSchool.builder().name(name).build();
|
||||
return danceSchoolRepository.add(danceSchool);
|
||||
}
|
||||
|
||||
public void addDancer(Long danceSchoolId, Dancer dancer) {
|
||||
public void addDancer(final Long danceSchoolId, final Dancer dancer) {
|
||||
danceSchoolRepository.findById(danceSchoolId).addDancer(dancer);
|
||||
}
|
||||
|
||||
public void addDanceGroup(Long danceSchoolId, DanceGroup danceGroup) {
|
||||
public void addDanceGroup(final Long danceSchoolId, final DanceGroup danceGroup) {
|
||||
danceSchoolRepository.findById(danceSchoolId).addDanceGroup(danceGroup);
|
||||
}
|
||||
|
||||
public void addCoach(Long danceSchoolId, Coach coach) {
|
||||
public void addCoach(final Long danceSchoolId, final Coach coach) {
|
||||
danceSchoolRepository.findById(danceSchoolId).addCoach(coach);
|
||||
}
|
||||
|
||||
public void addShow(Long danceSchoolId, Show show) {
|
||||
public void addShow(final Long danceSchoolId, final Show show) {
|
||||
danceSchoolRepository.findById(danceSchoolId).addShow(show);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -7,8 +7,8 @@ import nl.agilecc.agiledancers.repository.DancerRepository;
|
||||
public class DancerService {
|
||||
private final DancerRepository dancerRepository = new DancerRepository();
|
||||
|
||||
public Dancer addDancer(String name, LocalDate birthDate) {
|
||||
Dancer dancer = Dancer.builder().name(name).birthDate(birthDate).build();
|
||||
public Dancer addDancer(final String name, final LocalDate birthDate) {
|
||||
final Dancer dancer = Dancer.builder().name(name).birthDate(birthDate).build();
|
||||
return dancerRepository.add(dancer);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,7 +4,7 @@ public class ManagementException extends RuntimeException {
|
||||
|
||||
private final ManagementExceptionKind kind;
|
||||
|
||||
public ManagementException(ManagementExceptionKind kind, String message) {
|
||||
public ManagementException(final ManagementExceptionKind kind, final String message) {
|
||||
super(message);
|
||||
this.kind = kind;
|
||||
}
|
||||
|
||||
@@ -2,26 +2,35 @@ package nl.agilecc.agiledancers.service;
|
||||
|
||||
import jakarta.inject.Inject;
|
||||
import jakarta.inject.Singleton;
|
||||
import nl.agilecc.agiledancers.model.scheduling.SchedulingInput;
|
||||
import nl.agilecc.agiledancers.model.scheduling.SchedulingResult;
|
||||
import nl.agilecc.agiledancers.service.port.SchedulingEngine;
|
||||
|
||||
@Singleton
|
||||
public class SchedulingService {
|
||||
@Inject
|
||||
SchoolManagementService schools;
|
||||
private SchoolManagementService schools;
|
||||
|
||||
@Inject
|
||||
SchedulingEngine engine;
|
||||
private SchedulingEngine engine;
|
||||
|
||||
public SchedulingResult schedule(long schoolId, long showId) {
|
||||
var input = schools.beginScheduling(schoolId, showId);
|
||||
public SchedulingService() {}
|
||||
|
||||
/** Supplies collaborators directly for callers outside the CDI lifecycle. */
|
||||
public SchedulingService(final SchoolManagementService schools, final SchedulingEngine engine) {
|
||||
this.schools = schools;
|
||||
this.engine = engine;
|
||||
}
|
||||
|
||||
public SchedulingResult schedule(final long schoolId, final long showId) {
|
||||
final SchedulingInput input = schools.beginScheduling(schoolId, showId);
|
||||
try {
|
||||
var result = engine.solve(input);
|
||||
final SchedulingResult result = engine.solve(input);
|
||||
schools.finishScheduling(schoolId, input, result);
|
||||
return result;
|
||||
} catch (ManagementException e) {
|
||||
} catch (final ManagementException e) {
|
||||
throw e;
|
||||
} catch (RuntimeException | LinkageError e) {
|
||||
} catch (final RuntimeException | LinkageError e) {
|
||||
throw new ManagementException(
|
||||
ManagementExceptionKind.INVALID_INPUT,
|
||||
"Solver failed. Existing assignments were preserved. Please run scheduling again.");
|
||||
|
||||
@@ -1,17 +1,34 @@
|
||||
package nl.agilecc.agiledancers.service;
|
||||
|
||||
import static nl.agilecc.agiledancers.service.ManagementExceptionKind.*;
|
||||
|
||||
import jakarta.inject.Singleton;
|
||||
import java.time.LocalDate;
|
||||
import java.util.*;
|
||||
import nl.agilecc.agiledancers.model.*;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collection;
|
||||
import java.util.HashMap;
|
||||
import java.util.HashSet;
|
||||
import java.util.IdentityHashMap;
|
||||
import java.util.LinkedHashSet;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Objects;
|
||||
import java.util.Set;
|
||||
import nl.agilecc.agiledancers.model.AbstractEntity;
|
||||
import nl.agilecc.agiledancers.model.DanceGroup;
|
||||
import nl.agilecc.agiledancers.model.DanceSchool;
|
||||
import nl.agilecc.agiledancers.model.Dancer;
|
||||
import nl.agilecc.agiledancers.model.DayPart;
|
||||
import nl.agilecc.agiledancers.model.Show;
|
||||
import nl.agilecc.agiledancers.model.scheduling.GroupRequirement;
|
||||
import nl.agilecc.agiledancers.model.scheduling.SchedulingInput;
|
||||
import nl.agilecc.agiledancers.model.scheduling.SchedulingResult;
|
||||
import nl.agilecc.agiledancers.model.specifications.*;
|
||||
import nl.agilecc.agiledancers.repository.*;
|
||||
import nl.agilecc.agiledancers.service.model.*;
|
||||
import nl.agilecc.agiledancers.model.specifications.DanceGroupType;
|
||||
import nl.agilecc.agiledancers.repository.DanceGroupRepository;
|
||||
import nl.agilecc.agiledancers.repository.DanceSchoolRepository;
|
||||
import nl.agilecc.agiledancers.repository.DancerRepository;
|
||||
import nl.agilecc.agiledancers.repository.ShowRepository;
|
||||
import nl.agilecc.agiledancers.service.model.GroupDetails;
|
||||
import nl.agilecc.agiledancers.service.model.ImportPreview;
|
||||
import nl.agilecc.agiledancers.service.model.SchoolSnapshot;
|
||||
|
||||
/** Owns the in-memory school aggregates; all writes go through this shared service. */
|
||||
@Singleton
|
||||
@@ -24,18 +41,23 @@ public class SchoolManagementService {
|
||||
private final Map<Long, Long> revisions = new HashMap<>();
|
||||
private final Set<Long> running = new HashSet<>();
|
||||
|
||||
private void touch(Show show) {
|
||||
private void touch(final Show show) {
|
||||
revisions.merge(show.getId(), 1L, Long::sum);
|
||||
}
|
||||
|
||||
public synchronized SchedulingInput beginScheduling(long schoolId, long showId) {
|
||||
Show show = show(schoolId, showId);
|
||||
if (running.contains(showId))
|
||||
throw new ManagementException(CONFLICT, "Scheduling is already running for this show.");
|
||||
List<DanceGroup> current = groups(schoolId);
|
||||
if (current.isEmpty() || show.getDayParts().isEmpty())
|
||||
/** Reserves this show's run slot and snapshots its inputs; always release it with endScheduling. */
|
||||
public synchronized SchedulingInput beginScheduling(final long schoolId, final long showId) {
|
||||
final Show show = show(schoolId, showId);
|
||||
if (running.contains(showId)) {
|
||||
throw new ManagementException(
|
||||
INVALID_INPUT, "Scheduling requires at least one school group and one daypart.");
|
||||
ManagementExceptionKind.CONFLICT, "Scheduling is already running for this show.");
|
||||
}
|
||||
final List<DanceGroup> current = groups(schoolId);
|
||||
if (current.isEmpty() || show.getDayParts().isEmpty()) {
|
||||
throw new ManagementException(
|
||||
ManagementExceptionKind.INVALID_INPUT,
|
||||
"Scheduling requires at least one school group and one daypart.");
|
||||
}
|
||||
ShowConfiguration.validate(show.getDayParts(), show.getAppearances());
|
||||
running.add(showId);
|
||||
return new SchedulingInput(
|
||||
@@ -43,42 +65,29 @@ public class SchoolManagementService {
|
||||
revisions.getOrDefault(showId, 0L),
|
||||
show.getDayParts().size(),
|
||||
current.stream()
|
||||
.map(g -> new GroupRequirement(
|
||||
.map((final DanceGroup g) -> new GroupRequirement(
|
||||
g.getId(), show.getAppearances().get(g.getType())))
|
||||
.toList());
|
||||
}
|
||||
|
||||
public synchronized void finishScheduling(long schoolId, SchedulingInput input, SchedulingResult result) {
|
||||
Show show;
|
||||
/** Validates the complete result and input revision before replacing any saved assignments. */
|
||||
public synchronized void finishScheduling(
|
||||
final long schoolId, final SchedulingInput input, final SchedulingResult result) {
|
||||
final Show show;
|
||||
try {
|
||||
show = show(schoolId, input.showId());
|
||||
} catch (ManagementException e) {
|
||||
throw new ManagementException(CONFLICT, "Scheduling inputs changed. Run scheduling again.");
|
||||
}
|
||||
if (revisions.getOrDefault(input.showId(), 0L) != input.revision())
|
||||
throw new ManagementException(CONFLICT, "Scheduling inputs changed. Run scheduling again.");
|
||||
// Validate the complete solver output before mutating any assignments.
|
||||
if (result == null
|
||||
|| result.assignments() == null
|
||||
|| result.assignments().size() != input.dayParts())
|
||||
} catch (final ManagementException e) {
|
||||
throw new ManagementException(
|
||||
INVALID_INPUT, "Solver returned an invalid result. Existing assignments were preserved.");
|
||||
Map<Long, DanceGroup> current = new HashMap<>();
|
||||
groups(schoolId).forEach(g -> current.put(g.getId(), g));
|
||||
for (var part : result.assignments()) {
|
||||
if (part == null
|
||||
|| new HashSet<>(part).size() != part.size()
|
||||
|| !current.keySet().containsAll(part))
|
||||
throw new ManagementException(INVALID_INPUT, "Solver returned invalid group assignments.");
|
||||
ManagementExceptionKind.CONFLICT, "Scheduling inputs changed. Run scheduling again.");
|
||||
}
|
||||
for (GroupRequirement group : input.groups()) {
|
||||
long id = group.groupId();
|
||||
if (result.assignments().stream().filter(p -> p.contains(id)).count() != group.appearances())
|
||||
throw new ManagementException(INVALID_INPUT, "Solver did not satisfy every required appearance.");
|
||||
if (revisions.getOrDefault(input.showId(), 0L) != input.revision()) {
|
||||
throw new ManagementException(
|
||||
ManagementExceptionKind.CONFLICT, "Scheduling inputs changed. Run scheduling again.");
|
||||
}
|
||||
List<DayPart> replacement = new ArrayList<>();
|
||||
final Map<Long, DanceGroup> current = validateAssignments(schoolId, input, result);
|
||||
final List<DayPart> replacement = new ArrayList<>();
|
||||
for (int p = 0; p < input.dayParts(); p++) {
|
||||
DayPart old = show.getDayParts().get(p);
|
||||
final DayPart old = show.getDayParts().get(p);
|
||||
replacement.add(DayPart.builder()
|
||||
.date(old.getDate())
|
||||
.type(old.getType())
|
||||
@@ -90,7 +99,43 @@ public class SchoolManagementService {
|
||||
show.setDayParts(List.copyOf(replacement));
|
||||
}
|
||||
|
||||
public synchronized void endScheduling(long showId) {
|
||||
private Map<Long, DanceGroup> validateAssignments(
|
||||
final long schoolId, final SchedulingInput input, final SchedulingResult result) {
|
||||
// Validate the complete solver output before mutating any assignments.
|
||||
if (result == null
|
||||
|| result.assignments() == null
|
||||
|| result.assignments().size() != input.dayParts()) {
|
||||
throw new ManagementException(
|
||||
ManagementExceptionKind.INVALID_INPUT,
|
||||
"Solver returned an invalid result. Existing assignments were preserved.");
|
||||
}
|
||||
final Map<Long, DanceGroup> current = new HashMap<>();
|
||||
groups(schoolId).forEach((final DanceGroup g) -> current.put(g.getId(), g));
|
||||
validateParts(result.assignments(), current.keySet());
|
||||
for (final GroupRequirement group : input.groups()) {
|
||||
final long id = group.groupId();
|
||||
if (result.assignments().stream()
|
||||
.filter((final List<Long> p) -> p.contains(id))
|
||||
.count()
|
||||
!= group.appearances()) {
|
||||
throw new ManagementException(
|
||||
ManagementExceptionKind.INVALID_INPUT, "Solver did not satisfy every required appearance.");
|
||||
}
|
||||
}
|
||||
return current;
|
||||
}
|
||||
|
||||
private static void validateParts(final List<List<Long>> assignments, final Set<Long> groupIds) {
|
||||
for (final List<Long> part : assignments) {
|
||||
if (part == null || new HashSet<>(part).size() != part.size() || !groupIds.containsAll(part)) {
|
||||
throw new ManagementException(
|
||||
ManagementExceptionKind.INVALID_INPUT, "Solver returned invalid group assignments.");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/** Releases the run slot after success or failure, without changing saved assignments. */
|
||||
public synchronized void endScheduling(final long showId) {
|
||||
running.remove(showId);
|
||||
}
|
||||
|
||||
@@ -98,139 +143,134 @@ public class SchoolManagementService {
|
||||
return schools.findAll();
|
||||
}
|
||||
|
||||
public synchronized DanceSchool school(long schoolId) {
|
||||
public synchronized DanceSchool school(final long schoolId) {
|
||||
return required(schools.findById(schoolId), "Dance school", schoolId);
|
||||
}
|
||||
|
||||
public synchronized DanceSchool createSchool(String name) {
|
||||
public synchronized DanceSchool createSchool(final String name) {
|
||||
return schools.add(DanceSchool.builder().name(name).build());
|
||||
}
|
||||
|
||||
public synchronized DanceSchool updateSchool(long schoolId, String name) {
|
||||
DanceSchool school = school(schoolId);
|
||||
public synchronized DanceSchool updateSchool(final long schoolId, final String name) {
|
||||
final DanceSchool school = school(schoolId);
|
||||
school.setName(name);
|
||||
return school;
|
||||
}
|
||||
|
||||
public synchronized void deleteSchool(long schoolId) {
|
||||
DanceSchool school = school(schoolId);
|
||||
school.getDanceGroups().forEach(group -> groups.delete(group.getId()));
|
||||
school.getDancers().forEach(dancer -> dancers.delete(dancer.getId()));
|
||||
school.getShows().forEach(show -> shows.delete(show.getId()));
|
||||
public synchronized void deleteSchool(final long schoolId) {
|
||||
final DanceSchool school = school(schoolId);
|
||||
school.getDanceGroups().forEach((final DanceGroup group) -> groups.delete(group.getId()));
|
||||
school.getDancers().forEach((final Dancer dancer) -> dancers.delete(dancer.getId()));
|
||||
school.getShows().forEach((final Show show) -> shows.delete(show.getId()));
|
||||
schools.delete(schoolId);
|
||||
}
|
||||
|
||||
public synchronized List<Dancer> dancers(long schoolId) {
|
||||
public synchronized List<Dancer> dancers(final long schoolId) {
|
||||
return List.copyOf(school(schoolId).getDancers());
|
||||
}
|
||||
|
||||
public synchronized Dancer dancer(long schoolId, long dancerId) {
|
||||
public synchronized Dancer dancer(final long schoolId, final long dancerId) {
|
||||
return member(school(schoolId).getDancers(), dancerId, "Dancer");
|
||||
}
|
||||
|
||||
public synchronized Dancer createDancer(long schoolId, String name, LocalDate birthDate) {
|
||||
DanceSchool school = school(schoolId);
|
||||
Dancer dancer =
|
||||
public synchronized Dancer createDancer(final long schoolId, final String name, final LocalDate birthDate) {
|
||||
final DanceSchool school = school(schoolId);
|
||||
final Dancer dancer =
|
||||
dancers.add(Dancer.builder().name(name).birthDate(birthDate).build());
|
||||
school.addDancer(dancer);
|
||||
return dancer;
|
||||
}
|
||||
|
||||
public synchronized Dancer updateDancer(long schoolId, long dancerId, String name, LocalDate birthDate) {
|
||||
Dancer dancer = dancer(schoolId, dancerId);
|
||||
public synchronized Dancer updateDancer(
|
||||
final long schoolId, final long dancerId, final String name, final LocalDate birthDate) {
|
||||
final Dancer dancer = dancer(schoolId, dancerId);
|
||||
dancer.setName(name);
|
||||
dancer.setBirthDate(birthDate);
|
||||
return dancer;
|
||||
}
|
||||
|
||||
public synchronized void deleteDancer(long schoolId, long dancerId) {
|
||||
DanceSchool school = school(schoolId);
|
||||
Dancer dancer = dancer(schoolId, dancerId);
|
||||
school.getDanceGroups().forEach(group -> group.removeDancer(dancer));
|
||||
public synchronized void deleteDancer(final long schoolId, final long dancerId) {
|
||||
final DanceSchool school = school(schoolId);
|
||||
final Dancer dancer = dancer(schoolId, dancerId);
|
||||
school.getDanceGroups().forEach((final DanceGroup group) -> group.removeDancer(dancer));
|
||||
school.removeDancer(dancer);
|
||||
dancers.delete(dancerId);
|
||||
}
|
||||
|
||||
public synchronized List<DanceGroup> groups(long schoolId) {
|
||||
public synchronized List<DanceGroup> groups(final long schoolId) {
|
||||
return List.copyOf(school(schoolId).getDanceGroups());
|
||||
}
|
||||
|
||||
public synchronized DanceGroup group(long schoolId, long groupId) {
|
||||
public synchronized DanceGroup group(final long schoolId, final long groupId) {
|
||||
return member(school(schoolId).getDanceGroups(), groupId, "Dance group");
|
||||
}
|
||||
|
||||
public synchronized DanceGroup createGroup(
|
||||
long schoolId,
|
||||
String category,
|
||||
DanceGroupType type,
|
||||
int startAge,
|
||||
int endAge,
|
||||
Set<DanceGroupStyle> styles) {
|
||||
DanceSchool school = school(schoolId);
|
||||
validateAgeRange(startAge, endAge);
|
||||
DanceGroup group = groups.add(DanceGroup.builder()
|
||||
.category(category)
|
||||
.type(type)
|
||||
.startAge(startAge)
|
||||
.endAge(endAge)
|
||||
.styles(new LinkedHashSet<>(styles))
|
||||
public synchronized DanceGroup createGroup(final long schoolId, final GroupDetails details) {
|
||||
final DanceSchool school = school(schoolId);
|
||||
validateAgeRange(details.startAge(), details.endAge());
|
||||
final DanceGroup group = groups.add(DanceGroup.builder()
|
||||
.category(details.category())
|
||||
.type(details.type())
|
||||
.startAge(details.startAge())
|
||||
.endAge(details.endAge())
|
||||
.styles(new LinkedHashSet<>(details.styles()))
|
||||
.build());
|
||||
school.addDanceGroup(group);
|
||||
school.getShows().forEach(this::touch);
|
||||
return group;
|
||||
}
|
||||
|
||||
public synchronized DanceGroup updateGroup(
|
||||
long schoolId,
|
||||
long groupId,
|
||||
String category,
|
||||
DanceGroupType type,
|
||||
int startAge,
|
||||
int endAge,
|
||||
Set<DanceGroupStyle> styles) {
|
||||
DanceGroup group = group(schoolId, groupId);
|
||||
validateAgeRange(startAge, endAge);
|
||||
group.setCategory(category);
|
||||
if (group.getType() != type) school(schoolId).getShows().forEach(this::touch);
|
||||
group.setType(type);
|
||||
group.setStartAge(startAge);
|
||||
group.setEndAge(endAge);
|
||||
group.replaceStyles(styles);
|
||||
public synchronized DanceGroup updateGroup(final long schoolId, final long groupId, final GroupDetails details) {
|
||||
final DanceGroup group = group(schoolId, groupId);
|
||||
validateAgeRange(details.startAge(), details.endAge());
|
||||
group.setCategory(details.category());
|
||||
if (group.getType() != details.type()) {
|
||||
school(schoolId).getShows().forEach(this::touch);
|
||||
}
|
||||
group.setType(details.type());
|
||||
group.setStartAge(details.startAge());
|
||||
group.setEndAge(details.endAge());
|
||||
group.replaceStyles(details.styles());
|
||||
return group;
|
||||
}
|
||||
|
||||
public synchronized void deleteGroup(long schoolId, long groupId) {
|
||||
DanceGroup group = group(schoolId, groupId);
|
||||
school(schoolId).getShows().forEach(show -> show.getDayParts().forEach(part -> part.removeDanceGroup(group)));
|
||||
public synchronized void deleteGroup(final long schoolId, final long groupId) {
|
||||
final DanceGroup group = group(schoolId, groupId);
|
||||
school(schoolId)
|
||||
.getShows()
|
||||
.forEach((final Show show) ->
|
||||
show.getDayParts().forEach((final DayPart part) -> part.removeDanceGroup(group)));
|
||||
school(schoolId).getShows().forEach(this::touch);
|
||||
school(schoolId).removeDanceGroup(group);
|
||||
groups.delete(groupId);
|
||||
}
|
||||
|
||||
public synchronized void addGroupDancer(long schoolId, long groupId, long dancerId) {
|
||||
DanceGroup group = group(schoolId, groupId);
|
||||
public synchronized void addGroupDancer(final long schoolId, final long groupId, final long dancerId) {
|
||||
final DanceGroup group = group(schoolId, groupId);
|
||||
group.addDancer(dancer(schoolId, dancerId));
|
||||
}
|
||||
|
||||
public synchronized void removeGroupDancer(long schoolId, long groupId, long dancerId) {
|
||||
DanceGroup group = group(schoolId, groupId);
|
||||
public synchronized void removeGroupDancer(final long schoolId, final long groupId, final long dancerId) {
|
||||
final DanceGroup group = group(schoolId, groupId);
|
||||
group.removeDancer(dancer(schoolId, dancerId));
|
||||
}
|
||||
|
||||
public synchronized List<Show> shows(long schoolId) {
|
||||
public synchronized List<Show> shows(final long schoolId) {
|
||||
return List.copyOf(school(schoolId).getShows());
|
||||
}
|
||||
|
||||
public synchronized Show show(long schoolId, long showId) {
|
||||
public synchronized Show show(final long schoolId, final long showId) {
|
||||
return member(school(schoolId).getShows(), showId, "Show");
|
||||
}
|
||||
|
||||
public synchronized Show createShow(
|
||||
long schoolId, String name, List<DayPart> parts, Map<DanceGroupType, Integer> counts) {
|
||||
DanceSchool school = school(schoolId);
|
||||
if (counts == null) counts = ShowConfiguration.defaults(parts.size());
|
||||
final long schoolId, final String name, final List<DayPart> parts, Map<DanceGroupType, Integer> counts) {
|
||||
final DanceSchool school = school(schoolId);
|
||||
if (counts == null) {
|
||||
counts = ShowConfiguration.defaults(parts.size());
|
||||
}
|
||||
ShowConfiguration.validate(parts, counts);
|
||||
Show show = shows.add(Show.builder()
|
||||
final Show show = shows.add(Show.builder()
|
||||
.name(name)
|
||||
.dayParts(List.copyOf(parts))
|
||||
.appearances(Map.copyOf(counts))
|
||||
@@ -240,19 +280,25 @@ public class SchoolManagementService {
|
||||
}
|
||||
|
||||
public synchronized Show updateShow(
|
||||
long schoolId, long showId, String name, List<DayPart> parts, Map<DanceGroupType, Integer> counts) {
|
||||
Show show = show(schoolId, showId);
|
||||
if (counts == null)
|
||||
final long schoolId,
|
||||
final long showId,
|
||||
final String name,
|
||||
final List<DayPart> parts,
|
||||
Map<DanceGroupType, Integer> counts) {
|
||||
final Show show = show(schoolId, showId);
|
||||
if (counts == null) {
|
||||
counts = show.getAppearances().isEmpty() ? ShowConfiguration.defaults(parts.size()) : show.getAppearances();
|
||||
}
|
||||
ShowConfiguration.validate(parts, counts);
|
||||
boolean changed =
|
||||
final boolean changed =
|
||||
!counts.equals(show.getAppearances()) || !partKeys(parts).equals(partKeys(show.getDayParts()));
|
||||
List<DayPart> retained = parts.stream()
|
||||
.map(p -> DayPart.builder()
|
||||
final List<DayPart> retained = parts.stream()
|
||||
.map((final DayPart p) -> DayPart.builder()
|
||||
.date(p.getDate())
|
||||
.type(p.getType())
|
||||
.danceGroups(show.getDayParts().stream()
|
||||
.filter(old -> old.getDate().equals(p.getDate()) && old.getType() == p.getType())
|
||||
.filter((final DayPart old) ->
|
||||
old.getDate().equals(p.getDate()) && old.getType() == p.getType())
|
||||
.findFirst()
|
||||
.map(DayPart::getDanceGroups)
|
||||
.orElse(List.of()))
|
||||
@@ -261,28 +307,32 @@ public class SchoolManagementService {
|
||||
show.setName(name);
|
||||
show.setDayParts(retained);
|
||||
show.setAppearances(Map.copyOf(counts));
|
||||
if (changed) touch(show);
|
||||
if (changed) {
|
||||
touch(show);
|
||||
}
|
||||
return show;
|
||||
}
|
||||
|
||||
private static List<String> partKeys(List<DayPart> parts) {
|
||||
return parts.stream().map(p -> p.getDate() + ":" + p.getType()).toList();
|
||||
private static List<String> partKeys(final List<DayPart> parts) {
|
||||
return parts.stream()
|
||||
.map((final DayPart p) -> p.getDate() + ":" + p.getType())
|
||||
.toList();
|
||||
}
|
||||
|
||||
public synchronized void deleteShow(long schoolId, long showId) {
|
||||
Show show = show(schoolId, showId);
|
||||
public synchronized void deleteShow(final long schoolId, final long showId) {
|
||||
final Show show = show(schoolId, showId);
|
||||
school(schoolId).removeShow(show);
|
||||
shows.delete(showId);
|
||||
}
|
||||
|
||||
public synchronized SchoolSnapshot exportSchool(long id) {
|
||||
public synchronized SchoolSnapshot exportSchool(final long id) {
|
||||
return SchoolSnapshot.of(school(id));
|
||||
}
|
||||
|
||||
public synchronized ImportPreview preview(SchoolSnapshot snapshot) {
|
||||
public synchronized ImportPreview preview(final SchoolSnapshot snapshot) {
|
||||
snapshot.restore();
|
||||
DanceSchool existing = schools.findAll().stream()
|
||||
.filter(s -> s.getKey().equals(snapshot.key()))
|
||||
final DanceSchool existing = schools.findAll().stream()
|
||||
.filter((final DanceSchool s) -> s.getKey().equals(snapshot.key()))
|
||||
.findFirst()
|
||||
.orElse(null);
|
||||
return new ImportPreview(
|
||||
@@ -294,58 +344,30 @@ public class SchoolManagementService {
|
||||
existing == null ? null : existing.getName());
|
||||
}
|
||||
|
||||
public synchronized DanceSchool importSchool(SchoolSnapshot snapshot, Long replaceId) {
|
||||
DanceSchool staged = snapshot.restore();
|
||||
ImportPreview preview = preview(snapshot);
|
||||
/** Validates and stages the complete graph before replacing repositories; file IDs become runtime IDs. */
|
||||
public synchronized DanceSchool importSchool(final SchoolSnapshot snapshot, final Long replaceId) {
|
||||
final DanceSchool staged = snapshot.restore();
|
||||
final ImportPreview preview = preview(snapshot);
|
||||
if (!Objects.equals(preview.existingId(), replaceId)) {
|
||||
throw new ManagementException(INVALID_INPUT, "De bestaande school is gewijzigd. Bekijk de import opnieuw.");
|
||||
throw new ManagementException(
|
||||
ManagementExceptionKind.INVALID_INPUT,
|
||||
"De bestaande school is gewijzigd. Bekijk de import opnieuw.");
|
||||
}
|
||||
// All validation and construction finish before changing repository state.
|
||||
// Rebuild sets after assigning runtime IDs: entity hash codes include the ID.
|
||||
List<Dancer> importedDancers = new ArrayList<>(staged.getDancers());
|
||||
List<DanceGroup> importedGroups = new ArrayList<>(staged.getDanceGroups());
|
||||
List<Show> importedShows = new ArrayList<>(staged.getShows());
|
||||
Map<DanceGroup, List<Dancer>> memberships = new IdentityHashMap<>();
|
||||
importedGroups.forEach(g -> memberships.put(g, new ArrayList<>(g.getDancers())));
|
||||
importedDancers.forEach(d -> d.setId(dancers.nextId()));
|
||||
importedGroups.forEach(g -> {
|
||||
final List<Dancer> importedDancers = new ArrayList<>(staged.getDancers());
|
||||
final List<DanceGroup> importedGroups = new ArrayList<>(staged.getDanceGroups());
|
||||
final List<Show> importedShows = new ArrayList<>(staged.getShows());
|
||||
final Map<DanceGroup, List<Dancer>> memberships = new IdentityHashMap<>();
|
||||
importedGroups.forEach((final DanceGroup g) -> memberships.put(g, new ArrayList<>(g.getDancers())));
|
||||
importedDancers.forEach((final Dancer d) -> d.setId(dancers.nextId()));
|
||||
importedGroups.forEach((final DanceGroup g) -> {
|
||||
g.setId(groups.nextId());
|
||||
});
|
||||
List<DanceGroup> rebuiltGroups = new ArrayList<>();
|
||||
Map<DanceGroup, DanceGroup> replacements = new IdentityHashMap<>();
|
||||
for (DanceGroup g : importedGroups) {
|
||||
DanceGroup rebuilt = DanceGroup.builder()
|
||||
.category(g.getCategory())
|
||||
.type(g.getType())
|
||||
.startAge(g.getStartAge())
|
||||
.endAge(g.getEndAge())
|
||||
.styles(new LinkedHashSet<>(g.getStyles()))
|
||||
.coach(g.getCoach())
|
||||
.dancers(new LinkedHashSet<>(memberships.get(g)))
|
||||
.build();
|
||||
rebuilt.setId(g.getId());
|
||||
rebuiltGroups.add(rebuilt);
|
||||
replacements.put(g, rebuilt);
|
||||
}
|
||||
List<Show> rebuiltShows = new ArrayList<>();
|
||||
for (Show show : importedShows) {
|
||||
Show rebuilt = Show.builder()
|
||||
.name(show.getName())
|
||||
.appearances(show.getAppearances())
|
||||
.dayParts(show.getDayParts().stream()
|
||||
.map(p -> DayPart.builder()
|
||||
.date(p.getDate())
|
||||
.type(p.getType())
|
||||
.danceGroups(p.getDanceGroups().stream()
|
||||
.map(replacements::get)
|
||||
.toList())
|
||||
.build())
|
||||
.toList())
|
||||
.build();
|
||||
rebuilt.setId(shows.nextId());
|
||||
rebuiltShows.add(rebuilt);
|
||||
}
|
||||
DanceSchool imported = DanceSchool.builder()
|
||||
final Map<DanceGroup, DanceGroup> replacements = new IdentityHashMap<>();
|
||||
final List<DanceGroup> rebuiltGroups = rebuildGroups(importedGroups, memberships, replacements);
|
||||
final List<Show> rebuiltShows = rebuildShows(importedShows, replacements);
|
||||
final DanceSchool imported = DanceSchool.builder()
|
||||
.key(staged.getKey())
|
||||
.name(staged.getName())
|
||||
.dancers(new LinkedHashSet<>(importedDancers))
|
||||
@@ -363,22 +385,67 @@ public class SchoolManagementService {
|
||||
return schools.add(imported);
|
||||
}
|
||||
|
||||
private static void validateAgeRange(int startAge, int endAge) {
|
||||
private static List<DanceGroup> rebuildGroups(
|
||||
final List<DanceGroup> importedGroups,
|
||||
final Map<DanceGroup, List<Dancer>> memberships,
|
||||
final Map<DanceGroup, DanceGroup> replacements) {
|
||||
final List<DanceGroup> rebuiltGroups = new ArrayList<>();
|
||||
for (final DanceGroup g : importedGroups) {
|
||||
final DanceGroup rebuilt = DanceGroup.builder()
|
||||
.category(g.getCategory())
|
||||
.type(g.getType())
|
||||
.startAge(g.getStartAge())
|
||||
.endAge(g.getEndAge())
|
||||
.styles(new LinkedHashSet<>(g.getStyles()))
|
||||
.coach(g.getCoach())
|
||||
.dancers(new LinkedHashSet<>(memberships.get(g)))
|
||||
.build();
|
||||
rebuilt.setId(g.getId());
|
||||
rebuiltGroups.add(rebuilt);
|
||||
replacements.put(g, rebuilt);
|
||||
}
|
||||
return rebuiltGroups;
|
||||
}
|
||||
|
||||
private List<Show> rebuildShows(final List<Show> importedShows, final Map<DanceGroup, DanceGroup> replacements) {
|
||||
final List<Show> rebuiltShows = new ArrayList<>();
|
||||
for (final Show show : importedShows) {
|
||||
final Show rebuilt = Show.builder()
|
||||
.name(show.getName())
|
||||
.appearances(show.getAppearances())
|
||||
.dayParts(show.getDayParts().stream()
|
||||
.map((final DayPart p) -> DayPart.builder()
|
||||
.date(p.getDate())
|
||||
.type(p.getType())
|
||||
.danceGroups(p.getDanceGroups().stream()
|
||||
.map(replacements::get)
|
||||
.toList())
|
||||
.build())
|
||||
.toList())
|
||||
.build();
|
||||
rebuilt.setId(shows.nextId());
|
||||
rebuiltShows.add(rebuilt);
|
||||
}
|
||||
return rebuiltShows;
|
||||
}
|
||||
|
||||
private static void validateAgeRange(final int startAge, final int endAge) {
|
||||
if (startAge > endAge) {
|
||||
throw new ManagementException(INVALID_INPUT, "startAge must not exceed endAge");
|
||||
throw new ManagementException(ManagementExceptionKind.INVALID_INPUT, "startAge must not exceed endAge");
|
||||
}
|
||||
}
|
||||
|
||||
private static <T extends AbstractEntity> T member(Collection<T> entities, long id, String type) {
|
||||
private static <T extends AbstractEntity> T member(final Collection<T> entities, final long id, final String type) {
|
||||
return entities.stream()
|
||||
.filter(entity -> entity.getId() == id)
|
||||
.filter((final T entity) -> entity.getId() == id)
|
||||
.findFirst()
|
||||
.orElseThrow(() -> new ManagementException(NOT_FOUND, type + " " + id + " not found in this school"));
|
||||
.orElseThrow(() -> new ManagementException(
|
||||
ManagementExceptionKind.NOT_FOUND, type + " " + id + " not found in this school"));
|
||||
}
|
||||
|
||||
private static <T> T required(T value, String type, long id) {
|
||||
private static <T> T required(final T value, final String type, final long id) {
|
||||
if (value == null) {
|
||||
throw new ManagementException(NOT_FOUND, type + " " + id + " not found");
|
||||
throw new ManagementException(ManagementExceptionKind.NOT_FOUND, type + " " + id + " not found");
|
||||
}
|
||||
return value;
|
||||
}
|
||||
|
||||
@@ -1,16 +1,19 @@
|
||||
package nl.agilecc.agiledancers.service;
|
||||
|
||||
import java.util.*;
|
||||
import java.util.Arrays;
|
||||
import java.util.EnumMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import nl.agilecc.agiledancers.model.DayPart;
|
||||
import nl.agilecc.agiledancers.model.specifications.DanceGroupType;
|
||||
|
||||
public final class ShowConfiguration {
|
||||
private ShowConfiguration() {}
|
||||
|
||||
public static Map<DanceGroupType, Integer> defaults(int n) {
|
||||
Map<DanceGroupType, Integer> counts = new EnumMap<>(DanceGroupType.class);
|
||||
if (n > 0)
|
||||
for (var type : DanceGroupType.values())
|
||||
public static Map<DanceGroupType, Integer> defaults(final int n) {
|
||||
final Map<DanceGroupType, Integer> counts = new EnumMap<>(DanceGroupType.class);
|
||||
if (n > 0) {
|
||||
for (final DanceGroupType type : DanceGroupType.values()) {
|
||||
counts.put(
|
||||
type,
|
||||
switch (type) {
|
||||
@@ -18,28 +21,41 @@ public final class ShowConfiguration {
|
||||
case SHOW_CASE, PRE_EDUCATION -> n;
|
||||
default -> Math.min(2, n);
|
||||
});
|
||||
}
|
||||
}
|
||||
return counts;
|
||||
}
|
||||
|
||||
public static void validate(List<DayPart> parts, Map<DanceGroupType, Integer> counts) {
|
||||
public static void validate(final List<DayPart> parts, final Map<DanceGroupType, Integer> counts) {
|
||||
validateParts(parts);
|
||||
if (counts == null) {
|
||||
invalidCounts();
|
||||
}
|
||||
if (parts.isEmpty() && counts.isEmpty()) {
|
||||
return;
|
||||
}
|
||||
if (counts.size() != DanceGroupType.values().length
|
||||
|| Arrays.stream(DanceGroupType.values())
|
||||
.anyMatch((final DanceGroupType t) -> counts.get(t) == null
|
||||
|| counts.get(t) < 1
|
||||
|| (!parts.isEmpty() && counts.get(t) > parts.size()))) {
|
||||
invalidCounts();
|
||||
}
|
||||
}
|
||||
|
||||
private static void validateParts(final List<DayPart> parts) {
|
||||
if (parts == null
|
||||
|| parts.size() > 6
|
||||
|| parts.stream().anyMatch(p -> p == null || p.getDate() == null || p.getType() == null)
|
||||
|| parts.stream().anyMatch((final DayPart p) -> p == null || p.getDate() == null || p.getType() == null)
|
||||
|| parts.stream()
|
||||
.map(p -> p.getDate() + ":" + p.getType())
|
||||
.map((final DayPart p) -> p.getDate() + ":" + p.getType())
|
||||
.distinct()
|
||||
.count()
|
||||
!= parts.size())
|
||||
!= parts.size()) {
|
||||
throw new ManagementException(
|
||||
ManagementExceptionKind.INVALID_INPUT,
|
||||
"Configure zero to six dayparts with unique date/type combinations.");
|
||||
if (counts == null) invalidCounts();
|
||||
if (parts.isEmpty() && counts.isEmpty()) return;
|
||||
if (counts.size() != DanceGroupType.values().length
|
||||
|| Arrays.stream(DanceGroupType.values())
|
||||
.anyMatch(t -> counts.get(t) == null
|
||||
|| counts.get(t) < 1
|
||||
|| (!parts.isEmpty() && counts.get(t) > parts.size()))) invalidCounts();
|
||||
}
|
||||
}
|
||||
|
||||
private static void invalidCounts() {
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
package nl.agilecc.agiledancers.service.model;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
import nl.agilecc.agiledancers.model.specifications.DanceGroupStyle;
|
||||
import nl.agilecc.agiledancers.model.specifications.DanceGroupType;
|
||||
@@ -12,4 +14,10 @@ public record Group(
|
||||
Integer endAge,
|
||||
List<DanceGroupStyle> styles,
|
||||
List<String> dancerIds,
|
||||
String coachId) {}
|
||||
String coachId) {
|
||||
// The maintainer-approved parameter waiver preserves this version-2 JSON contract.
|
||||
public Group {
|
||||
styles = styles == null ? null : Collections.unmodifiableList(new ArrayList<>(styles));
|
||||
dancerIds = dancerIds == null ? null : Collections.unmodifiableList(new ArrayList<>(dancerIds));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,14 @@
|
||||
package nl.agilecc.agiledancers.service.model;
|
||||
|
||||
import java.util.Collections;
|
||||
import java.util.LinkedHashSet;
|
||||
import java.util.Set;
|
||||
import nl.agilecc.agiledancers.model.specifications.DanceGroupStyle;
|
||||
import nl.agilecc.agiledancers.model.specifications.DanceGroupType;
|
||||
|
||||
public record GroupDetails(
|
||||
String category, DanceGroupType type, int startAge, int endAge, Set<DanceGroupStyle> styles) {
|
||||
public GroupDetails {
|
||||
styles = styles == null ? null : Collections.unmodifiableSet(new LinkedHashSet<>(styles));
|
||||
}
|
||||
}
|
||||
@@ -1,7 +1,13 @@
|
||||
package nl.agilecc.agiledancers.service.model;
|
||||
|
||||
import java.time.LocalDate;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
import nl.agilecc.agiledancers.model.specifications.DayPartType;
|
||||
|
||||
public record Part(LocalDate date, DayPartType type, List<String> groupIds) {}
|
||||
public record Part(LocalDate date, DayPartType type, List<String> groupIds) {
|
||||
public Part {
|
||||
groupIds = groupIds == null ? null : Collections.unmodifiableList(new ArrayList<>(groupIds));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,7 +1,15 @@
|
||||
package nl.agilecc.agiledancers.service.model;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collections;
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import nl.agilecc.agiledancers.model.specifications.DanceGroupType;
|
||||
|
||||
public record Performance(String id, String name, List<Part> dayParts, Map<DanceGroupType, Integer> appearances) {}
|
||||
public record Performance(String id, String name, List<Part> dayParts, Map<DanceGroupType, Integer> appearances) {
|
||||
public Performance {
|
||||
dayParts = dayParts == null ? null : Collections.unmodifiableList(new ArrayList<>(dayParts));
|
||||
appearances = appearances == null ? null : Collections.unmodifiableMap(new LinkedHashMap<>(appearances));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,9 +1,24 @@
|
||||
package nl.agilecc.agiledancers.service.model;
|
||||
|
||||
import java.time.LocalDate;
|
||||
import java.util.*;
|
||||
import nl.agilecc.agiledancers.model.*;
|
||||
import nl.agilecc.agiledancers.model.specifications.*;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collection;
|
||||
import java.util.Collections;
|
||||
import java.util.HashSet;
|
||||
import java.util.IdentityHashMap;
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.LinkedHashSet;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Objects;
|
||||
import java.util.Set;
|
||||
import java.util.UUID;
|
||||
import nl.agilecc.agiledancers.model.Coach;
|
||||
import nl.agilecc.agiledancers.model.DanceGroup;
|
||||
import nl.agilecc.agiledancers.model.DanceSchool;
|
||||
import nl.agilecc.agiledancers.model.Dancer;
|
||||
import nl.agilecc.agiledancers.model.DayPart;
|
||||
import nl.agilecc.agiledancers.model.Show;
|
||||
import nl.agilecc.agiledancers.service.ManagementException;
|
||||
import nl.agilecc.agiledancers.service.ManagementExceptionKind;
|
||||
import nl.agilecc.agiledancers.service.ShowConfiguration;
|
||||
@@ -17,23 +32,30 @@ public record SchoolSnapshot(
|
||||
List<Trainer> coaches,
|
||||
List<Group> groups,
|
||||
List<Performance> shows) {
|
||||
public static SchoolSnapshot of(DanceSchool s) {
|
||||
Map<Dancer, String> dancers = ids(s.getDancers());
|
||||
Map<Coach, String> coaches = ids(s.getCoaches());
|
||||
Map<DanceGroup, String> groups = ids(s.getDanceGroups());
|
||||
Map<Show, String> shows = ids(s.getShows());
|
||||
public SchoolSnapshot {
|
||||
dancers = dancers == null ? null : Collections.unmodifiableList(new ArrayList<>(dancers));
|
||||
coaches = coaches == null ? null : Collections.unmodifiableList(new ArrayList<>(coaches));
|
||||
groups = groups == null ? null : Collections.unmodifiableList(new ArrayList<>(groups));
|
||||
shows = shows == null ? null : Collections.unmodifiableList(new ArrayList<>(shows));
|
||||
}
|
||||
|
||||
public static SchoolSnapshot of(final DanceSchool s) {
|
||||
final Map<Dancer, String> dancers = ids(s.getDancers());
|
||||
final Map<Coach, String> coaches = ids(s.getCoaches());
|
||||
final Map<DanceGroup, String> groups = ids(s.getDanceGroups());
|
||||
final Map<Show, String> shows = ids(s.getShows());
|
||||
return new SchoolSnapshot(
|
||||
2,
|
||||
s.getKey(),
|
||||
s.getName(),
|
||||
s.getDancers().stream()
|
||||
.map(d -> new Person(dancers.get(d), d.getName(), d.getBirthDate()))
|
||||
.map((final Dancer d) -> new Person(dancers.get(d), d.getName(), d.getBirthDate()))
|
||||
.toList(),
|
||||
s.getCoaches().stream()
|
||||
.map(c -> new Trainer(coaches.get(c), c.getName()))
|
||||
.map((final Coach c) -> new Trainer(coaches.get(c), c.getName()))
|
||||
.toList(),
|
||||
s.getDanceGroups().stream()
|
||||
.map(g -> new Group(
|
||||
.map((final DanceGroup g) -> new Group(
|
||||
groups.get(g),
|
||||
g.getCategory(),
|
||||
g.getType(),
|
||||
@@ -44,11 +66,11 @@ public record SchoolSnapshot(
|
||||
g.getCoach() == null ? null : coaches.get(g.getCoach())))
|
||||
.toList(),
|
||||
s.getShows().stream()
|
||||
.map(show -> new Performance(
|
||||
.map((final Show show) -> new Performance(
|
||||
shows.get(show),
|
||||
show.getName(),
|
||||
show.getDayParts().stream()
|
||||
.map(p -> new Part(
|
||||
.map((final DayPart p) -> new Part(
|
||||
p.getDate(),
|
||||
p.getType(),
|
||||
p.getDanceGroups().stream()
|
||||
@@ -59,42 +81,62 @@ public record SchoolSnapshot(
|
||||
.toList());
|
||||
}
|
||||
|
||||
private static <T> Map<T, String> ids(Collection<T> values) {
|
||||
Map<T, String> result = new IdentityHashMap<>();
|
||||
for (T value : values) result.put(value, Integer.toString(result.size() + 1));
|
||||
private static <T> Map<T, String> ids(final Collection<T> values) {
|
||||
final Map<T, String> result = new IdentityHashMap<>();
|
||||
for (final T value : values) {
|
||||
result.put(value, Integer.toString(result.size() + 1));
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
/** Validates all references and values, then rebuilds an independent graph with temporary IDs. */
|
||||
public DanceSchool restore() {
|
||||
check(Objects.equals(formatVersion, 2), "Niet-ondersteunde formatVersion: " + formatVersion);
|
||||
check(key != null, "key ontbreekt");
|
||||
text(name, "schoolnaam");
|
||||
DanceSchool school = DanceSchool.builder().key(key).name(name).build();
|
||||
Map<String, Dancer> dancerMap = new LinkedHashMap<>();
|
||||
for (Person d : list(dancers, "dancers")) {
|
||||
final DanceSchool school = DanceSchool.builder().key(key).name(name).build();
|
||||
final Map<String, Dancer> dancerMap = restoreDancers(school);
|
||||
final Map<String, Coach> coachMap = restoreCoaches(school);
|
||||
final Map<String, DanceGroup> groupMap = restoreGroups(school, dancerMap, coachMap);
|
||||
restoreShows(school, groupMap);
|
||||
return school;
|
||||
}
|
||||
|
||||
private Map<String, Dancer> restoreDancers(final DanceSchool school) {
|
||||
final Map<String, Dancer> dancerMap = new LinkedHashMap<>();
|
||||
for (final Person d : list(dancers, "dancers")) {
|
||||
check(d != null, "Lege danser");
|
||||
text(d.name(), "dansernaam");
|
||||
check(
|
||||
d.birthDate() != null && !d.birthDate().isAfter(LocalDate.now()),
|
||||
"Ongeldige geboortedatum voor " + d.name());
|
||||
Dancer dancer =
|
||||
final Dancer dancer =
|
||||
Dancer.builder().name(d.name()).birthDate(d.birthDate()).build();
|
||||
// Assign temporary distinct IDs before inserting entities into sets.
|
||||
dancer.setId((long) dancerMap.size());
|
||||
put(dancerMap, d.id(), dancer);
|
||||
school.addDancer(dancer);
|
||||
}
|
||||
Map<String, Coach> coachMap = new LinkedHashMap<>();
|
||||
for (Trainer c : list(coaches, "coaches")) {
|
||||
return dancerMap;
|
||||
}
|
||||
|
||||
private Map<String, Coach> restoreCoaches(final DanceSchool school) {
|
||||
final Map<String, Coach> coachMap = new LinkedHashMap<>();
|
||||
for (final Trainer c : list(coaches, "coaches")) {
|
||||
check(c != null, "Lege coach");
|
||||
text(c.name(), "coachnaam");
|
||||
Coach coach = Coach.builder().name(c.name()).build();
|
||||
final Coach coach = Coach.builder().name(c.name()).build();
|
||||
coach.setId((long) coachMap.size());
|
||||
put(coachMap, c.id(), coach);
|
||||
school.addCoach(coach);
|
||||
}
|
||||
Map<String, DanceGroup> groupMap = new LinkedHashMap<>();
|
||||
for (Group g : list(groups, "groups")) {
|
||||
return coachMap;
|
||||
}
|
||||
|
||||
private Map<String, DanceGroup> restoreGroups(
|
||||
final DanceSchool school, final Map<String, Dancer> dancerMap, final Map<String, Coach> coachMap) {
|
||||
final Map<String, DanceGroup> groupMap = new LinkedHashMap<>();
|
||||
for (final Group g : list(groups, "groups")) {
|
||||
check(g != null, "Lege groep");
|
||||
text(g.category(), "groepsnaam");
|
||||
check(
|
||||
@@ -104,7 +146,7 @@ public record SchoolSnapshot(
|
||||
&& g.startAge() >= 0
|
||||
&& g.endAge() >= g.startAge(),
|
||||
"Ongeldig type of leeftijdsbereik voor groep " + g.category());
|
||||
DanceGroup group = DanceGroup.builder()
|
||||
final DanceGroup group = DanceGroup.builder()
|
||||
.category(g.category())
|
||||
.type(g.type())
|
||||
.startAge(g.startAge())
|
||||
@@ -120,20 +162,25 @@ public record SchoolSnapshot(
|
||||
.build();
|
||||
check(!group.getStyles().contains(null), "Ongeldige dansstijl");
|
||||
group.setId((long) groupMap.size());
|
||||
for (String id : unique(g.dancerIds(), "dancerIds"))
|
||||
for (final String id : unique(g.dancerIds(), "dancerIds")) {
|
||||
group.addDancer(ref(dancerMap, id, "groep " + g.category() + " verwijst naar een onbekende danser"));
|
||||
}
|
||||
put(groupMap, g.id(), group);
|
||||
school.addDanceGroup(group);
|
||||
}
|
||||
Set<String> showIds = new HashSet<>();
|
||||
for (Performance show : list(shows, "shows")) {
|
||||
return groupMap;
|
||||
}
|
||||
|
||||
private void restoreShows(final DanceSchool school, final Map<String, DanceGroup> groupMap) {
|
||||
final Set<String> showIds = new HashSet<>();
|
||||
for (final Performance show : list(shows, "shows")) {
|
||||
check(show != null, "Lege show");
|
||||
text(show.id(), "show-id");
|
||||
check(showIds.add(show.id()), "Dubbele show-id " + show.id());
|
||||
text(show.name(), "shownaam");
|
||||
List<DayPart> parts = new ArrayList<>();
|
||||
Set<String> types = new HashSet<>();
|
||||
for (Part part : list(show.dayParts(), "dayParts")) {
|
||||
final List<DayPart> parts = new ArrayList<>();
|
||||
final Set<String> types = new HashSet<>();
|
||||
for (final Part part : list(show.dayParts(), "dayParts")) {
|
||||
check(
|
||||
part != null
|
||||
&& part.type() != null
|
||||
@@ -144,13 +191,13 @@ public record SchoolSnapshot(
|
||||
.date(part.date())
|
||||
.type(part.type())
|
||||
.danceGroups(unique(part.groupIds(), "groupIds").stream()
|
||||
.map(id ->
|
||||
.map((final String id) ->
|
||||
ref(groupMap, id, "Show " + show.name() + " verwijst naar een onbekende groep"))
|
||||
.toList())
|
||||
.build());
|
||||
}
|
||||
ShowConfiguration.validate(parts, show.appearances());
|
||||
Show entity = Show.builder()
|
||||
final Show entity = Show.builder()
|
||||
.name(show.name())
|
||||
.dayParts(parts)
|
||||
.appearances(Map.copyOf(show.appearances()))
|
||||
@@ -158,19 +205,18 @@ public record SchoolSnapshot(
|
||||
entity.setId((long) showIds.size());
|
||||
school.addShow(entity);
|
||||
}
|
||||
return school;
|
||||
}
|
||||
|
||||
private static void text(String value, String field) {
|
||||
private static void text(final String value, final String field) {
|
||||
check(value != null && !value.isBlank(), field + " ontbreekt");
|
||||
}
|
||||
|
||||
private static <T> List<T> list(List<T> value, String field) {
|
||||
private static <T> List<T> list(final List<T> value, final String field) {
|
||||
check(value != null, field + " ontbreekt");
|
||||
return value;
|
||||
}
|
||||
|
||||
private static List<String> unique(List<String> value, String field) {
|
||||
private static List<String> unique(final List<String> value, final String field) {
|
||||
list(value, field);
|
||||
check(
|
||||
value.stream().noneMatch(Objects::isNull) && new HashSet<>(value).size() == value.size(),
|
||||
@@ -178,18 +224,20 @@ public record SchoolSnapshot(
|
||||
return value;
|
||||
}
|
||||
|
||||
private static <T> void put(Map<String, T> map, String id, T value) {
|
||||
private static <T> void put(final Map<String, T> map, final String id, final T value) {
|
||||
text(id, "id");
|
||||
check(map.putIfAbsent(id, value) == null, "Dubbele id " + id);
|
||||
}
|
||||
|
||||
private static <T> T ref(Map<String, T> map, String id, String error) {
|
||||
T value = map.get(id);
|
||||
private static <T> T ref(final Map<String, T> map, final String id, final String error) {
|
||||
final T value = map.get(id);
|
||||
check(value != null, error);
|
||||
return value;
|
||||
}
|
||||
|
||||
private static void check(boolean valid, String message) {
|
||||
if (!valid) throw new ManagementException(ManagementExceptionKind.INVALID_INPUT, message);
|
||||
private static void check(final boolean valid, final String message) {
|
||||
if (!valid) {
|
||||
throw new ManagementException(ManagementExceptionKind.INVALID_INPUT, message);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,5 +4,6 @@ import nl.agilecc.agiledancers.model.scheduling.SchedulingInput;
|
||||
import nl.agilecc.agiledancers.model.scheduling.SchedulingResult;
|
||||
|
||||
public interface SchedulingEngine {
|
||||
SchedulingResult solve(SchedulingInput input);
|
||||
/** Computes assignments without mutating the captured inputs; the caller validates and commits the result. */
|
||||
public SchedulingResult solve(final SchedulingInput input);
|
||||
}
|
||||
|
||||
@@ -1,45 +1,71 @@
|
||||
package nl.agilecc.agiledancers.solver;
|
||||
|
||||
import com.google.ortools.Loader;
|
||||
import com.google.ortools.sat.*;
|
||||
import com.google.ortools.sat.BoolVar;
|
||||
import com.google.ortools.sat.CpModel;
|
||||
import com.google.ortools.sat.CpSolver;
|
||||
import com.google.ortools.sat.CpSolverStatus;
|
||||
import com.google.ortools.sat.IntVar;
|
||||
import com.google.ortools.sat.LinearExpr;
|
||||
import jakarta.inject.Singleton;
|
||||
import java.util.*;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import nl.agilecc.agiledancers.model.scheduling.SchedulingInput;
|
||||
import nl.agilecc.agiledancers.model.scheduling.SchedulingResult;
|
||||
import nl.agilecc.agiledancers.service.*;
|
||||
import nl.agilecc.agiledancers.service.ManagementException;
|
||||
import nl.agilecc.agiledancers.service.ManagementExceptionKind;
|
||||
import nl.agilecc.agiledancers.service.port.SchedulingEngine;
|
||||
|
||||
@Singleton
|
||||
public class OrToolsSchedulingEngine implements SchedulingEngine {
|
||||
@Override
|
||||
public SchedulingResult solve(SchedulingInput input) {
|
||||
public SchedulingResult solve(final SchedulingInput input) {
|
||||
Loader.loadNativeLibraries();
|
||||
CpModel model = new CpModel();
|
||||
int n = input.dayParts(), groups = input.groups().size();
|
||||
BoolVar[][] assigned = new BoolVar[groups][n];
|
||||
for (int g = 0; g < groups; g++) {
|
||||
for (int p = 0; p < n; p++) assigned[g][p] = model.newBoolVar("g" + g + "p" + p);
|
||||
model.addEquality(LinearExpr.sum(assigned[g]), input.groups().get(g).appearances());
|
||||
}
|
||||
IntVar[] loads = new IntVar[n];
|
||||
for (int p = 0; p < n; p++) {
|
||||
BoolVar[] column = new BoolVar[groups];
|
||||
for (int g = 0; g < groups; g++) column[g] = assigned[g][p];
|
||||
loads[p] = model.newIntVar(0, groups, "load" + p);
|
||||
model.addEquality(loads[p], LinearExpr.sum(column));
|
||||
}
|
||||
IntVar max = model.newIntVar(0, groups, "max"), min = model.newIntVar(0, groups, "min");
|
||||
model.addMaxEquality(max, loads);
|
||||
model.addMinEquality(min, loads);
|
||||
model.minimize(LinearExpr.weightedSum(new IntVar[] {max, min}, new long[] {1, -1}));
|
||||
CpSolver solver = new CpSolver();
|
||||
final CpModel model = new CpModel();
|
||||
final BoolVar[][] assigned = assignments(model, input);
|
||||
balance(model, assigned, input.dayParts());
|
||||
final CpSolver solver = new CpSolver();
|
||||
solver.getParameters().setMaxTimeInSeconds(1800);
|
||||
// Quarkus owns process shutdown; do not install a native SIGINT handler.
|
||||
solver.getParameters().setCatchSigintSignal(false);
|
||||
// Single-worker search with a fixed seed makes identical inputs repeatable.
|
||||
solver.getParameters().setNumSearchWorkers(1).setRandomSeed(0);
|
||||
CpSolverStatus status = solver.solve(model);
|
||||
if (status != CpSolverStatus.OPTIMAL && status != CpSolverStatus.FEASIBLE)
|
||||
final CpSolverStatus status = solver.solve(model);
|
||||
requireSolution(status);
|
||||
return new SchedulingResult(status == CpSolverStatus.OPTIMAL, readAssignments(input, assigned, solver));
|
||||
}
|
||||
|
||||
private static BoolVar[][] assignments(final CpModel model, final SchedulingInput input) {
|
||||
final BoolVar[][] assigned = new BoolVar[input.groups().size()][input.dayParts()];
|
||||
for (int g = 0; g < assigned.length; g++) {
|
||||
for (int p = 0; p < input.dayParts(); p++) {
|
||||
assigned[g][p] = model.newBoolVar("g" + g + "p" + p);
|
||||
}
|
||||
model.addEquality(LinearExpr.sum(assigned[g]), input.groups().get(g).appearances());
|
||||
}
|
||||
return assigned;
|
||||
}
|
||||
|
||||
private static void balance(final CpModel model, final BoolVar[][] assigned, final int dayParts) {
|
||||
final int groups = assigned.length;
|
||||
final IntVar[] loads = new IntVar[dayParts];
|
||||
for (int p = 0; p < dayParts; p++) {
|
||||
final BoolVar[] column = new BoolVar[groups];
|
||||
for (int g = 0; g < groups; g++) {
|
||||
column[g] = assigned[g][p];
|
||||
}
|
||||
loads[p] = model.newIntVar(0, groups, "load" + p);
|
||||
model.addEquality(loads[p], LinearExpr.sum(column));
|
||||
}
|
||||
final IntVar max = model.newIntVar(0, groups, "max");
|
||||
final IntVar min = model.newIntVar(0, groups, "min");
|
||||
model.addMaxEquality(max, loads);
|
||||
model.addMinEquality(min, loads);
|
||||
model.minimize(LinearExpr.weightedSum(new IntVar[] {max, min}, new long[] {1, -1}));
|
||||
}
|
||||
|
||||
private static void requireSolution(final CpSolverStatus status) {
|
||||
if (status != CpSolverStatus.OPTIMAL && status != CpSolverStatus.FEASIBLE) {
|
||||
throw new ManagementException(
|
||||
ManagementExceptionKind.INVALID_INPUT,
|
||||
switch (status) {
|
||||
@@ -49,14 +75,21 @@ public class OrToolsSchedulingEngine implements SchedulingEngine {
|
||||
+ " preserved.";
|
||||
default -> "Solver failed (" + status + "). Existing assignments were preserved.";
|
||||
});
|
||||
List<List<Long>> parts = new ArrayList<>();
|
||||
for (int p = 0; p < n; p++) {
|
||||
List<Long> ids = new ArrayList<>();
|
||||
for (int g = 0; g < groups; g++)
|
||||
if (solver.booleanValue(assigned[g][p]))
|
||||
}
|
||||
}
|
||||
|
||||
private static List<List<Long>> readAssignments(
|
||||
final SchedulingInput input, final BoolVar[][] assigned, final CpSolver solver) {
|
||||
final List<List<Long>> parts = new ArrayList<>();
|
||||
for (int p = 0; p < input.dayParts(); p++) {
|
||||
final List<Long> ids = new ArrayList<>();
|
||||
for (int g = 0; g < assigned.length; g++) {
|
||||
if (solver.booleanValue(assigned[g][p])) {
|
||||
ids.add(input.groups().get(g).groupId());
|
||||
}
|
||||
}
|
||||
parts.add(List.copyOf(ids));
|
||||
}
|
||||
return new SchedulingResult(status == CpSolverStatus.OPTIMAL, List.copyOf(parts));
|
||||
return List.copyOf(parts);
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user