diff --git a/build.gradle b/build.gradle index 5f5bb6b8..b535e7f7 100644 --- a/build.gradle +++ b/build.gradle @@ -8,7 +8,7 @@ plugins { } group 'com.docutools' -version = '1.2.13' +version = '1.3.0' sourceCompatibility = 17 targetCompatibility = 17 diff --git a/src/main/java/com/docutools/jocument/impl/ReflectionResolver.java b/src/main/java/com/docutools/jocument/impl/ReflectionResolver.java index af16b638..8459c002 100644 --- a/src/main/java/com/docutools/jocument/impl/ReflectionResolver.java +++ b/src/main/java/com/docutools/jocument/impl/ReflectionResolver.java @@ -11,6 +11,7 @@ import com.docutools.jocument.impl.word.placeholders.ImagePlaceholderData; import java.lang.annotation.Annotation; import java.lang.reflect.InvocationTargetException; +import java.lang.reflect.RecordComponent; import java.math.RoundingMode; import java.nio.file.Path; import java.text.NumberFormat; @@ -21,6 +22,7 @@ import java.time.format.DateTimeFormatter; import java.time.format.FormatStyle; import java.time.temporal.Temporal; +import java.util.Arrays; import java.util.Collection; import java.util.Currency; import java.util.List; @@ -49,8 +51,7 @@ public class ReflectionResolver extends PlaceholderResolver { private final CustomPlaceholderRegistry customPlaceholderRegistry; public ReflectionResolver(Object value) { - this.bean = value; - this.customPlaceholderRegistry = new CustomPlaceholderRegistryImpl(); //NoOp CustomPlaceholderRegistry + this(value, new CustomPlaceholderRegistryImpl()); //NoOp CustomPlaceholderRegistry } public ReflectionResolver(Object value, CustomPlaceholderRegistry customPlaceholderRegistry) { @@ -126,7 +127,8 @@ public Optional resolve(String placeholderName, Locale locale) logger.debug("Trying to resolve placeholder {}", placeholderName); Optional result = Optional.empty(); for (String property : placeholderName.split("\\.")) { - result = result.isEmpty() ? doResolve(property, locale) : + result = result.isEmpty() ? + doResolve(property, locale) : result .flatMap(r -> r.stream().findAny()) .flatMap(r -> r.resolve(property, locale)); @@ -139,7 +141,7 @@ private Optional doResolve(String placeholderName, Locale local if (customPlaceholderRegistry.governs(placeholderName)) { return customPlaceholderRegistry.resolve(placeholderName); } - var property = SELF_REFERENCE.equals(placeholderName) ? bean : pub.getProperty(bean, placeholderName); + var property = getBeanProperty(placeholderName); if (property == null) { return Optional.empty(); } @@ -161,9 +163,9 @@ private Optional doResolve(String placeholderName, Locale local .withMaxWidth(image.maxWidth())); } if (bean.equals(property)) { - return Optional.of(new IterablePlaceholderData(List.of(new ReflectionResolver(bean)), 1)); + return Optional.of(new IterablePlaceholderData(List.of(new ReflectionResolver(bean, customPlaceholderRegistry)), 1)); } else { - var value = pub.getProperty(bean, placeholderName); + var value = getBeanProperty(placeholderName); return Optional.of(new IterablePlaceholderData(List.of(new ReflectionResolver(value, customPlaceholderRegistry)), 1)); } } catch (NoSuchMethodException | IllegalArgumentException e) { @@ -178,6 +180,21 @@ private Optional doResolve(String placeholderName, Locale local } } + private Object getBeanProperty(String placeholderName) throws InvocationTargetException, IllegalAccessException, NoSuchMethodException { + if (SELF_REFERENCE.equals(placeholderName)) { + return bean; + } else if (bean.getClass().isRecord()) { + var accessor = Arrays.stream(bean.getClass().getRecordComponents()) + .filter(recordComponent -> recordComponent.getName().equals(placeholderName)) + .map(RecordComponent::getAccessor) + .findFirst() + .orElseThrow(() -> new NoSuchMethodException("Record %s does not have field %s".formatted(bean.getClass().toString(), placeholderName))); + return accessor.invoke(bean); + } else { + return pub.getProperty(bean, placeholderName); + } + } + private Optional formatTemporal(String placeholderName, Temporal time, Locale locale) { Optional formatter; if (isFieldAnnotatedWith(bean.getClass(), placeholderName, Format.class)) { diff --git a/src/test/java/com/docutools/jocument/ReflectionResolving.java b/src/test/java/com/docutools/jocument/ReflectionResolving.java index 6bf1ae24..7879bdc8 100644 --- a/src/test/java/com/docutools/jocument/ReflectionResolving.java +++ b/src/test/java/com/docutools/jocument/ReflectionResolving.java @@ -9,21 +9,15 @@ import com.docutools.jocument.sample.model.SampleModelData; import com.docutools.jocument.sample.model.Uniform; import java.util.List; +import java.util.Objects; import java.util.stream.Collectors; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Tag; import org.junit.jupiter.api.Test; -import java.util.List; -import java.util.Objects; -import java.util.stream.Collectors; - -import static org.hamcrest.MatcherAssert.assertThat; -import static org.hamcrest.Matchers.contains; -import static org.hamcrest.Matchers.equalTo; -import static org.hamcrest.Matchers.is; - @DisplayName("Resolve placeholders from an object graph via reflection.") +@Tag("automated") public class ReflectionResolving { private PlaceholderResolver resolver; @@ -117,4 +111,39 @@ void shouldResolveSelf() { // Assert assertThat(captainsName, equalTo(SampleModelData.PICARD.getName())); } + + @Test + @DisplayName("Resolve record") + void shouldResolveRecord() { + // Assemble + resolver = new ReflectionResolver(SampleModelData.ENTERPRISE); + + // Act + var shipName = resolver.resolve("name") + .map(Object::toString) + .orElseThrow(); + var captain = resolver.resolve("captain.name") + .map(Object::toString) + .orElseThrow(); + var shipCrew = resolver.resolve("crew") + .map(Object::toString) + .map(Integer::parseInt) + .orElseThrow(); + var visitedPlanets = resolver.resolve("services") + .orElseThrow() + .stream() + .flatMap(placeholderResolver -> placeholderResolver.resolve("visitedPlanets") + .orElseThrow() + .stream()) + .map(placeholderResolver -> placeholderResolver.resolve("planetName") + .orElseThrow()) + .map(Object::toString) + .collect(Collectors.toList()); + + // Assert + assertThat(shipName, equalTo(SampleModelData.ENTERPRISE.name())); + assertThat(captain, equalTo(SampleModelData.PICARD.getName())); + assertThat(shipCrew, equalTo(5)); + assertThat(visitedPlanets, contains("Mars", "Venus", "Jupiter")); + } } diff --git a/src/test/java/com/docutools/jocument/sample/model/SampleModelData.java b/src/test/java/com/docutools/jocument/sample/model/SampleModelData.java index 46e15657..637877e8 100644 --- a/src/test/java/com/docutools/jocument/sample/model/SampleModelData.java +++ b/src/test/java/com/docutools/jocument/sample/model/SampleModelData.java @@ -5,26 +5,30 @@ import java.time.LocalDate; import java.util.Collections; import java.util.List; +import java.util.UUID; public class SampleModelData { public static final Captain PICARD; public static final Person PICARD_PERSON = new Person("Jean-Luc", "Picard", LocalDate.of(1948, 9, 23)); public static final List CAPTAINS; + public static final Ship ENTERPRISE; static { try { + var services = List.of(new Service("USS Enterprise", Collections.singletonList( + new PlanetServiceInfo("Mars", Collections.singletonList(new City("Nova Rojava"))))), + new Service("US Defiant", List.of( + new PlanetServiceInfo("Venus", List.of(new City("Nova Parisia"), new City("Birnin Zana"))), + new PlanetServiceInfo("Jupiter", List.of(new City("Exarcheia"), new City("Nova Metalkova")))))); PICARD = new Captain("Jean-Luc Picard", 4, Uniform.Red, new FirstOfficer("Riker", 3, Uniform.Red), - List.of(new Service("USS Enterprise", Collections.singletonList( - new PlanetServiceInfo("Mars", Collections.singletonList(new City("Nova Rojava"))))), - new Service("US Defiant", List.of( - new PlanetServiceInfo("Venus", List.of(new City("Nova Parisia"), new City("Birnin Zana"))), - new PlanetServiceInfo("Jupiter", List.of(new City("Exarcheia"), new City("Nova Metalkova")))))), + services, Path.of(SampleModelData.class.getResource("/images/picardProfile.jpg").toURI())); CAPTAINS = List.of(PICARD); + ENTERPRISE = new Ship("USS Enterprise", PICARD, 5, services, LocalDate.now()); } catch (URISyntaxException e) { throw new RuntimeException(e); } diff --git a/src/test/java/com/docutools/jocument/sample/model/Ship.java b/src/test/java/com/docutools/jocument/sample/model/Ship.java new file mode 100644 index 00000000..7a16f820 --- /dev/null +++ b/src/test/java/com/docutools/jocument/sample/model/Ship.java @@ -0,0 +1,7 @@ +package com.docutools.jocument.sample.model; + +import java.time.LocalDate; +import java.util.List; + +public record Ship(String name, Captain captain, int crew, List services, LocalDate built) { +}