Skip to content

Commit

Permalink
Merge pull request #493 from exadel-inc/feature/EAK-492
Browse files Browse the repository at this point in the history
[EAK-492] Support for stringified properties in injectors
  • Loading branch information
smiakchilo authored Nov 16, 2023
2 parents 0a1a479 + 59b4ee6 commit 655a669
Show file tree
Hide file tree
Showing 43 changed files with 743 additions and 152 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@
import java.lang.reflect.Member;
import java.lang.reflect.Type;
import java.util.Objects;
import java.util.function.BiFunction;
import javax.annotation.CheckForNull;
import javax.annotation.Nonnull;

Expand Down Expand Up @@ -91,6 +92,19 @@ public final Object getValue(
@Nonnull
abstract Injectable getValue(Object adaptable, String name, Type type, T annotation);

/**
* When overridden in an injector class, retrieves a custom routine used to convert a provided value (usually the
* value of the {@link Default} annotation) to the needs of the current injector. This method only needs overriding
* when the injector supports conversion not covered by {@link CastUtil} (i.e., when conversion depends on the
* type of the injectable or an annotation parameter)
* @param type Type of the receiving Java class member
* @param annotation Annotation handled by the current injector
* @return A nullable {@code BiFunction} instance
*/
BiFunction<Object, Type, Object> getValueConverter(Type type, T annotation) {
return null;
}

/**
* When overridden in an injector class, retrieves the annotation processed by this particular injector. Takes into
* account that there might be several annotations attached to the current Java class member, and an injector can
Expand All @@ -109,15 +123,18 @@ public final Object getValue(
* @param element {@link AnnotatedElement} instance that facades the Java class member and allows retrieving
* @return A nullable value
*/
static Object defaultIfEmpty(Injectable source, Type type, AnnotatedElement element) {
final Object defaultIfEmpty(Injectable source, Type type, AnnotatedElement element) {
if (source != null && !source.isDefault()) {
return source.getValue();
}
if (!element.isAnnotationPresent(Default.class)) {
return source != null ? source.getValue() : null;
}
Object defaultValue = extractDefault(element.getDeclaredAnnotation(Default.class));
return CastUtil.toType(defaultValue, type).getValue();
return CastUtil.toType(
defaultValue,
type,
getValueConverter(type, getManagedAnnotation(element))).getValue();
}

/**
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@
import java.util.function.BiFunction;
import javax.annotation.Nonnull;

import org.apache.commons.lang3.ClassUtils;
import org.apache.commons.lang3.StringUtils;
import org.apache.sling.api.SlingHttpServletRequest;
import org.apache.sling.api.resource.Resource;
Expand Down Expand Up @@ -105,11 +106,30 @@ Object getValue(Object adaptable, String name, String valueMember, Type type) {
return valueMapValue;
}

BiFunction<Object, Type, Object> converter = valueMember.isEmpty()
if (componentType == null) {
return CastUtil.toType(valueMapValue, type);
}

return CastUtil.toType(valueMapValue, type, getValueConverter(valueMember));
}

/**
* {@inheritDoc}
*/
@Override
BiFunction<Object, Type, Object> getValueConverter(Type type, EnumValue annotation) {
return getValueConverter(annotation.valueMember());
}

/**
* Picks up an enum-producing value converter based on the provided {@code valueMember}
* @param valueMember The name of the enum's method or public field that we use for finding a match
* @return A {@code BiFunction} instance
*/
private BiFunction<Object, Type, Object> getValueConverter(String valueMember) {
return valueMember.isEmpty()
? EnumValueInjector::getEnumValue
: (value, t) -> getEnumValue(value, t, valueMember);

return CastUtil.toType(valueMapValue, type, converter);
}

/**
Expand All @@ -120,6 +140,9 @@ Object getValue(Object adaptable, String name, String valueMember, Type type) {
*/
@SuppressWarnings("unchecked")
private static Object getEnumValue(Object value, Type type) {
if (!ClassUtils.isAssignable((Class<?>) type, Enum.class)) {
return null;
}
Class<? extends Enum<?>> enumType = (Class<? extends Enum<?>>) type;
return Arrays.stream(enumType.getEnumConstants())
.filter(constant -> StringUtils.equalsAnyIgnoreCase(value.toString(), constant.name(), constant.toString()))
Expand All @@ -137,6 +160,9 @@ private static Object getEnumValue(Object value, Type type) {
*/
@SuppressWarnings("unchecked")
private static Object getEnumValue(Object value, Type type, String memberName) {
if (!ClassUtils.isAssignable((Class<?>) type, Enum.class)) {
return null;
}
Class<? extends Enum<?>> enumType = (Class<? extends Enum<?>>) type;
return Arrays.stream(enumType.getEnumConstants())
.filter(constant -> isMatch(constant, memberName, value.toString()))
Expand All @@ -161,7 +187,7 @@ private static boolean isMatch(Object enumConstant, String memberName, String va
}

/**
* Retrieves the value of a method from an enum constant object without throwing ex exception
* Retrieves the value of a method from an enum constant object without throwing an exception
* @param value The enum constant whose method invocation result is being retrieved
* @param name The name of the method that we want to invoke
* @return A string value if was able to find the requested method and invoke it; otherwise, {@code null}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -79,12 +79,16 @@ public static Injectable fallback(Object value) {
}

/**
* Gets whether the provided object is not a {@code Defaultable} instance in the "fallback" state> This is usually
* needed to validate an injectable value
* Gets whether the provided object is not a {@code Defaultable} instance in the "fallback" state. A {@code null} is
* considered a default value as well. This check is usually needed to place a value into an injectable array or
* collection
* @param value Value to check
* @return True or false
*/
public static boolean isNotDefault(Object value) {
if (value == null) {
return false;
}
return !(value instanceof Injectable) || !((Injectable) value).isDefault();
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -59,7 +59,7 @@ private CastUtil() {
*/
@Nonnull
public static Injectable toType(Object value, Type type) {
return toType(value, type, CastUtil::toInstanceOfType);
return toType(value, type, null);
}

/**
Expand All @@ -78,22 +78,26 @@ public static Injectable toType(Object value, Type type, BiFunction<Object, Type
if (value == null) {
return Injectable.EMPTY;
}

Class<?> elementType = TypeUtil.getElementType(type);
BiFunction<Object, Type, Object> effectiveConverter = converter != null ? converter : CastUtil::toInstanceOfType;

if (TypeUtil.isArray(type)) {
return Injectable.of(toArray(value, elementType, converter));
return Injectable.of(toArray(value, elementType, effectiveConverter));
}

if (TypeUtil.isSupportedCollection(type, true)) {
return Set.class.equals(TypeUtil.getRawType(type))
? Injectable.of(toCollection(value, elementType, converter, LinkedHashSet::new))
: Injectable.of(toCollection(value, elementType, converter, ArrayList::new));
? Injectable.of(toCollection(value, elementType, effectiveConverter, LinkedHashSet::new))
: Injectable.of(toCollection(value, elementType, effectiveConverter, ArrayList::new));
}

if (Object.class.equals(type)) {
return Injectable.of(converter.apply(value, type));
return Injectable.of(effectiveConverter.apply(value, type));
}
return Injectable.of(converter.apply(extractFirstElement(value), type));

Object convertable = ClassUtils.isAssignable((Class<?>) type, String.class) ? value : extractFirstElement(value);
return Injectable.of(effectiveConverter.apply(convertable, type));
}

/**
Expand Down Expand Up @@ -175,6 +179,33 @@ private static Object extractFirstElement(Object source) {
return source;
}

/**
* Called by {@code CastUtil#toType} to stringify the given value. Arrays and iterable collections are converted
* to a comma-separated string
* @param value An arbitrary non-null value
* @return String value
*/
private static String toString(Object value) {
if (value.getClass().isArray()) {
StringBuilder result = new StringBuilder();
for (int i = 0, length = Array.getLength(value); i < length; i++) {
Object entry = Array.get(value, i);
if (entry == null) {
continue;
}
if (result.length() > 0) {
result.append(CoreConstants.SEPARATOR_COMMA);
}
result.append(entry);
}
return result.toString();
}
if (TypeUtil.isSupportedCollection(value.getClass(), false)) {
return StringUtils.join((Collection<?>) value, CoreConstants.SEPARATOR_COMMA);
}
return String.valueOf(value);
}

/**
* Called by {@code CastUtil#toType} to further adapt the passed value to the given type if there is type
* compatibility. E.g., when an {@code int} value is passed, and the receiving type is {@code long}, or else the
Expand All @@ -187,7 +218,7 @@ private static Object extractFirstElement(Object source) {
* original one if type casting is not possible or not needed
*/
private static Injectable toInstanceOfType(Object value, Type type) {
if (value == null || value.getClass().equals(type) || type instanceof ParameterizedType) {
if (TypeUtil.isEmpty(value) || value.getClass().equals(type) || type instanceof ParameterizedType) {
return Injectable.of(value);
}
assert type instanceof Class<?>;
Expand All @@ -213,6 +244,9 @@ private static Injectable toInstanceOfType(Object value, Type type) {
if (ClassUtils.isAssignable(effectiveValue.getClass(), (Class<?>) type)) {
return Injectable.of(((Class<?>) type).cast(effectiveValue));
}
if (ClassUtils.isAssignable((Class<?>) type, String.class)) {
return Injectable.of(toString(value));
}
return getDefaultValue(type);
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -96,6 +96,8 @@ public Object getValue(
StringUtils.EMPTY,
type));
}
return BaseInjector.defaultIfEmpty(value, type, annotatedElement);
return delegate instanceof BaseInjector
? ((BaseInjector<?>) delegate).defaultIfEmpty(value, type, annotatedElement)
: null;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,7 @@ private void shouldInjectEnumConstant(Adaptable adaptable) {
assertEquals(EXPECTED_COLOR.name(), model.getObjectValue());
assertEquals(EXPECTED_COLOR, model.getValueSupplier().getValue());
assertEquals(EXPECTED_COLOR, model.getConstructorValue());
assertEquals(EXPECTED_COLOR, model.getDefaultValue());
}

@Test
Expand All @@ -75,6 +76,7 @@ private void shouldInjectEnumArray(Adaptable adaptable) {
assertEquals(EXPECTED_COLOR.name(), model.getObjectValue());
assertArrayEquals(EXPECTED_COLOR_ARRAY, model.getValueSupplier().getValue());
assertArrayEquals(EXPECTED_COLOR_ARRAY, model.getConstructorValue());
assertArrayEquals(EXPECTED_COLOR_ARRAY, model.getDefaultValue());
}

@Test
Expand All @@ -93,6 +95,7 @@ private void shouldInjectEnumCollection(Adaptable adaptable) {
assertTrue(CollectionUtils.isEqualCollection(EXPECTED_COLOR_COLLECTION, model.getValueSupplier().getValue()));
assertNotNull(model.getConstructorValue());
assertTrue(CollectionUtils.isEqualCollection(EXPECTED_COLOR_COLLECTION, model.getConstructorValue()));
assertTrue(CollectionUtils.isEqualCollection(EXPECTED_COLOR_COLLECTION, model.getDefaultValue()));
}

@Test
Expand All @@ -107,5 +110,6 @@ private void shouldInjectEnumArrayViaCustomAccessor(Adaptable adaptable) {
assertArrayEquals(EXPECTED_COLOR_ARRAY, model.getValue());
assertArrayEquals(EXPECTED_COLOR_ARRAY, model.getValueSupplier().getValue());
assertArrayEquals(EXPECTED_COLOR_ARRAY, model.getConstructorValue());
assertArrayEquals(EXPECTED_COLOR_ARRAY, model.getDefaultValue());
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -89,6 +89,11 @@ public void shouldInjectStringCollection() {
super.shouldInjectStringCollection();
}

@Test
public void shouldInjectDefaultStringCollection() {
super.shouldInjectDefaultStringCollection();
}

@Test
public void shouldInjectInteger() {
super.shouldInjectInteger(RequestAttributeInjectorTest::assertStringifiedObjectValueEquals);
Expand All @@ -114,6 +119,11 @@ public void shouldInjectIntegerCollection() {
super.shouldInjectIntegerCollection();
}

@Test
public void shouldInjectDefaultIntegerCollection() {
super.shouldInjectDefaultIntegerCollection();
}

@Test
public void shouldInjectUnparseableIntegerCollection() {
super.shouldInjectUnparseableIntegerCollection();
Expand Down Expand Up @@ -144,6 +154,11 @@ public void shouldInjectLongCollection() {
super.shouldInjectLongCollection();
}

@Test
public void shouldInjectDefaultLongCollection() {
super.shouldInjectDefaultLongCollection();
}

@Test
public void shouldInjectUnparseableLongCollection() {
super.shouldInjectUnparseableLongCollection();
Expand Down Expand Up @@ -174,6 +189,11 @@ public void shouldInjectDoubleCollection() {
super.shouldInjectDoubleCollection();
}

@Test
public void shouldInjectDefaultDoubleCollection() {
super.shouldInjectDefaultDoubleCollection();
}

@Test
public void shouldInjectUnparseableDoubleCollection() {
super.shouldInjectUnparseableDoubleCollection();
Expand Down Expand Up @@ -204,6 +224,11 @@ public void shouldInjectBooleanCollection() {
super.shouldInjectBooleanCollection();
}

@Test
public void shouldInjectDefaultBooleanCollection() {
super.shouldInjectDefaultBooleanCollection();
}

@Test
public void shouldInjectToWideningType() {
GregorianCalendar calendar = new GregorianCalendar();
Expand Down
Loading

0 comments on commit 655a669

Please sign in to comment.