Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@
import ai.timefold.solver.service.quarkus.deployment.builditem.AdditionalDescriptorFilesBuildItem;
import ai.timefold.solver.service.quarkus.deployment.builditem.ModelComponentsBuildItem;
import ai.timefold.solver.service.quarkus.deployment.builditem.ModelInfoBuildItem;
import ai.timefold.solver.service.quarkus.deployment.util.EmptyInstances;

import org.eclipse.microprofile.config.Config;
import org.eclipse.microprofile.config.ConfigProvider;
Expand Down Expand Up @@ -81,7 +82,7 @@ public void generateDefaultConfigProfile(ModelInfoBuildItem modelInfo,

ClassInfo modelConfigOverrides = modelComponentsBuildItem.getModelConfigOverrides();
Class<?> clazz = Thread.currentThread().getContextClassLoader().loadClass(modelConfigOverrides.name().toString());
Object instance = clazz.getDeclaredConstructor().newInstance();
Object instance = EmptyInstances.of(clazz);

Map<String, Object> modelOverrides = MAPPER.readValue(MAPPER.writeValueAsString(instance), Map.class);

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
package ai.timefold.solver.service.quarkus.deployment.util;

import java.lang.reflect.RecordComponent;
import java.util.Arrays;

/**
* Creates "empty" instances of user model types for build-time processing.
* <p>
* Regular classes are instantiated through their no-arg constructor.
* Records are instantiated through their canonical constructor with default component values
* ({@code null} for reference types, zero/{@code false} for primitives), so models do not need
* an all-null no-arg constructor solely to satisfy reflective instantiation.
*/
public final class EmptyInstances {

private EmptyInstances() {
}

/**
* Creates an empty instance of {@code type}.
*
* @param type the type to instantiate; never null
* @return a new empty instance; never null
* @throws ReflectiveOperationException if reflective construction fails
*/
@SuppressWarnings("unchecked")
public static <T> T of(Class<T> type) throws ReflectiveOperationException {
if (type.isRecord()) {
RecordComponent[] components = type.getRecordComponents();
Class<?>[] paramTypes = Arrays.stream(components)
.map(RecordComponent::getType)
.toArray(Class<?>[]::new);
Object[] args = Arrays.stream(components)
.map(component -> defaultValueFor(component.getType()))
.toArray();
return type.getDeclaredConstructor(paramTypes).newInstance(args);
}
return type.getDeclaredConstructor().newInstance();
}

static Object defaultValueFor(Class<?> type) {
if (!type.isPrimitive()) {
return null;
}
if (type == boolean.class) {
return false;
}
if (type == byte.class) {
return (byte) 0;
}
if (type == short.class) {
return (short) 0;
}
if (type == int.class) {
return 0;
}
if (type == long.class) {
return 0L;
}
if (type == float.class) {
return 0f;
}
if (type == double.class) {
return 0d;
}
if (type == char.class) {
return '\0';
}
throw new IllegalArgumentException("Unsupported primitive type: " + type.getName());
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -7,18 +7,14 @@
import com.fasterxml.jackson.annotation.JsonFormat;
import com.fasterxml.jackson.annotation.JsonInclude;

public class TestdataModelConfigOverrides implements ModelConfigOverrides {

// Make sure the field is not omitted by the model descriptor even if it's null by default.
@JsonFormat(shape = JsonFormat.Shape.STRING)
@JsonInclude(JsonInclude.Include.NON_NULL)
private Duration maximumTimeBurden;

public Duration getMaximumTimeBurden() {
return maximumTimeBurden;
}

public void setMaximumTimeBurden(Duration maximumTimeBurden) {
this.maximumTimeBurden = maximumTimeBurden;
}
/**
* Record-based model config overrides without a no-arg constructor.
* Used to verify {@link ai.timefold.solver.service.quarkus.deployment.util.EmptyInstances}
* can instantiate records for default config profile generation.
*/
public record TestdataModelConfigOverrides(
// Make sure the field is not omitted by the model descriptor even if it's null by default.
@JsonFormat(shape = JsonFormat.Shape.STRING) @JsonInclude(JsonInclude.Include.NON_NULL) Duration maximumTimeBurden)
implements
ModelConfigOverrides {
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
package ai.timefold.solver.service.quarkus.deployment.util;

import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.assertThatThrownBy;

import org.junit.jupiter.api.Test;

class EmptyInstancesTest {

@Test
void instantiatesClassViaNoArgConstructor() throws Exception {
PlainConfig instance = EmptyInstances.of(PlainConfig.class);

assertThat(instance).isNotNull();
assertThat(instance.value).isNull();
}

@Test
void instantiatesRecordViaCanonicalConstructorWithNullComponents() throws Exception {
RecordConfig instance = EmptyInstances.of(RecordConfig.class);

assertThat(instance).isNotNull();
assertThat(instance.name()).isNull();
assertThat(instance.weight()).isNull();
}

@Test
void instantiatesRecordWithPrimitiveDefaults() throws Exception {
PrimitiveRecordConfig instance = EmptyInstances.of(PrimitiveRecordConfig.class);

assertThat(instance.enabled()).isFalse();
assertThat(instance.count()).isZero();
assertThat(instance.ratio()).isZero();
assertThat(instance.label()).isNull();
}

@Test
void compactConstructorStillRunsForEmptyRecord() throws Exception {
CompactRecordConfig instance = EmptyInstances.of(CompactRecordConfig.class);

assertThat(instance.name()).isEqualTo("default");
}

@Test
void failsForClassWithoutNoArgConstructor() {
assertThatThrownBy(() -> EmptyInstances.of(ClassWithoutNoArgConstructor.class))
.isInstanceOf(NoSuchMethodException.class);
}

public static class PlainConfig {
public String value;
}

public record RecordConfig(String name, Integer weight) {
}

public record PrimitiveRecordConfig(boolean enabled, int count, double ratio, String label) {
}

public record CompactRecordConfig(String name) {
public CompactRecordConfig {
if (name == null) {
name = "default";
}
}
}

public static class ClassWithoutNoArgConstructor {
public ClassWithoutNoArgConstructor(String value) {
}
}
}