Please wait. This can take some minutes ...
Many resources are needed to download a project. Please understand that we have to compensate our server costs. Thank you in advance.
Project price only 1 $
You can buy this project and download/modify it how often you want.
com.artnaseef.immutable.utils.MutationUtils Maven / Gradle / Ivy
Go to download
Utilities for creating and maintaining immutable classes.
/*
* Copyright (c) 2021 Arthur Naseef
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.artnaseef.immutable.utils;
import com.artnaseef.immutable.exception.CannotConstructException;
import com.artnaseef.immutable.exception.CannotMutateException;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.lang.reflect.Constructor;
import java.lang.reflect.Method;
import java.util.Collections;
import java.util.HashMap;
import java.util.LinkedHashMap;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import java.util.function.Function;
import java.util.function.Supplier;
import java.util.stream.Collectors;
import static com.artnaseef.immutable.utils.MutationResult.UNCHANGED_MUTATION_RESULT;
/**
* Utilities for mutating the state of complex immutable objects by copying existing objects where feasible, and
* constructing new ones in the hierarchy, as-needed, to implement desired mutations.
*
* LIMITATIONS:
* - All objects processed here are expected to be immutable and use immutable fields.
* - Fields in immutable objects are always declared final.
* - Fields in immutable objects are always immutable themselves.
* - Lists MUST be declared as List type (e.g. not ArrayList nor LinkedList)
* - The class declaration of all objects to be processed MUST include the MutationUtilsImmutableProperties.
* - The ORDER of the properties in the annotation MUST match the order they are provided to the Constructor.
*
* TODO:
* - Removal of list entries as a MutationResultType? REMOVE - sets fields to null, or drops from collections
* - Support for Map and Set built-ins
* - Use model object to carry around state of the tree walk
*/
@SuppressWarnings("unchecked")
public class MutationUtils {
private static final Logger DEFAULT_LOGGER = LoggerFactory.getLogger(MutationUtils.class);
private Logger log = DEFAULT_LOGGER;
private MutatorUtils mutatorUtils = new MutatorUtils();
/**
* Mutate the complex data structure given using the provided Mutator to choose the paths through the structure's
* hierarchy to visit and modify. Produces a copy of the original structure with minimal changes needed to produce
* the mutations, thereby minimizing the additional memory needed.
*
* For example, if a list with 1000 entries changes the element at position 3, a new list is created using the
* same entries for all 999 elements that didn't change, and the 1 new element that did change.
*
* @param root
* @param mutator
* @param
* @return
*/
public T mutateDeep(T root, Mutator mutator) {
return this.mutateDeep(new LinkedList<>(), new LinkedList<>(), root, mutator);
}
/**
* Mutate the complex data structure given using the provided Mutator to choose the paths through the structure's
* hierarchy to visit and modify. Produces a copy of the original structure with minimal changes needed to produce
* the mutations, thereby minimizing the additional memory needed. Takes the ancestry and property names accessed
* to get to the current node.
*
* For example, if a list with 1000 entries changes the element at position 3, a new list is created using the
* same entries for all 999 elements that didn't change, and the 1 new element that did change.
*
* The convenience version of this method is recommended unless there is a real reason to get in deep.
*
* @see #mutateDeep(Object, Mutator)
*
* @param ancestry list of ancestor objects (from the root) to the current node's immediate parent. Empty list means
* this node is the root node.
* @param propertyNames list of property names accessed to get to the current node. propertyNames.get(0) is the
* name of the field in ancestry.get(0) to get to ancestry.get(1). Empty list means this is the
* root node.
* @param node the current node being processed with the mutator.
* @param mutator node visitor that decides whether to mutate the current node, and whether to continue walking the
* children of this node.
* @param the type of the current node being processed.
* @return either the original node, or a copy of the node with mutated values populated. Either way, the result
* is an immutable object (assuming no "cheats").
*/
public T mutateDeep(LinkedList ancestry, LinkedList propertyNames, T node, Mutator mutator) {
if (node == null) {
return null;
}
MutationUtilsImmutableProperties mutationUtilsImmutableProperties = node.getClass().getAnnotation(MutationUtilsImmutableProperties.class);
//
// Class not marked with ImmutableProperties. Try built-in classes that are supported (e.g. List).
//
if (mutationUtilsImmutableProperties == null) {
return this.mutateDeepBuiltIn(ancestry, propertyNames, node, mutator);
}
//
// Walk the properties and check for mutation on each of them.
//
boolean changed = false;
Map> updateMapper = new HashMap<>();
Map childrenToWalkMap = new HashMap<>();
for (String propertyName : mutationUtilsImmutableProperties.properties()) {
boolean propertyChanged =
this.mutateProperty(
ancestry,
propertyNames,
node,
mutator,
updateMapper,
childrenToWalkMap,
propertyName);
if (propertyChanged) {
changed = true;
}
}
//
// Walk all of the children requested now.
//
for (Map.Entry childPropertyToWalk : childrenToWalkMap.entrySet()) {
boolean childChanged =
this.walkChildProperty(ancestry, propertyNames, node, mutator, changed, updateMapper, childPropertyToWalk);
if (childChanged) {
changed = true;
}
}
//
// If anything changed, construct a new instance with the updated values.
//
if (changed) {
return this.constructNewInstanceWithProperties((Class) node.getClass(), updateMapper);
}
// No Changes
return node;
}
/**
* Mutate a built-in node type using the given mutator. This method can be called directly, but generally calling
* mutateDeep is the right starting point.
*
* Note: currently only supports List types.
*
* @see #mutateDeep(Object, Mutator)
*
* @param ancestry list of ancestor objects (from the root) to the current node's immediate parent. Empty list means
* this node is the root node.
* @param propertyNames list of property names accessed to get to the current node. propertyNames.get(0) is the
* name of the field in ancestry.get(0) to get to ancestry.get(1). Empty list means this is the
* root node.
* @param node the current node being processed with the mutator.
* @param mutator node visitor that decides whether to mutate the current node, and whether to continue walking the
* children of this node.
* @param the type of the current node being processed.
* @return either the original node, or a copy of the node with mutated values populated. Either way, the result
* is an immutable object (assuming no "cheats").
*/
public T mutateDeepBuiltIn(LinkedList ancestry, LinkedList propertyNames, T node, Mutator mutator) {
if (node instanceof List) {
return (T) this.mutateDeepList(ancestry, propertyNames, (List) node, mutator);
}
this.log.error("Request to mutateDeep() on a class lacking ImmutableProperties and lacking built-in support; keeping existing value: class={}", node.getClass().getName());
return node;
}
/**
* Mutate a List node using the given mutator. This method can be called directly, but generally calling
* mutateDeep is the right starting point.
*
* NOTE: only List type elements are supported as the result is wrapped with Collection.unmodifiableList(). Model
* object list fields must support List to use these tools. Other forms of immutable lists are not supported.
*
* @see #mutateDeep(Object, Mutator)
*
* @param ancestry
* @param propertyNames
* @param node
* @param mutator
* @param
* @return
*/
public List mutateDeepList(LinkedList ancestry, LinkedList propertyNames, List node, Mutator mutator) {
boolean changed = false;
Map> updateMapper = new LinkedHashMap<>();
Map childrenToWalkMap = new LinkedHashMap<>();
int cur = 0;
while (cur < node.size()) {
T child = node.get(cur);
boolean elementChanged =
this.mutatePropertyWithSupplier(
ancestry,
propertyNames,
node,
mutator,
updateMapper,
childrenToWalkMap,
Integer.toString(cur),
() -> child
);
if (elementChanged) {
changed = true;
}
cur++;
}
//
// Walk all of the children requested now.
//
for (Map.Entry childPropertyToWalk : childrenToWalkMap.entrySet()) {
boolean childChanged =
this.walkChildProperty(ancestry, propertyNames, node, mutator, changed, updateMapper, childPropertyToWalk);
if (childChanged) {
changed = true;
}
}
if (changed) {
List resultList =
updateMapper.entrySet().stream()
.map((entry) -> (T) entry.getValue().get())
.collect(Collectors.toList())
;
return Collections.unmodifiableList(resultList);
}
return node;
}
/**
* Check the given path of ancestor objects and property names against the path to the current entry. Matches
* against the end of the path and not the beginning (unless, of course, the full path is specified). Don't be
* tricked by the more argument - keep alternating Class and String arguments. For example:
*
* checkPath(ancestry, propertyNames, StorageChest.class, "itemList", List.class, null, Item.class, "itemType")
*
*
* Passes through to the MutatorUtils method; see there for more information.
*
* @see MutatorUtils#checkPath(List, List, Class, String, Object...)
*/
public boolean checkPath(List ancestry, List propertyNames, Class earliestAncestorToCheck, String earliestPropertyNameToCheck, Object... more) {
return this.mutatorUtils.checkPath(ancestry, propertyNames, earliestAncestorToCheck, earliestPropertyNameToCheck, more);
}
/**
* Determine if the path specified by the given Classes and Property-Names is at least partially matched by the
* ancestry and property names of the current node, so we know whether to continue walking down the tree from this
* node to find a match. The path matching is anchored from the root node, so the entire match must apply from the
* root of the tree.
*
* Passes through to the MutatorUtils method; see there for more information.
*
* @see MutatorUtils#isInAnchoredPath(List, List, Class, String, Object...)
*/
public boolean isInAnchoredPath(List ancestry, List propertyNames, Class earliestAncestorToCheck, String earliestPropertyNameToCheck, Object... more) {
return this.mutatorUtils.isInAnchoredPath(ancestry, propertyNames, earliestAncestorToCheck, earliestPropertyNameToCheck, more);
}
/**
* Convenience method for creating a mutator that targets a specific element in an object hierarchy.
*
* Passes through to the MutatorUtils method; see there for more information.
*
* @see MutatorUtils#makeAnchoredPathMutator(Function, Class, String, Object...)
*/
public Mutator makeAnchoredPathMutator(Function, Object> leafValueCalculator, Class earliestAncestorToCheck, String earliestPropertyNameToCheck, Object... more) {
return this.mutatorUtils.makeAnchoredPathMutator(leafValueCalculator, earliestAncestorToCheck, earliestPropertyNameToCheck, more);
}
/**
* Convenience method for creating a mutator that applies the results of multiple mutators. Note that WALK_CHILDREN
* takes precedence over CHANGED when merging results.
*
* @param mutators
* @return
*/
public Mutator combineMutators(Mutator... mutators) {
return (LinkedList ancestry, LinkedList propertyNames, Supplier valueSupplier) -> {
MutationResult finalResult = UNCHANGED_MUTATION_RESULT;
//
// Run the child mutators and choose the result to use.
//
for (Mutator oneMutator : mutators) {
MutationResult result = oneMutator.mutate(ancestry, propertyNames, valueSupplier);
finalResult = this.mergeMutationResults(finalResult, result);
}
return finalResult;
} ;
}
/**
* Merge the given mutation results. WALK_CHILDREN takes priority and immediately returns
* since there is no other state associated with WALK_CHILDREN. CHANGED has priority over UNCHANGED, with the last
* one present having priority over those preceding it.
*
* @param results the list of mutation results to merge.
* @return the resulting mutation chosen by the merge.
*/
public MutationResult mergeMutationResults(MutationResult... results) {
MutationResult finalResult = UNCHANGED_MUTATION_RESULT;
//
// Walk all the given results and pick the one to use.
//
for (MutationResult oneResult : results) {
switch (oneResult.getResultType()) {
case WALK_CHILDREN:
return oneResult;
case CHANGED:
finalResult = oneResult;
}
}
return finalResult;
}
//========================================
// Internals
//----------------------------------------
// TBD: consider using a model object to track the state of the mutation tree walk
private
boolean
mutateProperty(
LinkedList ancestry,
LinkedList propertyNames,
T parentNode,
Mutator mutator,
Map> updateMapper,
Map childrenToWalkMap,
String propertyName) {
Method getter = this.locateGetter(parentNode.getClass(), propertyName);
if (getter == null) {
throw new IllegalArgumentException("missing getter: property=" + propertyName + "; class=" + parentNode.getClass().getName());
}
return this.mutatePropertyWithSupplier(
ancestry,
propertyNames,
parentNode,
mutator,
updateMapper,
childrenToWalkMap,
propertyName,
() -> this.readFieldWithGetter(parentNode, getter, propertyName)
);
}
private
boolean
mutatePropertyWithSupplier(
LinkedList ancestry,
LinkedList propertyNames,
T parentNode,
Mutator mutator,
Map> updateMapper,
Map childrenToWalkMap,
String propertyName,
Supplier propertySupplier
) {
//
// Prepare the information for the mutator.
//
LinkedList newAncestry = this.newLinkedListWithAdd(ancestry, parentNode);
LinkedList newPropertyNames = this.newLinkedListWithAdd(propertyNames, propertyName);
MemoizeSupplier memoizeSupplier = new MemoizeSupplier(propertySupplier);
//
// Always add every property to the updater because it will be used to construct new parent object instances
// and needs all of the properties.
//
updateMapper.put(propertyName, memoizeSupplier);
//
// Apply the mutator now.
//
MutationResult mutationResult = mutator.mutate(newAncestry, newPropertyNames, memoizeSupplier);
//
// Handle the outcome.
//
boolean changed = processMutatorOutcome(updateMapper, childrenToWalkMap, propertyName, memoizeSupplier, mutationResult);
return changed;
}
/**
* Walk the properties of the child property given. This method recursively calls mutateDeep() and handles the
* result.
*
* @param ancestry
* @param propertyNames
* @param node
* @param mutator
* @param changed
* @param updateMapper
* @param childPropertyToWalk
* @param
* @return
*/
private
boolean
walkChildProperty(
LinkedList ancestry,
LinkedList propertyNames,
T node,
Mutator mutator,
boolean changed,
Map> updateMapper,
Map.Entry childPropertyToWalk) {
String propertyName = childPropertyToWalk.getKey();
Supplier valueSupplier = childPropertyToWalk.getValue();
LinkedList newAncestry = this.newLinkedListWithAdd(ancestry, node);
LinkedList newPropertyNames = this.newLinkedListWithAdd(propertyNames, propertyName);
Object child = valueSupplier.get();
Object updatedChild = this.mutateDeep(newAncestry, newPropertyNames, child, mutator);
// If the child changed, record the change. Using != here works perfectly because the child is immutable;
// the same instance will, therefore, surely be unchanged. A new instance could actually contain the same
// contents as well - but that's up to the mutator to avoid as appropriate.
if (updatedChild != child) {
changed = true;
updateMapper.put(propertyName, () -> updatedChild);
}
return changed;
}
/**
* Given the mutator was run against a property, apply the results to the mutation process.
*
* @param updateMapper map keyed by property name that holds suppliers of the property value (either original or
* mutated)
* @param childrenToWalkMap map of children that will be walked later keyed by the property name for the child.
* @param propertyName name of the property that finished being processed by the mutator.
* @param memoizeSupplier supplier of the value for this property.
* @param mutationResult result indicating how to handle the property.
* @return true => if the value changed; false => if the value did not change yet; when the outcome is
* "WALK_CHILDREN", false is returned.
*/
private boolean processMutatorOutcome(Map> updateMapper, Map childrenToWalkMap, String propertyName, MemoizeSupplier memoizeSupplier, MutationResult mutationResult) {
boolean changed = false;
switch (mutationResult.getResultType()) {
case UNCHANGED:
// The property remains unchanged.
break;
case CHANGED:
// The property was changed; record the update. Don't walk the children of the value as a new
// replacement has been given.
changed = true;
updateMapper.put(propertyName, mutationResult::getReplacementValue);
break;
case WALK_CHILDREN:
// Walk the children of the property to update. The result of that walk will be used as the
// replacement, if a change occurs.
childrenToWalkMap.put(propertyName, memoizeSupplier);
break;
}
return changed;
}
/**
* Create a new linked list with the contents of the original list, and the given element added to the end.
*
* @param original list whose contents will be copied.
* @param toAdd additional element that willl be added to the end of the resulting list.
* @param type of elements in the list.
* @return new list with the contents of the original list plus the added element specified.
*/
private LinkedList newLinkedListWithAdd(LinkedList original, T toAdd) {
LinkedList result = new LinkedList<>(original);
result.add(toAdd);
return result;
}
/**
* Safely read a field using its getter.
*
* @param instance instance of the object from which to read the field.
* @param getter method that gets the field value from the object.
* @param propertyName name of the field being read.
* @param type of the field being read.
* @return the value of the field returned by the getter.
* @throws CannotMutateException if an exception is thrown on invoking the getter.
*/
private T readFieldWithGetter(Object instance, Method getter, String propertyName) {
try {
return (T) getter.invoke(instance);
} catch (Exception exc) {
this.log.info("Problem accessing property: property={}; readMethod={}", propertyName, getter.getName());
throw new CannotMutateException("property name = " + propertyName, exc);
}
}
//========================================
// Deep Internals: Reflection
//----------------------------------------
// TODO: consider caching the results of reflection
/**
*
* @param clazz
* @param propertySuppliers
* @param
* @return
*/
private T constructNewInstanceWithProperties(Class clazz, Map> propertySuppliers) {
try {
Constructor constructor = this.findConstructor(clazz);
if (constructor == null) {
throw new CannotConstructException("Cannot construct instance; failed to find a suitable constructor: class=" + clazz.getName());
}
MutationUtilsImmutableProperties mutationUtilsImmutableProperties = clazz.getAnnotation(MutationUtilsImmutableProperties.class);
//
Object[] arguments = prepareConstructorArguments(propertySuppliers, mutationUtilsImmutableProperties);
T instance = (T) constructor.newInstance(arguments);
return instance;
} catch (Exception exc) {
throw new CannotConstructException("failed to construct new instance of class " + clazz.getName(), exc);
}
}
/**
* Prepare the arguments for the constructor using the class MutationUtilsImmutableProperties annotation which
* specifies the fields and order.
*
* @param propertySuppliers
* @param mutationUtilsImmutableProperties
* @return
*/
private Object[] prepareConstructorArguments(Map> propertySuppliers, MutationUtilsImmutableProperties mutationUtilsImmutableProperties) {
Object[] arguments = new Object[mutationUtilsImmutableProperties.properties().length];
int cur = 0;
for (String onePropertyName : mutationUtilsImmutableProperties.properties()) {
Supplier newValueSupplier = propertySuppliers.get(onePropertyName);
arguments[cur] = newValueSupplier.get();
cur++;
}
return arguments;
}
/**
* Find the constructor. Expecting to only have 1. If more than 1, we take the first one. Not great logic, but
* the expectation is that the model objects will be built to match this library.
*
* @param clazz
* @return
*/
private Constructor findConstructor(Class clazz) {
Constructor[] constructors = clazz.getConstructors();
// Really shouldn't do much here - the entire intent of this library is to work with classes that are built
// for its use, so a single constructor is expected at all times.
if (constructors.length > 0) {
return constructors[0];
}
return null;
}
private Method locateGetter(Class clazz, String fieldname) {
String caseNormalizedFieldName = fieldname.substring(0, 1).toUpperCase() + fieldname.substring(1);
String getterName = "get" + caseNormalizedFieldName;
Method method = this.getNamedMethodInHierarchy(clazz, getterName);
// Look for isField(). TODO: only if the field is a boolean or Boolean?
if (method == null) {
getterName = "is" + caseNormalizedFieldName;
method = this.getNamedMethodInHierarchy(clazz, getterName);
}
return method;
}
private Method getNamedMethodInHierarchy(Class clazz, String methodName) {
try {
Method result = clazz.getDeclaredMethod(methodName);
if (result != null) {
return result;
}
} catch (NoSuchMethodException e) {
}
Class superClass = clazz.getSuperclass();
if ((superClass != clazz) && (superClass != null)) {
return this.getNamedMethodInHierarchy(superClass, methodName);
}
return null;
}
}