Skip to content

Commit 0506e9c

Browse files
committed
chore: address review comments
1 parent 0e7b8ec commit 0506e9c

17 files changed

Lines changed: 62 additions & 55 deletions

File tree

core/src/main/java/ai/timefold/solver/core/api/score/constraint/ConstraintRef.java

Lines changed: 1 addition & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -17,22 +17,12 @@ public record ConstraintRef(String constraintName)
1717
implements
1818
Comparable<ConstraintRef> {
1919

20-
private static final char PACKAGE_SEPARATOR = '/';
21-
2220
public static ConstraintRef of(String constraintName) {
2321
return new ConstraintRef(constraintName);
2422
}
2523

2624
public ConstraintRef {
27-
var sanitized = AbstractConstraintBuilder.sanitize("constraintName", constraintName);
28-
if (sanitized.isEmpty()) {
29-
throw new IllegalArgumentException("The %s cannot be empty."
30-
.formatted("constraint name"));
31-
} else if (sanitized.contains("" + PACKAGE_SEPARATOR)) {
32-
throw new IllegalArgumentException("The %s (%s) cannot contain a package separator (%s)."
33-
.formatted("constraint name", sanitized, PACKAGE_SEPARATOR));
34-
}
35-
constraintName = sanitized;
25+
constraintName = AbstractConstraintBuilder.sanitize("constraintName", constraintName);
3626
}
3727

3828
@Override

core/src/main/java/ai/timefold/solver/core/api/score/stream/ConstraintBuilder.java

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@ default Constraint asConstraint(String constraintName) {
2222
* placing the constraint in the {@link Constraint#DEFAULT_CONSTRAINT_GROUP default constraint group}.
2323
*
2424
* @param constraintName shows up in {@link ConstraintMatchTotal} during score justification
25+
* @param constraintDescription can contain any character, but it is recommended to keep it short and concise.
2526
*/
2627
default Constraint asConstraintDescribed(String constraintName, String constraintDescription) {
2728
return asConstraintDescribed(constraintName, constraintDescription, Constraint.DEFAULT_CONSTRAINT_GROUP);
@@ -38,6 +39,7 @@ default Constraint asConstraintDescribed(String constraintName, String constrain
3839
* and therefore doesn't need to be URL-friendly, or protected against injection attacks.
3940
*
4041
* @param constraintName shows up in {@link ConstraintMatchTotal} during score justification
42+
* @param constraintDescription can contain any character, but it is recommended to keep it short and concise.
4143
* @param constraintGroup not used by the solver directly, but may be used by external tools to group constraints together,
4244
* such as by their source or by their purpose
4345
*/

core/src/main/java/ai/timefold/solver/core/impl/score/stream/common/AbstractConstraintBuilder.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -38,7 +38,7 @@ public final Constraint asConstraintDescribed(String constraintName, String cons
3838

3939
public static String sanitize(String fieldName, String fieldValue) {
4040
if (fieldValue == null || fieldValue.equalsIgnoreCase("null") || fieldValue.equalsIgnoreCase("nil")) {
41-
throw new IllegalArgumentException("The %s cannot be null.".formatted(fieldName));
41+
throw new IllegalArgumentException("The %s (%s) cannot be null.".formatted(fieldName, fieldValue));
4242
}
4343
if (!fieldValue.matches("^[a-zA-Z0-9]+[a-zA-Z0-9 _.'-]*$")) {
4444
throw new IllegalArgumentException(

core/src/main/java/ai/timefold/solver/core/impl/solver/DefaultSolverJob.java

Lines changed: 30 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -58,13 +58,13 @@ public final class DefaultSolverJob<Solution_> implements SolverJob<Solution_>,
5858
private final @Nullable Consumer<SolverJobStartedEvent<Solution_>> solverJobStartedConsumer;
5959
private final BiConsumer<? super Object, ? super Throwable> exceptionHandler;
6060

61-
private volatile SolverStatus solverStatus;
6261
private final CountDownLatch terminatedLatch;
6362
private final ReentrantLock solverStatusModifyingLock;
6463
private final AtomicBoolean terminatedEarly = new AtomicBoolean(false);
6564
private final BestSolutionHolder<Solution_> bestSolutionHolder = new BestSolutionHolder<>();
6665
private final AtomicReference<@Nullable ProblemSizeStatistics> temporaryProblemSizeStatistics = new AtomicReference<>();
6766

67+
private volatile SolverStatus solverStatus = SolverStatus.SOLVING_SCHEDULED;
6868
private @Nullable Future<Solution_> finalBestSolutionFuture;
6969
private @Nullable ConsumerSupport<Solution_, Object> consumerSupport;
7070

@@ -88,9 +88,8 @@ public DefaultSolverJob(DefaultSolverManager<Solution_> solverManager, Solver<So
8888
this.firstInitializedSolutionConsumer = firstInitializedSolutionConsumer;
8989
this.solverJobStartedConsumer = solverJobStartedConsumer;
9090
this.exceptionHandler = exceptionHandler;
91-
solverStatus = SolverStatus.SOLVING_SCHEDULED;
92-
terminatedLatch = new CountDownLatch(1);
93-
solverStatusModifyingLock = new ReentrantLock();
91+
this.terminatedLatch = new CountDownLatch(1);
92+
this.solverStatusModifyingLock = new ReentrantLock();
9493
}
9594

9695
public void setFinalBestSolutionFuture(Future<Solution_> finalBestSolutionFuture) {
@@ -268,7 +267,7 @@ public ProblemSizeStatistics getProblemSizeStatistics() {
268267
// before the solving has started.
269268
// Once the solving has started, the problem size statistics will be computed
270269
// using the ScoreDirector's hot ValueRangeManager.
271-
return temporaryProblemSizeStatistics.updateAndGet(oldStatistics -> {
270+
var result = temporaryProblemSizeStatistics.updateAndGet(oldStatistics -> {
272271
if (oldStatistics != null) {
273272
// If the problem size statistics were already computed, return them.
274273
// This can happen if the problem size statistics were computed before the solving started.
@@ -278,6 +277,11 @@ public ProblemSizeStatistics getProblemSizeStatistics() {
278277
var valueManager = ValueRangeManager.of(solutionDescriptor, problemFinder.apply(problemId));
279278
return valueManager.getProblemSizeStatistics();
280279
});
280+
// Avoids nullness issues reported by IDE which cannot actually happen.
281+
// The result can never be null, because none of the methods called in the lambda can return null,
282+
// and the lambda is the only way to set the value of the temporaryProblemSizeStatistics,
283+
// which is the only way for it to be null.
284+
return Objects.requireNonNull(result);
281285
}
282286

283287
public SolverTermination<Solution_> getSolverTermination() {
@@ -293,21 +297,33 @@ void close() {
293297

294298
/**
295299
* A listener that unlocks the solverStatusModifyingLock when Solving has started.
296-
*
297300
* It prevents the following scenario caused by unlocking before Solving started:
298301
*
299-
* Thread 1:
300-
* solverStatusModifyingLock.unlock()
301-
* >solver.solve(...) // executes second
302-
*
303-
* Thread 2:
304-
* case SOLVING_ACTIVE:
305-
* >solver.terminateEarly(); // executes first
302+
* <dl>
303+
* <dt>Thread 1</dt>
304+
* <dd>
305+
*
306+
* <pre>
307+
* solverStatusModifyingLock.unlock()
308+
* > solver.solve(...) // executes second
309+
* </pre>
310+
*
311+
* </dd>
312+
* <dt>Thread 2</dt>
313+
* <dd>
314+
*
315+
* <pre>
316+
* case SOLVING_ACTIVE:
317+
* >solver.terminateEarly(); // executes first
318+
* </pre>
319+
*
320+
* </dd>
321+
* </dl>
306322
*
307323
* The solver.solve() call resets the terminateEarly flag, and thus the solver will not be terminated
308324
* by the call, which means terminatedLatch will not be decremented, causing Thread 2 to wait forever
309325
* (at least until another Thread calls terminateEarly again).
310-
*
326+
* <p>
311327
* To prevent Thread 2 from potentially waiting forever, we only unlock the lock after the
312328
* solvingStarted phase lifecycle event is fired, meaning the terminateEarly flag will not be
313329
* reset and thus the solver will actually terminate.

core/src/main/java/ai/timefold/solver/core/impl/solver/DefaultSolverManager.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -66,7 +66,7 @@ private void validateSolverFactory() {
6666
solverFactory.buildSolver();
6767
}
6868

69-
private Object getProblemIdOrThrow(Object problemId) {
69+
private static Object getProblemIdOrThrow(Object problemId) {
7070
return Objects.requireNonNull(problemId, "Invalid problemId (null) given to SolverManager.");
7171
}
7272

docs/TODO.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -38,5 +38,6 @@
3838
- [ ] `ProblemChangeDirector#lookUpWorkingObject()` no longer returns `Optional`.
3939
- [ ] `AutoDiscoverMemberType` is gone
4040
- [ ] Constraint name and group now forces strict validation.
41+
- [ ] `SolverManager` and `SolverJob` no longer have the `ProblemId_` type parameter.
4142

4243
Remove this file when done.

docs/src/modules/ROOT/pages/integration/integration.adoc

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -177,8 +177,9 @@ Add a dependency to the `timefold-solver-jackson` jar and register `TimefoldJack
177177

178178
[source,java,options="nowrap"]
179179
----
180-
ObjectMapper objectMapper = new ObjectMapper();
181-
objectMapper.registerModule(TimefoldJacksonModule.createModule());
180+
var objectMapper = JsonMapper.builder()
181+
.addModule(TimefoldJacksonModule.createModule())
182+
.build();
182183
----
183184

184185

docs/src/modules/ROOT/pages/quickstart/quarkus/quarkus-quickstart.adoc

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -315,9 +315,13 @@ class TimetableResourceTest {
315315
@Test
316316
void solveDemoDataUntilFeasible() throws IOException {
317317
// Provide the same input data shown in the text above.
318-
Timetable testTimetable = new ObjectMapper()
319-
.findAndRegisterModules()
320-
.readValue(new File("src/test/resources/testing-timetable.json"), Timetable.class);
318+
ObjectMapper objectMapper = JsonMapper.builder()
319+
.findAndAddModules()
320+
.build();
321+
Timetable testTimetable = objectMapper
322+
.readValue(
323+
new File("src/test/resources/testing-timetable.json"),
324+
Timetable.class);
321325
322326
Timetable solution = given()
323327
.contentType(ContentType.JSON)

docs/src/modules/ROOT/pages/using-timefold-solver/benchmarking-and-tweaking.adoc

Lines changed: 3 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -219,10 +219,9 @@ public class VehicleRoutePlanJsonSolutionFileIO extends JacksonSolutionFileIO<Ve
219219
public VehicleRoutePlanJsonSolutionFileIO() {
220220
// VehicleRoutePlan is the @PlanningSolution class.
221221
super(VehicleRoutePlan.class,
222-
new ObjectMapper()
223-
.registerModule(new JavaTimeModule())
224-
.disable(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS)
225-
);
222+
new JsonMapper.builder()
223+
...
224+
.build());
226225
}
227226
228227
}

persistence/jackson/src/test/java/ai/timefold/solver/jackson/api/AbstractJacksonRoundTripTest.java

Lines changed: 0 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -9,10 +9,6 @@ public abstract class AbstractJacksonRoundTripTest {
99
// Helper methods
1010
// ************************************************************************
1111

12-
protected static <W> W serializeAndDeserialize(W input) {
13-
return serializeAndDeserialize(new ObjectMapper(), input);
14-
}
15-
1612
protected static <W> W serializeAndDeserialize(ObjectMapper objectMapper, W input) {
1713
String jsonString;
1814
W output;

0 commit comments

Comments
 (0)