Skip to content

Commit

Permalink
Add new "DynamicAccess" Annotation for RR
Browse files Browse the repository at this point in the history
To allow the `ReflectionResolver` to resolve objects dynamically
(meaning that they could not be resolved at compilation time),
a new `DynamicAccessPlaceholder` Annotation is added.
Methods annotated by it return an `Optional<Object>`, which is
then used by the resolver to create a new `PlaceholderData` for
further resolving.
  • Loading branch information
AntonOellerer committed May 22, 2024
1 parent be947cc commit 7bd6aed
Show file tree
Hide file tree
Showing 6 changed files with 135 additions and 24 deletions.
10 changes: 6 additions & 4 deletions build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ plugins {
}

group 'com.docutools'
version = '1.7.0'
version = '1.8.0'

java {
toolchain {
Expand Down Expand Up @@ -80,10 +80,12 @@ test {
useJUnitPlatform()
}

task automatedTests(type: Test) {
tasks.register('automatedTests', Test) {
testClassesDirs = testing.suites.test.sources.output.classesDirs
classpath = testing.suites.test.sources.runtimeClasspath
environment 'DT_JT_RR_PLACEHOLDER_MAPPINGS', 'src/test/resources/mappings.txt'
useJUnitPlatform{
includeTags "automated"
useJUnitPlatform {
includeTags 'automated'
}
}

Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
package com.docutools.jocument.annotations;

import com.docutools.jocument.impl.models.MatchPlaceholderData;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.util.Locale;
Expand All @@ -11,8 +12,7 @@
*
* <p>{@link MatchPlaceholder}-annotated methods precede any other properties.
*
* <p>Can be applied to a public method taking a {@link String} and an optional {@link java.util.Locale} as second parameter, returning an {@link
* java.util.Optional} of {@link String}.
* <p>Can be applied to a public method taking a {@link MatchPlaceholderData}, returning an {@link java.util.Optional} of {@link String}.
*
* @author amp
* @since 2022-03-01
Expand Down
96 changes: 78 additions & 18 deletions src/main/java/com/docutools/jocument/impl/ReflectionResolver.java
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
import com.docutools.jocument.PlaceholderData;
import com.docutools.jocument.PlaceholderMapper;
import com.docutools.jocument.PlaceholderResolver;
import com.docutools.jocument.annotations.DynamicAccessPlaceholder;
import com.docutools.jocument.annotations.Format;
import com.docutools.jocument.annotations.Image;
import com.docutools.jocument.annotations.MatchPlaceholder;
Expand Down Expand Up @@ -107,8 +108,11 @@ private static boolean isFieldAnnotatedWith(Class<?> clazz, String fieldName, Cl
try {
return clazz.getDeclaredField(fieldName)
.getDeclaredAnnotation(annotation) != null;
} catch (Exception e) {
logger.debug("Class %s not annotated with %s".formatted(clazz, fieldName), e);
} catch (NoSuchFieldException e) {
logger.debug("Class %s does not have field %s".formatted(clazz, fieldName));
return false;
} catch (SecurityException e) {
logger.warn(e);
return false;
}
}
Expand Down Expand Up @@ -183,14 +187,32 @@ private String strip(String placeholderName) {
return placeholder.substring(0, placeholder.length() - 1);
}

private static boolean validateDynamicAccessMethod(Method method) {
var returnType = method.getReturnType();
if (!returnType.equals(Optional.class)) {
if (method.getParameterCount() != 1) {
if (!method.getParameterTypes()[0].isAssignableFrom(MatchPlaceholderData.class)) {
logger.warn("@DynamicAccessPlaceholder: parameter should be assignable to MatchPlaceholderData");
return false;
}
logger.warn("@DynamicAccessPlaceholder: method should only expect one MatchPlaceholderData");
return false;
}
logger.warn("@DynamicAccessPlaceholder: method {} must return a java.util.Optional but returns {}.", method, returnType);
return false;
}
return true;
}

private Optional<PlaceholderData> resolveStripped(Locale locale, String placeholder) {
return matchPattern(placeholder, locale)
.or(() -> dynamicAccess(placeholder, locale))
.or(() -> resolveFieldAccessor(placeholder, locale))
.or(() -> tryResolveInParent(placeholder, locale));
}

private Optional<PlaceholderData> matchPattern(String placeholderName, Locale locale) {
return findMethod(placeholderMapper.tryToMap(placeholderName))
return findMatchPlaceholderMethod(placeholderMapper.tryToMap(placeholderName))
.flatMap(method -> {
var returnType = method.getReturnType();
if (returnType.equals(Optional.class)) {
Expand Down Expand Up @@ -234,13 +256,45 @@ private Optional<PlaceholderData> matchPattern(String placeholderName, Locale lo
.map(ScalarPlaceholderData::new);
}

private Optional<Method> findMethod(String placeholderName) {
private Optional<Method> findMatchPlaceholderMethod(String placeholderName) {
var beanClass = bean.getClass();
return Arrays.stream(beanClass.getMethods()).filter(
method -> Optional.ofNullable(method.getAnnotation(MatchPlaceholder.class)).map(MatchPlaceholder::pattern).filter(placeholderName::matches)
.isPresent()).findFirst();
}

private Optional<PlaceholderData> dynamicAccess(String placeholderName, Locale locale) {
return findDynamicAccessMethod(placeholderMapper.tryToMap(placeholderName))
.filter(ReflectionResolver::validateDynamicAccessMethod)
.flatMap(method -> {
try {
var returnValue = method.invoke(bean, new MatchPlaceholderData(placeholderName, locale, options));
if (returnValue instanceof Optional<?> returnOptional) {
if (returnOptional.isPresent()) {
return toPlaceholderData(placeholderName, locale, returnOptional.get());
} else {
return Optional.empty();
}
} else {
logger.warn("@DynamicAccessPlaceholder: method {} does not return a java.util.Optional!", method);
return Optional.empty();
}
} catch (InvocationTargetException | IllegalAccessException e) {
logger.warn(e);
return Optional.empty();
}
});
}

private Optional<Method> findDynamicAccessMethod(String placeholderName) {
var beanClass = bean.getClass();
return Arrays.stream(beanClass.getMethods()).filter(
method -> Optional.ofNullable(method.getAnnotation(DynamicAccessPlaceholder.class)).map(DynamicAccessPlaceholder::pattern)
.filter(placeholderName::matches)
.isPresent()).findFirst();
}


private Optional<String> evaluateSingleParameterFunction(String placeholderName, Method method)
throws IllegalAccessException, InvocationTargetException {
var parameterTypes = method.getParameterTypes();
Expand Down Expand Up @@ -336,7 +390,25 @@ private Optional<PlaceholderData> doReflectiveResolve(String placeholderName, Lo
if (wrappedProperty.isEmpty()) {
return Optional.of(new ScalarPlaceholderData<>(null));
}
var property = resolveNonFinalValue(wrappedProperty.get(), placeholderName);
return toPlaceholderData(placeholderName, locale, wrappedProperty.get());
} catch (NoSuchMethodException | IllegalArgumentException e) {
logger.debug("Did not find placeholder {}, {}", placeholderName, e.getMessage());
return Optional.empty();
} catch (IllegalAccessException | InvocationTargetException e) {
logger.error("Could not call method of placeholder %s".formatted(placeholderName), e);
return Optional.empty();
} catch (InstantiationException e) {
logger.warn("InstantiationException when resolving custom placeholder %s".formatted(placeholderName), e);
return Optional.empty();
} catch (ClassCastException e) {
logger.warn("ClassCastException when resolving custom placeholder %s".formatted(placeholderName), e);
return Optional.empty();
}
}

private Optional<PlaceholderData> toPlaceholderData(String placeholderName, Locale locale, Object wrappedProperty) {
try {
var property = resolveNonFinalValue(wrappedProperty, placeholderName);
var simplePlaceholder = resolveSimplePlaceholder(property, placeholderName, locale, options);
if (simplePlaceholder.isPresent()) {
logger.debug("Placeholder {} resolved to simple placeholder", placeholderName);
Expand All @@ -357,17 +429,7 @@ private Optional<PlaceholderData> doReflectiveResolve(String placeholderName, Lo
return Optional.of(new IterablePlaceholderData(List.of(new ReflectionResolver(bean, customPlaceholderRegistry, options, this)), 1));
} else {
return Optional.of(new IterablePlaceholderData(List.of(new ReflectionResolver(property, customPlaceholderRegistry, options, this)), 1));

}
} catch (NoSuchMethodException | IllegalArgumentException e) {
logger.debug("Did not find placeholder {}, {}", placeholderName, e.getMessage());
return Optional.empty();
} catch (IllegalAccessException | InvocationTargetException e) {
logger.error("Could not call method of placeholder %s".formatted(placeholderName), e);
return Optional.empty();
} catch (InstantiationException e) {
logger.warn("InstantiationException when resolving custom placeholder %s".formatted(placeholderName), e);
return Optional.empty();
} catch (InterruptedException e) {
logger.warn("InterruptedException when waiting for Future placeholder %s".formatted(placeholderName), e);
Thread.currentThread().interrupt();
Expand All @@ -381,10 +443,8 @@ private Optional<PlaceholderData> doReflectiveResolve(String placeholderName, Lo
} catch (EmptyOptionalException e) {
logger.warn("Placeholder {} property is an empty optional", e.getMessage());
return Optional.empty();
} catch (ClassCastException e) {
logger.warn("ClassCastException when resolving custom placeholder %s".formatted(placeholderName), e);
return Optional.empty();
}

}

private Optional<PlaceholderData> resolveSimplePlaceholder(Object property, String placeholderName, Locale locale, GenerationOptions options) {
Expand Down
21 changes: 21 additions & 0 deletions src/test/java/com/docutools/jocument/ReflectionResolvingTests.java
Original file line number Diff line number Diff line change
Expand Up @@ -3,9 +3,11 @@
import static org.hamcrest.MatcherAssert.assertThat;
import static org.hamcrest.Matchers.contains;
import static org.hamcrest.Matchers.equalTo;
import static org.hamcrest.Matchers.instanceOf;
import static org.hamcrest.Matchers.is;

import com.docutools.jocument.impl.CustomPlaceholderRegistryImpl;
import com.docutools.jocument.impl.IterablePlaceholderData;
import com.docutools.jocument.impl.ReflectionResolver;
import com.docutools.jocument.sample.model.Person;
import com.docutools.jocument.sample.model.SampleModelData;
Expand Down Expand Up @@ -295,4 +297,23 @@ void shouldResolveNullToEmptyString() {
assertThat(name.get().toString(), equalTo(""));
}

@Test
void resolvesDynamicAccessPlaceholder() {
Person picardPerson = SampleModelData.PICARD_NULL;
picardPerson.setFavouriteShip(SampleModelData.ENTERPRISE);
var resolver = new ReflectionResolver(picardPerson);

var returnObject = resolver.resolve("last-used-ship");

assertThat(returnObject, instanceOf(Optional.class));
var shipOptional = (Optional<?>) returnObject;
assertThat(shipOptional.isPresent(), is(true));
assertThat(shipOptional.get(), instanceOf(IterablePlaceholderData.class));
var shipPlaceholderData = ((IterablePlaceholderData) shipOptional.get());
var first = shipPlaceholderData.stream().findFirst();
assertThat(first.isPresent(), is(true));
Optional<PlaceholderData> nameOptionalPlaceholder = first.get().resolve("name");
assertThat(nameOptionalPlaceholder.isPresent(), is(true));
assertThat(nameOptionalPlaceholder.get().toString(), is(SampleModelData.ENTERPRISE.name()));
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -492,4 +492,24 @@ void shouldResolveCustomPlaceholderInCustomPlaceholderData() throws InterruptedE
assertThat(documentWrapper.bodyElement(0).asParagraph().run(0).text(),
equalTo("Live your life not celebrating victories, but overcoming defeats."));
}

@Test
void dynamicAccess() throws InterruptedException, IOException {
// assemble
Template template = Template.fromClassPath("/templates/word/DynamicAccess.docx")
.orElseThrow();
SampleModelData.PICARD_PERSON.setFavouriteShip(SampleModelData.ENTERPRISE);
PlaceholderResolver resolver = new ReflectionResolver(SampleModelData.PICARD_PERSON);

// act
Document document = template.startGeneration(resolver);
document.blockUntilCompletion(60000L); // 1 minute

// assert
assertThat(document.completed(), is(true));
xwpfDocument = TestUtils.getXWPFDocumentFromDocument(document);
var documentWrapper = new XWPFDocumentWrapper(xwpfDocument);
assertThat(documentWrapper.bodyElement(0).asParagraph().run(0).text(), equalTo(SampleModelData.ENTERPRISE.name()));
}

}
8 changes: 8 additions & 0 deletions src/test/java/com/docutools/jocument/sample/model/Person.java
Original file line number Diff line number Diff line change
@@ -1,11 +1,14 @@
package com.docutools.jocument.sample.model;

import com.docutools.jocument.annotations.DynamicAccessPlaceholder;
import com.docutools.jocument.annotations.Format;
import com.docutools.jocument.annotations.Translatable;
import com.docutools.jocument.impl.models.MatchPlaceholderData;
import java.time.Instant;
import java.time.LocalDate;
import java.time.Period;
import java.time.ZoneOffset;
import java.util.Optional;
import java.util.UUID;

public class Person {
Expand Down Expand Up @@ -61,4 +64,9 @@ public Ship getFavouriteShip() {
public UUID getId() {
return id;
}

@DynamicAccessPlaceholder(pattern = "last-used-ship")
public Optional<Ship> getLastUsedShip(MatchPlaceholderData matchPlaceholderData) {
return Optional.ofNullable(this.favouriteShip);
}
}

0 comments on commit 7bd6aed

Please sign in to comment.