Skip to content

Commit

Permalink
Implement reflection resolving for records
Browse files Browse the repository at this point in the history
Apache BeanUtils only works with Classes, not
with records.
This commit adds the logic to deal with
records handed to the ReflectionResolver.
  • Loading branch information
AntonOellerer committed Dec 14, 2021
1 parent b6194f9 commit 207ac24
Show file tree
Hide file tree
Showing 5 changed files with 79 additions and 22 deletions.
2 changes: 1 addition & 1 deletion build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ plugins {
}

group 'com.docutools'
version = '1.2.13'
version = '1.3.0'

sourceCompatibility = 17
targetCompatibility = 17
Expand Down
31 changes: 24 additions & 7 deletions src/main/java/com/docutools/jocument/impl/ReflectionResolver.java
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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;
Expand Down Expand Up @@ -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) {
Expand Down Expand Up @@ -126,8 +127,9 @@ public Optional<PlaceholderData> resolve(String placeholderName, Locale locale)
logger.debug("Trying to resolve placeholder {}", placeholderName);
Optional<PlaceholderData> result = Optional.empty();
for (String property : placeholderName.split("\\.")) {
result = result.isEmpty() ? doResolve(property, locale) :
result
result = result.isEmpty()
? doResolve(property, locale)
: result
.flatMap(r -> r.stream().findAny())
.flatMap(r -> r.resolve(property, locale));
}
Expand All @@ -139,7 +141,7 @@ private Optional<PlaceholderData> 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();
}
Expand All @@ -161,9 +163,9 @@ private Optional<PlaceholderData> 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) {
Expand All @@ -178,6 +180,21 @@ private Optional<PlaceholderData> 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<PlaceholderData> formatTemporal(String placeholderName, Temporal time, Locale locale) {
Optional<DateTimeFormatter> formatter;
if (isFieldAnnotatedWith(bean.getClass(), placeholderName, Format.class)) {
Expand Down
47 changes: 38 additions & 9 deletions src/test/java/com/docutools/jocument/ReflectionResolving.java
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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"));
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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<Captain> 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);
}
Expand Down
7 changes: 7 additions & 0 deletions src/test/java/com/docutools/jocument/sample/model/Ship.java
Original file line number Diff line number Diff line change
@@ -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<Service> services, LocalDate built) {
}

0 comments on commit 207ac24

Please sign in to comment.