Skip to content

Commit

Permalink
Enable ReflectionResolver to resolve map keys
Browse files Browse the repository at this point in the history
This commit adds the functionality s.t.
`ReflectionResolver` can resolve placeholders
referencing map keys.
  • Loading branch information
AntonOellerer committed Apr 13, 2022
1 parent ae20b83 commit 0d27dcb
Show file tree
Hide file tree
Showing 8 changed files with 124 additions and 12 deletions.
1 change: 1 addition & 0 deletions build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@ dependencies {

implementation("com.google.code.gson:gson:2.8.9")
implementation("org.apache.tika:tika-core:2.2.1")
implementation("org.apache.commons:commons-lang3:3.12.0")

runtimeOnly group: 'org.apache.logging.log4j', name: 'log4j-core', version: '2.17.1'
implementation group: 'org.apache.logging.log4j', name: 'log4j-api', version: '2.17.1'
Expand Down
28 changes: 28 additions & 0 deletions src/main/java/com/docutools/jocument/MapPlaceholderData.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
package com.docutools.jocument;

import java.util.Map;
import java.util.stream.Stream;

public record MapPlaceholderData(Map<String, PlaceholderData> map)
implements PlaceholderData {

@Override
public PlaceholderType getType() {
return PlaceholderType.MAP;
}

@Override
public Stream<PlaceholderResolver> stream() {
return map.values().stream().filter(placeholderData -> placeholderData.getType().equals(PlaceholderType.SET)).flatMap(PlaceholderData::stream);
}

@Override
public long count() {
return map.size();
}

@Override
public Object getRawValue() {
return map;
}
}
6 changes: 5 additions & 1 deletion src/main/java/com/docutools/jocument/PlaceholderType.java
Original file line number Diff line number Diff line change
Expand Up @@ -21,5 +21,9 @@ public enum PlaceholderType {
* The transformation of the placeholder object is implemented by the {@link com.docutools.jocument.PlaceholderData}
* object.
*/
CUSTOM
CUSTOM,
/**
* A map of values where the placeholder can be resolved against the map keys.
*/
MAP,
}
Original file line number Diff line number Diff line change
Expand Up @@ -3,18 +3,21 @@
import com.docutools.jocument.CustomPlaceholderRegistry;
import com.docutools.jocument.GenerationOptions;
import com.docutools.jocument.GenerationOptionsBuilder;
import com.docutools.jocument.MapPlaceholderData;
import com.docutools.jocument.PlaceholderData;
import com.docutools.jocument.PlaceholderResolver;
import java.lang.reflect.InvocationTargetException;
import java.util.Collection;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import java.util.Optional;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.Future;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.TimeoutException;
import java.util.stream.Collectors;
import org.apache.commons.lang3.ClassUtils;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;

Expand Down Expand Up @@ -74,7 +77,20 @@ public Optional<PlaceholderData> doReflectiveResolve(String placeholderName, Loc
logger.debug("Placeholder {} resolved to simple placeholder", placeholderName);
return simplePlaceholder;
} else {
if (property instanceof Collection<?> collection) {
if (property instanceof Map<?, ?> valueMap) {
logger.debug("Placeholder {} resolved to map", placeholderName);
Map<String, PlaceholderData> dataMap = valueMap.entrySet()
.stream()
.map(entry -> {
var mapValue = entry.getValue();
var scalarType = ClassUtils.isPrimitiveOrWrapper(entry.getValue().getClass()) || mapValue instanceof String;
PlaceholderData placeholderData = scalarType ? new ScalarPlaceholderData<>(entry.getValue()) :
new IterablePlaceholderData(new ReflectionResolver(entry.getValue(), customPlaceholderRegistry, options, this));
return Map.entry(entry.getKey().toString().toLowerCase(), placeholderData);
})
.collect(Collectors.toMap(Map.Entry::getKey, Map.Entry::getValue));
return Optional.of(new MapPlaceholderData(dataMap));
} else if (property instanceof Collection<?> collection) {
logger.debug("Placeholder {} resolved to collection", placeholderName);
List<PlaceholderResolver> list = collection.stream()
.map(object -> new FutureReflectionResolver(object, customPlaceholderRegistry, options, maximumWaitTime))
Expand Down
42 changes: 37 additions & 5 deletions src/main/java/com/docutools/jocument/impl/ReflectionResolver.java
Original file line number Diff line number Diff line change
Expand Up @@ -3,9 +3,11 @@
import com.docutools.jocument.CustomPlaceholderRegistry;
import com.docutools.jocument.GenerationOptions;
import com.docutools.jocument.GenerationOptionsBuilder;
import com.docutools.jocument.MapPlaceholderData;
import com.docutools.jocument.PlaceholderData;
import com.docutools.jocument.PlaceholderMapper;
import com.docutools.jocument.PlaceholderResolver;
import com.docutools.jocument.PlaceholderType;
import com.docutools.jocument.annotations.Format;
import com.docutools.jocument.annotations.Image;
import com.docutools.jocument.annotations.MatchPlaceholder;
Expand All @@ -31,8 +33,11 @@
import java.util.Currency;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import java.util.Optional;
import java.util.stream.Collectors;
import org.apache.commons.beanutils.PropertyUtilsBean;
import org.apache.commons.lang3.ClassUtils;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;

Expand Down Expand Up @@ -72,7 +77,8 @@ public ReflectionResolver(Object value, CustomPlaceholderRegistry customPlacehol
* @param customPlaceholderRegistry The custom placeholder registry to check for custom placeholders
* @param parent The parent registry
*/
public ReflectionResolver(Object value, CustomPlaceholderRegistry customPlaceholderRegistry, GenerationOptions options, PlaceholderResolver parent) {
public ReflectionResolver(Object value, CustomPlaceholderRegistry customPlaceholderRegistry, GenerationOptions options,
PlaceholderResolver parent) {
this.bean = value;
this.customPlaceholderRegistry = customPlaceholderRegistry;
this.parent = parent;
Expand Down Expand Up @@ -225,15 +231,28 @@ private Optional<PlaceholderData> matchPattern(String placeholderName, Locale lo
private Optional<PlaceholderData> resolveAccessor(String placeholderName, Locale locale) {
Optional<PlaceholderData> result = Optional.empty();
for (String property : placeholderName.split("\\.")) {
if (bean instanceof Map<?, ?> map) {
var object = map.get(property);
if (object instanceof PlaceholderData placeholderData) {
result = Optional.of(placeholderData);
continue;
}
}
result = result.isEmpty()
? doReflectiveResolve(property, locale).or(() -> tryResolveInParent(placeholderName, locale))
: result
.flatMap(r -> r.stream().findAny())
.flatMap(r -> r.resolve(property, locale));
: result.map(placeholderData -> resolveChild(placeholderData, property, locale));
}
return result;
}

private PlaceholderData resolveChild(PlaceholderData placeholderData, String property, Locale locale) {
if (placeholderData.getType().equals(PlaceholderType.MAP)) {
return ((Map<String, PlaceholderData>) placeholderData.getRawValue()).get(property.toLowerCase());
} else {
return placeholderData.stream().findAny().flatMap(r -> r.resolve(property, locale)).orElse(null);
}
}

private Optional<PlaceholderData> tryResolveInParent(String placeholderName, Locale locale) {
return Optional.ofNullable(parent)
.flatMap(parentResolver -> parentResolver.resolve(placeholderName, locale));
Expand Down Expand Up @@ -268,7 +287,20 @@ public Optional<PlaceholderData> doReflectiveResolve(String placeholderName, Loc
logger.debug("Placeholder {} resolved to simple placeholder", placeholderName);
return simplePlaceholder;
} else {
if (property instanceof Collection<?> collection) {
if (property instanceof Map<?, ?> valueMap) {
logger.debug("Placeholder {} resolved to map", placeholderName);
Map<String, PlaceholderData> dataMap = valueMap.entrySet()
.stream()
.map(entry -> {
var mapValue = entry.getValue();
var scalarType = ClassUtils.isPrimitiveOrWrapper(entry.getValue().getClass()) || mapValue instanceof String;
PlaceholderData placeholderData = scalarType ? new ScalarPlaceholderData<>(entry.getValue()) :
new IterablePlaceholderData(new ReflectionResolver(entry.getValue(), customPlaceholderRegistry, options, this));
return Map.entry(entry.getKey().toString().toLowerCase(), placeholderData);
})
.collect(Collectors.toMap(Map.Entry::getKey, Map.Entry::getValue));
return Optional.of(new MapPlaceholderData(dataMap));
} else if (property instanceof Collection<?> collection) {
logger.debug("Placeholder {} resolved to collection", placeholderName);
List<PlaceholderResolver> list = collection.stream()
// cast is needed for `.toList()`
Expand Down
32 changes: 30 additions & 2 deletions src/test/java/com/docutools/jocument/ReflectionResolvingTests.java
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@
import java.time.LocalDate;
import java.time.format.DateTimeFormatter;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.stream.Collectors;
import org.junit.jupiter.api.BeforeEach;
Expand Down Expand Up @@ -184,11 +185,11 @@ void shouldResolveByIgnoreCaseRegex() {
assertThat(numberOfServices, equalTo(String.valueOf(SampleModelData.ENTERPRISE.services().size())));
}

@ValueSource(strings = {"name", "captain", "crew", "services"})
@ValueSource(strings = {"name", "captain", "crew", "services", "staff"})
@ParameterizedTest(name = "Resolve falsy condition for {0} on empty ship")
void shouldResolveFalsyCondition(String propertyName) {
// Assemble
var emptyShip = new Ship("", null, 0, List.of(), LocalDate.now());
var emptyShip = new Ship("", null, 0, List.of(), LocalDate.now(), Map.of());
var resolver = new ReflectionResolver(emptyShip);

// Act
Expand All @@ -199,4 +200,31 @@ void shouldResolveFalsyCondition(String propertyName) {
assertThat(emptyPlaceholderData.count(), is(0L));
}

@Test
@DisplayName("Resolve map key")
void shouldResolveMapKey() {
// Assemble
resolver = new ReflectionResolver(Map.of("technician", "Elsy Kachikian"));

// Act
var technician = resolver.resolve("technician")
.orElseThrow();

// Assert
assertThat(technician.getRawValue(), equalTo("Elsy Kachikian"));
}

@Test
@DisplayName("Resolve nested map key")
void shouldResolveNestedMapKey() {
// Assemble
resolver = new ReflectionResolver(SampleModelData.ENTERPRISE);

// Act
var cook = resolver.resolve("staff.cook")
.orElseThrow();

// Assert
assertThat(cook.getRawValue(), equalTo(SampleModelData.ENTERPRISE.staff().get("cook")));
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
import java.time.LocalDate;
import java.util.Collections;
import java.util.List;
import java.util.Map;
import java.util.concurrent.CompletableFuture;

public class SampleModelData {
Expand All @@ -18,6 +19,7 @@ public class SampleModelData {

static {
try {
var staff = Map.of("cook", "Puro Okin");
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(
Expand All @@ -30,14 +32,14 @@ public class SampleModelData {
services,
Path.of(SampleModelData.class.getResource("/images/picardProfile.jpg").toURI()));
CAPTAINS = List.of(PICARD);
ENTERPRISE = new Ship("USS Enterprise", PICARD, 5, services, LocalDate.now());
ENTERPRISE = new Ship("USS Enterprise", PICARD, 5, services, LocalDate.now(), staff);
FUTURE_PICARD = new FutureCaptain(CompletableFuture.completedFuture("Jean-Luc Picard"),
CompletableFuture.completedFuture(4),
CompletableFuture.completedFuture(Uniform.Red),
CompletableFuture.completedFuture(new FirstOfficer("Riker", 3, Uniform.Red)),
CompletableFuture.completedFuture(services),
CompletableFuture.completedFuture(Path.of(SampleModelData.class.getResource("/images/picardProfile.jpg").toURI())));
ENTERPRISE_WITHOUT_SERVICES = new Ship("USS Enterprise", PICARD, 5, Collections.emptyList(), LocalDate.now());
ENTERPRISE_WITHOUT_SERVICES = new Ship("USS Enterprise", PICARD, 5, Collections.emptyList(), LocalDate.now(), staff);
} catch (URISyntaxException e) {
throw new RuntimeException(e);
}
Expand Down
3 changes: 2 additions & 1 deletion src/test/java/com/docutools/jocument/sample/model/Ship.java
Original file line number Diff line number Diff line change
Expand Up @@ -4,11 +4,12 @@
import java.time.LocalDate;
import java.time.format.DateTimeFormatter;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;

public record Ship(String name, Captain captain, int crew, List<Service> services, LocalDate built) {
public record Ship(String name, Captain captain, int crew, List<Service> services, LocalDate built, Map<String, String> staff) {

private static final Logger log = LogManager.getLogger(Ship.class);

Expand Down

0 comments on commit 0d27dcb

Please sign in to comment.