AccessorChainMutator.java
package org.codefilarete.reflection;
import java.lang.reflect.Method;
import java.util.ArrayList;
import java.util.List;
import org.codefilarete.tool.Reflections;
import org.codefilarete.tool.StringAppender;
import org.codefilarete.tool.ThreadLocals;
import org.codefilarete.tool.VisibleForTesting;
import org.codefilarete.tool.collection.Iterables;
import org.codefilarete.tool.function.ThrowingRunnable;
/**
* @param <C> source bean type
* @param <X> last mutator bean type
* @param <T> value type to be set
* @author Guillaume Mary
*/
public class AccessorChainMutator<C, X, T> extends AccessorChain<C, X> implements ReversibleMutator<C, T> {
/**
* Keeps track of the mutator that returned null during the {@link #get(Object)} phase, if any.
* Will be used to give better exception message when {@link NullPointerException} will be thrown by {@link #set(Object, Object)}.
* Using a ThreadLocal is quite an overkill design, but can't find a less intrusive & thread-safe way to do it.
*/
@SuppressWarnings("java:S5164" /* remove() will be invoked by AutoRemoveThreadLocal while using ThreadLocals.doWithThreadLocal(..) */)
private static final ThreadLocal<Accessor> CURRENT_NULL_RETURNING_MUTATOR = new ThreadLocal<>();
private final Mutator<X, T> mutator;
public AccessorChainMutator(List<? extends Accessor<?, ?>> accessors, Mutator<X, T> mutator) {
super(accessors);
this.mutator = mutator;
}
public AccessorChainMutator(AccessorChain<C, X> accessors, Mutator<X, T> mutator) {
super(accessors.getAccessors());
this.mutator = mutator;
}
public Mutator<X, T> getMutator() {
return mutator;
}
@Override
public void set(C c, T t) {
final Object[] finalTarget = new Object[1];
ThreadLocals.doWithThreadLocal(CURRENT_NULL_RETURNING_MUTATOR, () -> null, (ThrowingRunnable<NullPointerException>) () -> {
X target = get(c); // Warn : this line changes state of CURRENT_NULL_RETURNING_MUTATOR by invoking onNullValue()
if (target == null) {
throwNullPointerException(c);
}
finalTarget[0] = target;
});
mutator.set((X) finalTarget[0], t);
}
/**
* Overridden to keep track of the culprit mutator that returned null
*
* @param targetBean bean on which accessor was invoked
* @param accessor accessor that returned null when invoked on targetBean
* @return super.onNullValue(targetBean, accessor)
*/
@Override
protected Object onNullValue(Object targetBean, Accessor accessor) {
CURRENT_NULL_RETURNING_MUTATOR.set(accessor);
return super.onNullValue(targetBean, accessor);
}
private void throwNullPointerException(Object srcBean) {
String accessorDescription = new AccessorPathBuilder().ccat(getAccessors(), ".").toString();
Accessor nullReturningMutator = CURRENT_NULL_RETURNING_MUTATOR.get();
List<Accessor<?, ?>> pathToNullPointerException = Iterables.head(getAccessors(), nullReturningMutator);
pathToNullPointerException.add(nullReturningMutator);
String nullProviderDescription = new AccessorPathBuilder().ccat(pathToNullPointerException, ".").toString();
throw new NullPointerException("Call of " + accessorDescription + " on " + srcBean + " returned null, because "
+ nullProviderDescription + " returned null");
}
/**
* Only supported when last mutator is reversible (aka implements {@link ReversibleMutator}.
*
* @return a new chain which path is the same as this
* @throws UnsupportedOperationException if last mutator is not reversible
*/
@Override
public AccessorChain<C, T> toAccessor() {
if (mutator instanceof ReversibleMutator) {
List<Accessor<?, ?>> newAccessors = new ArrayList<>(getAccessors());
newAccessors.add(((ReversibleMutator) mutator).toAccessor());
return new AccessorChain<>(newAccessors);
} else {
throw new UnsupportedOperationException(
"Last mutator cannot be reverted because it doesn't implement " + Reflections.toString(ReversibleAccessor.class) + ": " + mutator);
}
}
@Override
public boolean equals(Object other) {
return super.equals(other) && this.mutator.equals(((AccessorChainMutator) other).mutator);
}
@Override
public int hashCode() {
return 31 * super.hashCode() + this.mutator.hashCode();
}
/**
* Overridden to take mutator into account
* @return getters and final setter aggregated
*/
@Override
public String getDescription() {
// NB: arrow mark is totally arbitrary and is only here to distinguish mutator from accessor part
return super.getDescription() + " <- " + this.mutator.toString();
}
@Override
public String toString() {
return getDescription();
}
/**
* Aimed at giving a simple and readable description of a collection of accessor
*/
@VisibleForTesting
static class AccessorPathBuilder extends StringAppender {
@Override
public StringAppender cat(Object o) {
if (o instanceof AccessorByMember) {
super.cat(((AccessorByMember) o).getGetter().getName());
if (((AccessorByMember) o).getGetter() instanceof Method) {
super.cat("(");
if (o instanceof ListAccessor) {
super.cat(((ListAccessor) o).getIndex());
} else {
if (((Method) ((AccessorByMember) o).getGetter()).getParameterCount() > 0) {
// we don't need a perfect description for our case (exception message) so we shortcut method parameters
super.cat("..");
}
}
super.cat(")");
}
return this;
} else if (o instanceof AccessorByMethodReference) {
super.cat(((AccessorByMethodReference) o).getMethodName() + "()");
return this;
} else if (o instanceof ArrayAccessor) {
return super.cat("[" + ((ArrayAccessor) o).getIndex() + "]");
} else {
return super.cat(o);
}
}
}
}