Skip to content

Commit

Permalink
Traverse now "offers" all graph node fields to the visitor
Browse files Browse the repository at this point in the history
  • Loading branch information
jdereg committed Jan 11, 2025
1 parent e711ac3 commit cd7e387
Show file tree
Hide file tree
Showing 2 changed files with 120 additions and 112 deletions.
24 changes: 10 additions & 14 deletions src/main/java/com/cedarsoftware/util/GraphComparator.java
Original file line number Diff line number Diff line change
Expand Up @@ -488,25 +488,21 @@ public static List<Delta> compare(Object source, Object target, final ID idFetch

// source objects by ID
final Set<Object> potentialOrphans = new HashSet<>();
Traverser.traverse(source, new Traverser.Visitor()
{
public void process(Object o)
{
if (isIdObject(o, idFetcher))
{
potentialOrphans.add(idFetcher.getId(o));
}
Traverser.traverse(source, visit -> {
Object node = visit.getNode();
if (isIdObject(node, idFetcher)) {
potentialOrphans.add(idFetcher.getId(node));
}
});
}, null);

// Remove all target objects from potential orphan map, leaving remaining objects
// that are no longer referenced in the potentialOrphans map.
Traverser.traverse(target, o -> {
if (isIdObject(o, idFetcher))
{
potentialOrphans.remove(idFetcher.getId(o));
Traverser.traverse(target, visit -> {
Object node = visit.getNode();
if (isIdObject(node, idFetcher)) {
potentialOrphans.remove(idFetcher.getId(node));
}
});
}, null);

List<Delta> forReturn = new ArrayList<>(deltas);
// Generate DeltaCommands for orphaned objects
Expand Down
208 changes: 110 additions & 98 deletions src/main/java/com/cedarsoftware/util/Traverser.java
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
import java.util.Collection;
import java.util.Collections;
import java.util.Deque;
import java.util.HashMap;
import java.util.HashSet;
import java.util.IdentityHashMap;
import java.util.LinkedList;
Expand All @@ -16,13 +17,36 @@
/**
* A Java Object Graph traverser that visits all object reference fields and invokes a
* provided callback for each encountered object, including the root. It properly
* detects cycles within the graph to prevent infinite loops.
* detects cycles within the graph to prevent infinite loops. For each visited node,
* complete field information including metadata is provided.
*
* <p>
* <b>Usage Examples:</b>
* </p>
*
* <p><b>Using the Old API with {@link Traverser.Visitor}:</b></p>
* <p><b>Using the Modern API (Recommended):</b></p>
* <pre>{@code
* // Define classes to skip (optional)
* Set<Class<?>> classesToSkip = new HashSet<>();
* classesToSkip.add(String.class);
*
* // Traverse with full node information
* Traverser.traverse(root, classesToSkip, visit -> {
* System.out.println("Node: " + visit.getNode());
* visit.getFields().forEach((field, value) -> {
* System.out.println(" Field: " + field.getName() +
* " (type: " + field.getType().getSimpleName() + ") = " + value);
*
* // Access field metadata if needed
* if (field.isAnnotationPresent(JsonProperty.class)) {
* JsonProperty ann = field.getAnnotation(JsonProperty.class);
* System.out.println(" JSON property: " + ann.value());
* }
* });
* });
* }</pre>
*
* <p><b>Using the Legacy API (Deprecated):</b></p>
* <pre>{@code
* // Define a visitor that processes each object
* Traverser.Visitor visitor = new Traverser.Visitor() {
Expand All @@ -37,17 +61,6 @@
* Traverser.traverse(root, visitor);
* }</pre>
*
* <p><b>Using the New API with Lambda and {@link Set} of classes to skip:</b></p>
* <pre>{@code
* // Define classes to skip
* Set<Class<?>> classesToSkip = new HashSet<>();
* classesToSkip.add(String.class);
* classesToSkip.add(Integer.class);
*
* // Traverse the object graph with a lambda callback
* Traverser.traverse(root, classesToSkip, o -> System.out.println("Visited: " + o));
* }</pre>
*
* <p>
* <b>Thread Safety:</b> This class is <i>not</i> thread-safe. If multiple threads access
* a {@code Traverser} instance concurrently, external synchronization is required.
Expand All @@ -68,18 +81,45 @@
* 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.
*
* @see ReflectionUtils#getAllDeclaredFields(Class)
*/
public class Traverser {

/**
* Represents a node visit during traversal, containing the node and its field information.
*/
public static class NodeVisit {
private final Object node;
private final Map<Field, Object> fields;

public NodeVisit(Object node, Map<Field, Object> fields) {
this.node = node;
this.fields = Collections.unmodifiableMap(new HashMap<>(fields));
}

/**
* @return The object (node) being visited
*/
public Object getNode() { return node; }

/**
* @return Unmodifiable map of fields to their values, including metadata about each field
*/
public Map<Field, Object> getFields() { return fields; }

/**
* @return The class of the node being visited
*/
public Class<?> getNodeClass() { return node.getClass(); }
}

/**
* A visitor interface to process each object encountered during traversal.
* <p>
* <b>Note:</b> This interface is deprecated in favor of using lambda expressions
* <b>Note:</b> This interface is deprecated in favor of using {@link Consumer<NodeVisit>}
* with the new {@code traverse} method.
* </p>
*
* @deprecated Use lambda expressions with {@link #traverse(Object, Set, Consumer)} instead.
* @deprecated Use {@link #traverse(Object, Set, Consumer)} instead.
*/
@Deprecated
@FunctionalInterface
Expand All @@ -92,63 +132,59 @@ public interface Visitor {
void process(Object o);
}

// Tracks visited objects to prevent cycles. Uses identity comparison.
private final Set<Object> objVisited = Collections.newSetFromMap(new IdentityHashMap<>());
private final Consumer<NodeVisit> nodeVisitor;

private Traverser(Consumer<NodeVisit> nodeVisitor) {
this.nodeVisitor = nodeVisitor;
}

/**
* Traverses the object graph starting from the provided root object.
* <p>
* This method uses the new API with a {@code Set<Class<?>>} and a lambda expression.
* </p>
* Traverses the object graph with complete node visiting capabilities.
*
* @param root the root object to start traversal
* @param classesToSkip a {@code Set} of {@code Class} objects to skip during traversal; may be {@code null}
* @param objectProcessor a lambda expression to process each encountered object
* @param root the root object to start traversal
* @param classesToSkip classes to skip during traversal (can be null)
* @param visitor visitor that receives detailed node information
*/
public static void traverse(Object root, Set<Class<?>> classesToSkip, Consumer<Object> objectProcessor) {
public static void traverse(Object root, Consumer<NodeVisit> visitor, Set<Class<?>> classesToSkip) {
if (visitor == null) {
throw new IllegalArgumentException("visitor cannot be null");
}
Traverser traverser = new Traverser(visitor);
traverser.walk(root, classesToSkip);
}

private static void traverse(Object root, Set<Class<?>> classesToSkip, Consumer<Object> objectProcessor) {
if (objectProcessor == null) {
throw new IllegalArgumentException("objectProcessor cannot be null");
}
Traverser traverser = new Traverser();
traverser.walk(root, classesToSkip, objectProcessor);
traverse(root, visit -> objectProcessor.accept(visit.getNode()), classesToSkip);
}

/**
* Traverses the object graph starting from the provided root object.
*
* @param root the root object to start traversal
* @param visitor the visitor to process each encountered object
*
* @deprecated Use {@link #traverse(Object, Set, Consumer)} instead with a lambda expression.
* @deprecated Use {@link #traverse(Object, Set, Consumer)} instead.
*/
@Deprecated
public static void traverse(Object root, Visitor visitor) {
traverse(root, (Set<Class<?>>) null, visitor == null ? null : visitor::process);
if (visitor == null) {
throw new IllegalArgumentException("visitor cannot be null");
}
traverse(root, visit -> visitor.process(visit.getNode()), null);
}

/**
* Traverses the object graph starting from the provided root object, skipping specified classes.
*
* @param root the root object to start traversal
* @param skip an array of {@code Class} objects to skip during traversal; may be {@code null}
* @param visitor the visitor to process each encountered object
*
* @deprecated Use {@link #traverse(Object, Set, Consumer)} instead with a {@code Set<Class<?>>} and a lambda expression.
* @deprecated Use {@link #traverse(Object, Set, Consumer)} instead.
*/
@Deprecated
public static void traverse(Object root, Class<?>[] skip, Visitor visitor) {
if (visitor == null) {
throw new IllegalArgumentException("visitor cannot be null");
}
Set<Class<?>> classesToSkip = (skip == null) ? null : new HashSet<>(Arrays.asList(skip));
traverse(root, classesToSkip, visitor == null ? null : visitor::process);
traverse(root, visit -> visitor.process(visit.getNode()), classesToSkip);
}

/**
* Traverses the object graph referenced by the provided root.
*
* @param root the root object to start traversal
* @param classesToSkip a {@code Set} of {@code Class} objects to skip during traversal; may be {@code null}
* @param objectProcessor a lambda expression to process each encountered object
*/
private void walk(Object root, Set<Class<?>> classesToSkip, Consumer<Object> objectProcessor) {
private void walk(Object root, Set<Class<?>> classesToSkip) {
if (root == null) {
return;
}
Expand All @@ -164,13 +200,14 @@ private void walk(Object root, Set<Class<?>> classesToSkip, Consumer<Object> obj
}

Class<?> clazz = current.getClass();

if (shouldSkipClass(clazz, classesToSkip)) {
continue;
}

objVisited.add(current);
objectProcessor.accept(current);

Map<Field, Object> fields = collectFields(current);
nodeVisitor.accept(new NodeVisit(current, fields));

if (clazz.isArray()) {
processArray(stack, current, classesToSkip);
Expand All @@ -184,18 +221,24 @@ private void walk(Object root, Set<Class<?>> classesToSkip, Consumer<Object> obj
}
}

/**
* Determines whether the specified class should be skipped based on the provided skip set.
*
* @param clazz the class to check
* @param classesToSkip a {@code Set} of {@code Class} objects to skip; may be {@code null}
* @return {@code true} if the class should be skipped; {@code false} otherwise
*/
private Map<Field, Object> collectFields(Object obj) {
Map<Field, Object> fields = new HashMap<>();
Collection<Field> allFields = ReflectionUtils.getAllDeclaredFields(obj.getClass());

for (Field field : allFields) {
try {
fields.put(field, field.get(obj));
} catch (IllegalAccessException e) {
fields.put(field, "<inaccessible>");
}
}
return fields;
}

private boolean shouldSkipClass(Class<?> clazz, Set<Class<?>> classesToSkip) {
if (classesToSkip == null) {
return false;
}

for (Class<?> skipClass : classesToSkip) {
if (skipClass.isAssignableFrom(clazz)) {
return true;
Expand All @@ -204,18 +247,11 @@ private boolean shouldSkipClass(Class<?> clazz, Set<Class<?>> classesToSkip) {
return false;
}

/**
* Processes array elements, adding non-primitive and non-skipped elements to the stack.
*
* @param stack the traversal stack
* @param array the array object to process
* @param classesToSkip a {@code Set} of {@code Class} objects to skip during traversal; may be {@code null}
*/
private void processArray(Deque<Object> stack, Object array, Set<Class<?>> classesToSkip) {
int length = Array.getLength(array);
Class<?> componentType = array.getClass().getComponentType();

if (!componentType.isPrimitive()) { // Skip primitive arrays
if (!componentType.isPrimitive()) {
for (int i = 0; i < length; i++) {
Object element = Array.get(array, i);
if (element != null && !shouldSkipClass(element.getClass(), classesToSkip)) {
Expand All @@ -225,62 +261,38 @@ private void processArray(Deque<Object> stack, Object array, Set<Class<?>> class
}
}

/**
* Processes elements of a {@link Collection}, adding non-primitive and non-skipped elements to the stack.
*
* @param stack the traversal stack
* @param collection the collection to process
*/
private void processCollection(Deque<Object> stack, Collection<?> collection) {
for (Object element : collection) {
if (element != null && !element.getClass().isPrimitive()) {
if (element != null) {
stack.addFirst(element);
}
}
}

/**
* Processes entries of a {@link Map}, adding non-primitive keys and values to the stack.
*
* @param stack the traversal stack
* @param map the map to process
*/
private void processMap(Deque<Object> stack, Map<?, ?> map) {
for (Map.Entry<?, ?> entry : map.entrySet()) {
Object key = entry.getKey();
Object value = entry.getValue();

if (key != null && !key.getClass().isPrimitive()) {
if (key != null) {
stack.addFirst(key);
}
if (value != null && !value.getClass().isPrimitive()) {
if (value != null) {
stack.addFirst(value);
}
}
}

/**
* Processes the fields of an object, adding non-primitive field values to the stack.
*
* @param stack the traversal stack
* @param object the object whose fields are to be processed
* @param classesToSkip a {@code Set} of {@code Class} objects to skip during traversal; may be {@code null}
*/
private void processFields(Deque<Object> stack, Object object, Set<Class<?>> classesToSkip) {
Collection<Field> fields = ReflectionUtils.getAllDeclaredFields(object.getClass());

for (Field field : fields) {
Class<?> fieldType = field.getType();

if (!fieldType.isPrimitive()) { // Only process reference fields
if (!field.getType().isPrimitive()) {
try {
Object value = field.get(object);
if (value != null && !shouldSkipClass(value.getClass(), classesToSkip)) {
stack.addFirst(value);
}
} catch (IllegalAccessException e) {
// Optionally log inaccessible fields
// For now, we'll ignore inaccessible fields
} catch (IllegalAccessException ignored) {
}
}
}
Expand Down

0 comments on commit cd7e387

Please sign in to comment.