Преглед изворни кода

draft

Signed-off-by: Ranides Atterwim <ranides@gmail.com>
Mariusz Czarnowski пре 5 година
родитељ
комит
54a05a58c8

+ 62 - 336
assira/src/main/java/net/ranides/assira/reflection/ObjectScanner.java

@@ -1,304 +1,109 @@
 package net.ranides.assira.reflection;
 
-import lombok.AllArgsConstructor;
-import lombok.RequiredArgsConstructor;
+import net.ranides.assira.reflection.impl.scanner.ScanWalker;
 
-import net.ranides.assira.collection.arrays.NativeArray;
-import net.ranides.assira.collection.lists.NativeArrayList;
-import net.ranides.assira.functional.special.LazyFunction;
-import net.ranides.assira.generic.LazyReference;
-
-import javax.swing.text.html.Option;
-import java.lang.reflect.Array;
-import java.util.IdentityHashMap;
 import java.util.Iterator;
 import java.util.Map;
-import java.util.Optional;
-import java.util.function.Consumer;
+import java.util.Set;
 import java.util.function.Predicate;
-import java.util.function.Supplier;
 
 public class ObjectScanner {
 
-    public static boolean walk(Object input, ObjectListener listener) {
-        return new Walker(input, listener).walk();
+    public static void walk(Object input, Predicate<ObjectContext> visitor) {
+        walk(input, new SimpleListener(visitor));
     }
 
-    @AllArgsConstructor
-    private static class Walker {
-
-        private static final Object UNDEFINED = new Object(){
-            @Override
-            public String toString() {
-                return "<UNDEFINED>";
-            }
-        };
-
-        private final Map<Object, Integer> visited = new IdentityHashMap<>();
-        private final int counter = 0;
+    public static void walk(Object input, ObjectVisitor visitor) {
+        walk(input, new SimpleListener(visitor));
+    }
 
-        private final Object root;
+    public static void walk(Object input, ObjectListener listener) {
+        new ScanWalker(input, listener).walk();
+    }
 
-        private final ObjectListener listener;
+    public static class SimpleListener extends AbstractObjectListener {
 
-        public boolean walk() {
-            return walk(null, root);
-        }
+        private final ObjectVisitor visitor;
 
-        public boolean walk(ObjectContext context, Object input) {
-            return
-                walkArray(context, input) ||
-                walkList(context, input) ||
-                walkCollection(context, input) ||
-                walkMap(context, input) ||
-                walkObject(context, input);
+        public SimpleListener(ObjectVisitor visitor) {
+            this.visitor = visitor;
         }
 
-        @RequiredArgsConstructor
-        private class CObject implements ObjectContext {
-            private final ObjectContext parent;
-            private final String name;
-            private final IClass<?> type;
-            private final Object value;
-            private final int id;
-
-            private final Optional<IField> field;
-
-            private final LazyFunction<String> typePath = LazyReference.concurrent();
-            private final LazyFunction<String> namePath = LazyReference.concurrent();
-            private final LazyFunction<String> idPath = LazyReference.concurrent();
-
-            @Override
-            public ObjectContext parent() {
-                return parent;
-            }
-
-            @Override
-            public String name() {
-                return name;
-            }
-
-            @Override
-            public IClass<?> type() {
-                IClass<Object> vtype = IClass.typefor(value);
-                return type;
-            }
-
-            @Override
-            public Object value() {
-                return value;
-            }
-
-            @Override
-            public long id() {
-                return id;
-            }
-
-            @Override
-            public long ref() {
-                return visited.get(value);
-            }
-
-            @Override
-            public String typePath() {
-                return typePath.apply(() -> {
-                    if(parent == null) {
-                        return type().shortname();
-                    } else {
-                        return parent.typePath() + "." + type().shortname();
-                    }
-                });
-            }
-
-            @Override
-            public String namePath() {
-                return namePath.apply(() -> {
-                    if(parent == null) {
-                        return name();
-                    } else {
-                        return parent.name() + "." + name();
-                    }
-                });
-            }
-
-            @Override
-            public String idPath() {
-                return idPath.apply(() -> {
-                    if(parent == null) {
-                        return String.valueOf(id());
-                    } else {
-                        return parent.idPath() + "." + id();
-                    }
-                });
-            }
-
-            @Override
-            public boolean isRoot() {
-                return value == root;
-            }
-
-            @Override
-            public boolean isField() {
-                return field.isPresent();
-            }
-
-            @Override
-            public boolean isVisited() {
-                return visited.containsKey(value);
-            }
-
-            @Override
-            public boolean isDefined() {
-                return value == UNDEFINED;
-            }
-
-            @Override
-            public boolean isPrimitive() {
-                return type.isPrimitive();
-            }
-
-            @Override
-            public boolean isNull() {
-                return value == null;
-            }
-
-            @Override
-            public boolean isSubclass() {
-                return type.isInstance(value) && !type.equals(IClass.typefor(value));
-            }
+        public SimpleListener(Predicate<ObjectContext> predicate) {
+            this(new AbstractObjectVisitor() {
+                @Override
+                public boolean visitObject(ObjectContext value) {
+                    return predicate.test(value);
+                }
+            });
         }
 
-    }
-
-    public static ObjectListener newListener(Predicate<ObjectContext> visitor) {
-        return newListener(new AbstractObjectVisitor() {
-            @Override
-            public boolean visitObject(ObjectContext value) {
-                return visitor.test(value);
-            }
-        });
-    }
-
-    public static ObjectListener newListener(ObjectVisitor visitor) {
-        return new AbstractObjectListener() {
-            @Override
-            public boolean enterObject(ObjectContext value) {
-                return visitor.visitObject(value);
-            }
-
-            @Override
-            public boolean leaveObject(ObjectContext value) {
-                return false;
-            }
-
-            @Override
-            public boolean enterArray(ArrayContext value) {
-                return visitor.visitArray(value);
-            }
-
-            @Override
-            public boolean enterArrayItem(ArrayItemContext value) {
-                return visitor.visitArrayItem(value);
-            }
-
-            @Override
-            public boolean enterCollection(CollectionContext value) {
-                return visitor.visitCollection(value);
-            }
-
-            @Override
-            public boolean enterCollectionItem(CollectionItemContext value) {
-                return visitor.visitCollectionItem(value);
-            }
-
-            @Override
-            public boolean enterList(ListContext value) {
-                return visitor.visitList(value);
-            }
-
-            @Override
-            public boolean enterListItem(ListItemContext value) {
-                return visitor.visitListItem(value);
-            }
-
-            @Override
-            public boolean enterMap(MapContext value) {
-                return visitor.visitMap(value);
-            }
-
-            @Override
-            public boolean enterMapItem(MapItemContext value) {
-                return visitor.visitMapItem(value);
-            }
-
-        };
-    }
-
-    public static abstract class AbstractObjectListener implements ObjectListener {
         @Override
-        public abstract boolean enterObject(ObjectContext value);
+        public boolean enterObject(ObjectContext value) {
+            return visitor.visitObject(value);
+        }
 
         @Override
-        public abstract boolean leaveObject(ObjectContext value);
+        public void leaveObject(ObjectContext value) {
+            // do nothing
+        }
 
         @Override
         public boolean enterArray(ArrayContext value) {
-            return enterObject(value);
+            return visitor.visitArray(value);
         }
 
         @Override
-        public boolean leaveArray(ArrayContext value) {
-            return leaveObject(value);
+        public boolean enterCollection(CollectionContext value) {
+            return visitor.visitCollection(value);
         }
 
         @Override
-        public boolean enterArrayItem(ArrayItemContext value) {
-            return enterObject(value);
+        public boolean enterList(ListContext value) {
+            return visitor.visitList(value);
         }
 
         @Override
-        public boolean leaveArrayItem(ArrayItemContext value) {
-            return leaveObject(value);
+        public boolean enterMap(MapContext value) {
+            return visitor.visitMap(value);
         }
+    }
 
+    public static abstract class AbstractObjectListener implements ObjectListener {
         @Override
-        public boolean enterCollection(CollectionContext value) {
-            return enterObject(value);
-        }
+        public abstract boolean enterObject(ObjectContext value);
 
         @Override
-        public boolean leaveCollection(CollectionContext value) {
-            return leaveObject(value);
-        }
+        public abstract void leaveObject(ObjectContext value);
 
         @Override
-        public boolean enterCollectionItem(CollectionItemContext value) {
+        public boolean enterArray(ArrayContext value) {
             return enterObject(value);
         }
 
         @Override
-        public boolean leaveCollectionItem(CollectionItemContext value) {
-            return leaveObject(value);
+        public void leaveArray(ArrayContext value) {
+            leaveObject(value);
         }
 
         @Override
-        public boolean enterList(ListContext value) {
+        public boolean enterCollection(CollectionContext value) {
             return enterObject(value);
         }
 
         @Override
-        public boolean leaveList(ListContext value) {
-            return leaveObject(value);
+        public void leaveCollection(CollectionContext value) {
+            leaveObject(value);
         }
 
         @Override
-        public boolean enterListItem(ListItemContext value) {
+        public boolean enterList(ListContext value) {
             return enterObject(value);
         }
 
         @Override
-        public boolean leaveListItem(ListItemContext value) {
-            return leaveObject(value);
+        public void leaveList(ListContext value) {
+            leaveObject(value);
         }
 
         @Override
@@ -307,19 +112,10 @@ public class ObjectScanner {
         }
 
         @Override
-        public boolean leaveMap(MapContext value) {
-            return leaveObject(value);
-        }
-
-        @Override
-        public boolean enterMapItem(MapItemContext value) {
-            return enterObject(value);
+        public void leaveMap(MapContext value) {
+            leaveObject(value);
         }
 
-        @Override
-        public boolean leaveMapItem(MapItemContext value) {
-            return leaveObject(value);
-        }
     }
 
     public static abstract class AbstractObjectVisitor implements ObjectVisitor {
@@ -332,40 +128,21 @@ public class ObjectScanner {
             return visitObject(value);
         }
 
-        @Override
-        public boolean visitArrayItem(ArrayItemContext value) {
-            return visitObject(value);
-        }
-
         @Override
         public boolean visitCollection(CollectionContext value) {
             return visitObject(value);
         }
 
-        @Override
-        public boolean visitCollectionItem(CollectionItemContext value) {
-            return visitObject(value);
-        }
-
         @Override
         public boolean visitList(ListContext value) {
             return visitObject(value);
         }
 
-        @Override
-        public boolean visitListItem(ListItemContext value) {
-            return visitObject(value);
-        }
-
         @Override
         public boolean visitMap(MapContext value) {
             return visitObject(value);
         }
 
-        @Override
-        public boolean visitMapItem(MapItemContext value) {
-            return visitObject(value);
-        }
     }
 
     public interface ObjectVisitor {
@@ -374,69 +151,52 @@ public class ObjectScanner {
 
         boolean visitArray(ArrayContext value);
 
-        boolean visitArrayItem(ArrayItemContext value);
-
         boolean visitCollection(CollectionContext value);
 
-        boolean visitCollectionItem(CollectionItemContext value);
-
         boolean visitList(ListContext value);
 
-        boolean visitListItem(ListItemContext value);
-
         boolean visitMap(MapContext value);
 
-        boolean visitMapItem(MapItemContext value);
-
     }
 
     public interface ObjectListener {
 
         boolean enterObject(ObjectContext value);
 
-        boolean leaveObject(ObjectContext value);
+        void leaveObject(ObjectContext value);
 
         boolean enterArray(ArrayContext value);
 
-        boolean leaveArray(ArrayContext value);
-
-        boolean enterArrayItem(ArrayItemContext value);
-
-        boolean leaveArrayItem(ArrayItemContext value);
+        void leaveArray(ArrayContext value);
 
         boolean enterCollection(CollectionContext value);
 
-        boolean leaveCollection(CollectionContext value);
-
-        boolean enterCollectionItem(CollectionItemContext value);
-
-        boolean leaveCollectionItem(CollectionItemContext value);
+        void leaveCollection(CollectionContext value);
 
         boolean enterList(ListContext value);
 
-        boolean leaveList(ListContext value);
-
-        boolean enterListItem(ListItemContext value);
-
-        boolean leaveListItem(ListItemContext value);
+        void leaveList(ListContext value);
 
         boolean enterMap(MapContext value);
 
-        boolean leaveMap(MapContext value);
+        void leaveMap(MapContext value);
 
-        boolean enterMapItem(MapItemContext value);
-
-        boolean leaveMapItem(MapItemContext value);
     }
 
     public interface ObjectContext {
 
+        void accept(ObjectListener listener);
+
         ObjectContext parent();
 
+        String typeName();
+
         String name();
 
         IClass<?> type();
 
+        IClass<?> actual();
+
         Object value();
 
         long id();
@@ -490,7 +250,7 @@ public class ObjectScanner {
 
         int size();
 
-        Iterator<Object> iterator();
+        Iterator<?> iterator();
 
     }
 
@@ -502,41 +262,7 @@ public class ObjectScanner {
 
         int size();
 
-        Iterator<Map.Entry<?, ?>> entries();
-
-    }
-
-    public interface ArrayItemContext extends ObjectContext {
-
-        @Override
-        ArrayContext parent();
-
-        int index();
-
-    }
-
-    public interface ListItemContext extends ObjectContext {
-
-        @Override
-        ListContext parent();
-
-        int index();
-
-    }
-
-    public interface CollectionItemContext extends ObjectContext {
-
-        @Override
-        CollectionContext parent();
-
-    }
-
-    public interface MapItemContext extends ObjectContext {
-
-        @Override
-        MapContext parent();
-
-        Object key();
+        Set<? extends Map.Entry<?, ?>> entries();
 
     }
 

+ 59 - 0
assira/src/main/java/net/ranides/assira/reflection/impl/scanner/ScanSupport.java

@@ -0,0 +1,59 @@
+package net.ranides.assira.reflection.impl.scanner;
+
+import net.ranides.assira.generic.TypeToken;
+import net.ranides.assira.reflection.IClass;
+
+import java.util.Collection;
+import java.util.List;
+import java.util.Map;
+
+public class ScanSupport {
+
+    public static final Undefined UNDEFINED = new Undefined();
+
+    private static final IClass<?> LIST = new TypeToken<List<?>>() {};
+
+    private static final IClass<?> COLLECTION = new TypeToken<Collection<?>>() {};
+
+    private static final IClass<?> MAP = new TypeToken<Map<?,?>>() {};
+
+    public static boolean isList(IClass<?> actual) {
+        return actual.isSubclass(LIST);
+    }
+
+    public static boolean isList(IClass<?> declared, IClass<?> actual) {
+        return isList(declared) || isList(actual);
+    }
+
+    public static boolean isCollection(IClass<?> actual) {
+        return actual.isSubclass(COLLECTION);
+    }
+
+    public static boolean isCollection(IClass<?> declared, IClass<?> actual) {
+        return isCollection(declared) || isCollection(actual);
+    }
+
+    public static boolean isMap(IClass<?> actual) {
+        return actual.isSubclass(MAP);
+    }
+
+    public static boolean isMap(IClass<?> declared, IClass<?> actual) {
+        return isMap(declared) || isMap(actual);
+    }
+
+    public static boolean isArray(IClass<?> actual) {
+        return actual.isArray();
+    }
+
+    public static boolean isArray(IClass<?> declared, IClass<?> actual) {
+        return isArray(declared) || isArray(actual);
+    }
+
+    public static class Undefined {
+        @Override
+        public String toString() {
+            return "<undefined>";
+        }
+    }
+
+}

+ 7 - 0
assira/src/main/java/net/ranides/assira/reflection/impl/scanner/ScanVisitors.java

@@ -0,0 +1,7 @@
+package net.ranides.assira.reflection.impl.scanner;
+
+public class ScanVisitors {
+
+
+
+}

+ 516 - 0
assira/src/main/java/net/ranides/assira/reflection/impl/scanner/ScanWalker.java

@@ -0,0 +1,516 @@
+package net.ranides.assira.reflection.impl.scanner;
+
+import net.ranides.assira.collection.arrays.NativeArray;
+import net.ranides.assira.functional.special.LazyFunction;
+import net.ranides.assira.generic.LazyReference;
+import net.ranides.assira.generic.ValueUtils;
+import net.ranides.assira.reflection.IAttribute;
+import net.ranides.assira.reflection.IClass;
+import net.ranides.assira.reflection.IElement;
+import net.ranides.assira.reflection.IField;
+import net.ranides.assira.reflection.ObjectScanner;
+
+import java.util.Collection;
+import java.util.Comparator;
+import java.util.IdentityHashMap;
+import java.util.Iterator;
+import java.util.List;
+import java.util.Map;
+import java.util.Optional;
+import java.util.Set;
+
+public class ScanWalker {
+
+    private static final String TYPE_PATH = "/";
+
+    private static final String NAME_PATH = "/";
+
+    private static final String ID_PATH = ".";
+
+    private final Map<Object, Long> visited = new IdentityHashMap<>();
+
+    private long counter = 0;
+
+    private final Object root;
+
+    private final ObjectScanner.ObjectListener listener;
+
+    public ScanWalker(Object root, ObjectScanner.ObjectListener listener) {
+        this.root = root;
+        this.listener = listener;
+    }
+
+    public void walk() {
+        context(null, null, IClass.OBJECT, root).accept(listener);
+    }
+
+    private void mark(ObjectScanner.ObjectContext ctx) {
+        if(!ctx.isPrimitive()) {
+            visited.put(ctx.value(), ctx.id());
+        }
+    }
+
+    private ObjectScanner.ObjectContext context(ObjectScanner.ObjectContext parent, IField field) {
+        Object value = ScanSupport.UNDEFINED;
+        if(!parent.isNull()) {
+            try {
+                value = field.accessible().get(parent.value());
+            } catch (Exception cause) {
+                // ignore
+            }
+        }
+        IClass<?> declared = field.type();
+        IClass<?> actual = IClass.typefor(value);
+
+        if(ScanSupport.isList(declared, actual)) {
+            return new CList(parent, field, value);
+        }
+        if(ScanSupport.isCollection(declared, actual)) {
+            return new CCollection(parent, field, value);
+        }
+        if(ScanSupport.isMap(declared, actual)) {
+            return new CMap(parent, field, value);
+        }
+        if(ScanSupport.isArray(declared, actual)) {
+            return new CArray(parent, field, value);
+        }
+        return new CObject(parent, field, value);
+    }
+
+    private ObjectScanner.ObjectContext context(ObjectScanner.ObjectContext parent, String name, IClass<?> declared, Object value) {
+        IClass<?> actual = IClass.typefor(value);
+
+        if(ScanSupport.isList(declared, actual)) {
+            return new CList(parent, name, declared, value);
+        }
+        if(ScanSupport.isCollection(declared, actual)) {
+            return new CCollection(parent, name, declared, value);
+        }
+        if(ScanSupport.isMap(declared, actual)) {
+            return new CMap(parent, name, declared, value);
+        }
+        if(ScanSupport.isArray(declared, actual)) {
+            return new CArray(parent, name, declared, value);
+        }
+        return new CObject(parent, name, declared, value);
+    }
+
+
+    private abstract class CAbstract implements ObjectScanner.ObjectContext {
+        private final ObjectScanner.ObjectContext parent;
+        private final String name;
+        private final IClass<?> declared;
+        private final IClass<?> actual;
+        private final Object value;
+        private final long id;
+
+        private final Optional<IField> field;
+
+        private final LazyFunction<String> typePath = LazyReference.concurrent();
+        private final LazyFunction<String> namePath = LazyReference.concurrent();
+        private final LazyFunction<String> idPath = LazyReference.concurrent();
+
+        public CAbstract(ObjectScanner.ObjectContext parent, IField field, Object value) {
+            this.field = Optional.of(field);
+            this.parent = parent;
+            this.name = field.name();
+            this.declared = field.type();
+            this.value = value;
+            this.actual = IClass.typefor(value);
+            this.id = counter++;
+
+        }
+
+        public CAbstract(ObjectScanner.ObjectContext parent, String name, IClass<?> declared, Object value) {
+            this.field = Optional.empty();
+            this.parent = parent;
+            this.name = name;
+            this.declared = declared;
+            this.value = value;
+            this.actual = IClass.typefor(value);
+            this.id = counter++;
+        }
+
+        @Override
+        public ObjectScanner.ObjectContext parent() {
+            return parent;
+        }
+
+        @Override
+        public String typeName() {
+            return declared.shortname();
+        }
+
+        @Override
+        public String name() {
+            return name;
+        }
+
+        @Override
+        public IClass<?> type() {
+            return declared;
+        }
+
+        @Override
+        public IClass<?> actual() {
+            return actual;
+        }
+
+        @Override
+        public Object value() {
+            return value;
+        }
+
+        @Override
+        public long id() {
+            return id;
+        }
+
+        @Override
+        public long ref() {
+            return ValueUtils.or(visited.get(value), id);
+        }
+
+        @Override
+        public String typePath() {
+            return typePath.apply(() -> isRoot() ? typeName() : parent.typePath() + TYPE_PATH + typeName());
+        }
+
+        @Override
+        public String namePath() {
+            return namePath.apply(() -> {
+                if(parent != null) {
+                    String head = parent.namePath();
+                    if(head != null) {
+                        return head + NAME_PATH + name();
+                    }
+                }
+                return name();
+            });
+        }
+
+        @Override
+        public String idPath() {
+            return idPath.apply(() -> isRoot() ? String.valueOf(id()) : parent.idPath() + ID_PATH + id());
+        }
+
+        @Override
+        public boolean isRoot() {
+            return value == root || parent == null;
+        }
+
+        @Override
+        public boolean isField() {
+            return field.isPresent();
+        }
+
+        @Override
+        public boolean isVisited() {
+            return visited.containsKey(value);
+        }
+
+        @Override
+        public boolean isDefined() {
+            return value == ScanSupport.UNDEFINED;
+        }
+
+        @Override
+        public boolean isPrimitive() {
+            return declared.isPrimitive();
+        }
+
+        @Override
+        public boolean isNull() {
+            return value == null;
+        }
+
+        @Override
+        public boolean isSubclass() {
+            return declared.isInstance(value) && !declared.equals(actual);
+        }
+
+    }
+
+    private class CObject extends CAbstract implements ObjectScanner.ObjectContext {
+        public CObject(ObjectScanner.ObjectContext parent, IField field, Object value) {
+            super(parent, field, value);
+        }
+
+        public CObject(ObjectScanner.ObjectContext parent, String name, IClass<?> declared, Object value) {
+            super(parent, name, declared, value);
+        }
+
+        @Override
+        public void accept(ObjectScanner.ObjectListener listener) {
+            try {
+                if (!listener.enterObject(this)) {
+                    return;
+                }
+
+                if (!isNull() && !isPrimitive()) {
+                    actual()
+                        .fields()
+                        .require(IAttribute.ANY)
+                        .discard(IAttribute.STATIC)
+                        .stream()
+                        .sort(Comparator.comparing(IElement::name))
+                        .map(field -> context(this, field))
+                        .each(context -> context.accept(listener));
+                }
+            } finally {
+                mark(this);
+                listener.leaveObject(this);
+            }
+        }
+
+    }
+
+    private class CArray extends CAbstract implements ObjectScanner.ArrayContext {
+
+        private final NativeArray array;
+
+        public CArray(ObjectScanner.ObjectContext parent, IField field, Object value) {
+            super(parent, field, value);
+            this.array = NativeArray.wrap(value());
+        }
+
+        public CArray(ObjectScanner.ObjectContext parent, String name, IClass<?> type, Object value) {
+            super(parent, name, type, value);
+            this.array = NativeArray.wrap(value());
+        }
+
+        @Override
+        public IClass<?> component() {
+            return type().component();
+        }
+
+        @Override
+        public int size() {
+            return array.size();
+        }
+
+        @Override
+        public Object at(int index) {
+            return array.get(index);
+        }
+
+        @Override
+        public void accept(ObjectScanner.ObjectListener listener) {
+            try {
+                if (!listener.enterArray(this)) {
+                    return;
+                }
+                if (!isNull()) {
+                    IClass<?> cmp = component();
+                    for (int index = 0, n = size(); index < n; index++) {
+                        context(this, "[" + index + "]", cmp, at(index)).accept(listener);
+                    }
+                }
+            } finally {
+                mark(this);
+                listener.leaveArray(this);
+            }
+        }
+    }
+
+    private class CList extends CAbstract implements ObjectScanner.ListContext {
+
+        private final List<?> list;
+        private final LazyFunction<IClass<?>> component = LazyReference.concurrent();
+
+        public CList(ObjectScanner.ObjectContext parent, IField field, Object value) {
+            super(parent, field, value);
+            this.list = (List<?>)value();
+        }
+
+        public CList(ObjectScanner.ObjectContext parent, String name, IClass<?> type, Object value) {
+            super(parent, name, type, value);
+            this.list = (List<?>)value();
+        }
+
+        @Override
+        public String typeName() {
+            return type().shortname() + "<" + component().shortname() + ">";
+        }
+
+        @Override
+        public IClass<?> component() {
+            return component.apply(() -> {
+                if(ScanSupport.isList(type())) {
+                    return type().params().get(0);
+                }
+                if(ScanSupport.isList(actual())) {
+                    return actual().params().get(0);
+                }
+                return IClass.OBJECT;
+            });
+        }
+
+        @Override
+        public int size() {
+            return list.size();
+        }
+
+        @Override
+        public Object at(int index) {
+            return list.get(index);
+        }
+
+        @Override
+        public void accept(ObjectScanner.ObjectListener listener) {
+            try {
+                if (!listener.enterList(this)) {
+                    return;
+                }
+                if (!isNull()) {
+                    IClass<?> cmp = component();
+                    for (int index = 0, n = size(); index < n; index++) {
+                        context(this, "[" + index + "]", cmp, at(index)).accept(listener);
+                    }
+                }
+            } finally {
+                mark(this);
+                listener.leaveList(this);
+            }
+        }
+    }
+
+    private class CCollection extends CAbstract implements ObjectScanner.CollectionContext {
+
+        private final Collection<?> collection;
+        private final LazyFunction<IClass<?>> component = LazyReference.concurrent();
+
+        public CCollection(ObjectScanner.ObjectContext parent, IField field, Object value) {
+            super(parent, field, value);
+            this.collection = (Collection<?>)value();
+        }
+
+        public CCollection(ObjectScanner.ObjectContext parent, String name, IClass<?> type, Object value) {
+            super(parent, name, type, value);
+            this.collection = (Collection<?>)value();
+        }
+
+        @Override
+        public String typeName() {
+            return type().shortname() + "<" + component().shortname() + ">";
+        }
+
+        @Override
+        public IClass<?> component() {
+            return component.apply(() -> {
+                if(ScanSupport.isCollection(type())) {
+                    return type().params().get(0);
+                }
+                if(ScanSupport.isCollection(actual())) {
+                    return actual().params().get(0);
+                }
+                return IClass.OBJECT;
+            });
+        }
+
+        @Override
+        public int size() {
+            return collection.size();
+        }
+
+        @Override
+        public Iterator<?> iterator() {
+            return collection.iterator();
+        }
+
+        @Override
+        public void accept(ObjectScanner.ObjectListener listener) {
+            try {
+                if (!listener.enterCollection(this)) {
+                    return;
+                }
+                if (!isNull()) {
+                    IClass<?> cmp = component();
+                    for (Object item : collection) {
+                        context(this, "[?]", cmp, item).accept(listener);
+                    }
+                }
+            } finally {
+                mark(this);
+                listener.leaveCollection(this);
+            }
+        }
+    }
+
+    private class CMap extends CAbstract implements ObjectScanner.MapContext {
+
+        private final Map<?,?> map;
+        private final LazyFunction<IClass<?>> keyComponent = LazyReference.concurrent();
+        private final LazyFunction<IClass<?>> valueComponent = LazyReference.concurrent();
+
+        public CMap(ObjectScanner.ObjectContext parent, IField field, Object value) {
+            super(parent, field, value);
+            this.map = (Map<?,?>)value();
+        }
+
+        public CMap(ObjectScanner.ObjectContext parent, String name, IClass<?> type, Object value) {
+            super(parent, name, type, value);
+            this.map = (Map<?,?>)value();
+        }
+
+        @Override
+        public int size() {
+            return map.size();
+        }
+
+        @Override
+        public String typeName() {
+            return type().shortname() + "<" + keyComponent().shortname() + "," + valueComponent().shortname() + ">";
+        }
+
+        @Override
+        public IClass<?> keyComponent() {
+            return keyComponent.apply(() -> {
+                if(ScanSupport.isMap(type())) {
+                    return type().params().get(0);
+                }
+                if(ScanSupport.isMap(actual())) {
+                    return actual().params().get(0);
+                }
+                return IClass.OBJECT;
+            });
+        }
+
+        @Override
+        public IClass<?> valueComponent() {
+            return valueComponent.apply(() -> {
+                if(ScanSupport.isMap(type())) {
+                    return type().params().get(1);
+                }
+                if(ScanSupport.isMap(actual())) {
+                    return actual().params().get(1);
+                }
+                return IClass.OBJECT;
+            });
+        }
+
+        @Override
+        public Set<? extends Map.Entry<?, ?>> entries() {
+            return map.entrySet();
+        }
+
+        @Override
+        public void accept(ObjectScanner.ObjectListener listener) {
+            try {
+                if (!listener.enterMap(this)) {
+                    return;
+                }
+                if (!isNull()) {
+                    IClass<?> cmp = valueComponent();
+                    for (Map.Entry<?, ?> entry : map.entrySet()) {
+                        context(this, "[" + entry.getKey() + "]", cmp, entry.getValue()).accept(listener);
+                    }
+                }
+            } finally {
+                mark(this);
+                listener.leaveMap(this);
+            }
+        }
+    }
+
+}

+ 24 - 9
assira/src/test/java/net/ranides/assira/functional/BranchTest.java

@@ -1,6 +1,5 @@
 package net.ranides.assira.functional;
 
-import net.ranides.assira.junit.NewAssert;
 import org.junit.Test;
 
 import java.util.NoSuchElementException;
@@ -26,14 +25,30 @@ public class BranchTest {
             .forValue(17, "invalid")
             .get();
         assertEquals("seventeen", value2);
+    }
+
+    @Test(expected = NoSuchElementException.class)
+    public void noElement() {
+        Branch.single(17)
+            .forValue(5, "five")
+            .forValue(17, "seventeen")
+            .forValue(9, "nine")
+            .forValue(17, "invalid")
+            .get();
+    }
+
+    @Test
+    public void type() {
+        String value1 = Branch.single(17L).forType(String.class, "text").forType(Long.class, "long").get();
+        assertEquals("long", value1);
+
+        String value2 = Branch.single(17L).forType(String.class, "text").forType(long.class, "long").get();
+        assertEquals("long", value2);
+
+        String value3 = Branch.single(17L).forType(String.class, "text").forType(Number.class, "number").get();
+        assertEquals("number", value3);
 
-        NewAssert.assertThrows(NoSuchElementException.class, () -> {
-            String value3 = Branch.single(17)
-                .forValue(5, "five")
-                .forValue(17, "seventeen")
-                .forValue(9, "nine")
-                .forValue(17, "invalid")
-                .get();
-        });
+        String value4 = Branch.single(null).forType(String.class, "text").forType(Number.class, "number").orElse("?");
+        assertEquals("?", value4);
     }
 }

+ 147 - 0
assira/src/test/java/net/ranides/assira/reflection/ObjectScannerTest.java

@@ -0,0 +1,147 @@
+package net.ranides.assira.reflection;
+
+import lombok.Builder;
+import lombok.Data;
+import lombok.Singular;
+import net.ranides.assira.functional.Branch;
+import org.junit.Test;
+
+import java.util.Arrays;
+import java.util.List;
+import java.util.Map;
+
+import static org.junit.Assert.*;
+
+public class ObjectScannerTest {
+
+    @Test
+    public void basic() {
+        Foo foo = Foo.builder()
+            .bar(new Bar("456", "0xFAD"))
+            .name("John")
+            .name("Jenny")
+            .name("Adam")
+            .point(17, new Point(21,6,4))
+            .point(18, new Point(21,6,4))
+            .point(88, new Point(21,6,4))
+            .build();
+
+        ObjectScanner.walk(foo, new ObjectScanner.AbstractObjectVisitor() {
+            @Override
+            public boolean visitObject(ObjectScanner.ObjectContext ctx) {
+                System.out.printf(" - [%-12s] %-40s | %s = %s%n",
+                    ctx.getClass().getSimpleName(),
+                    ctx.typePath(),
+                    ctx.namePath(),
+                    ctx.value()
+                );
+
+                if(ctx.value() instanceof String) {
+                    return false;
+                }
+
+                return true;
+            }
+        });
+    }
+
+    @Test
+    public void visitors() {
+        Foo foo = Foo.builder()
+            .bar(new Bar("456", "0xFAD"))
+            .name("John")
+            .name("Jenny")
+            .name("Adam")
+            .point(17, new Point(21,6,4))
+            .point(18, new Point(21,6,4))
+            .point(88, new Point(21,6,4))
+            .build();
+
+        ObjectScanner.walk(foo, new ObjectScanner.AbstractObjectVisitor() {
+            @Override
+            public boolean visitObject(ObjectScanner.ObjectContext ctx) {
+                System.out.printf(" - [%-12s] %-40s | %s = %s%n",
+                    ctx.getClass().getSimpleName(),
+                    ctx.typePath(),
+                    ctx.namePath(),
+                    ctx.value()
+                );
+
+                if(ctx.value() instanceof String) {
+                    return false;
+                }
+
+                return true;
+            }
+        });
+    }
+
+    @Test
+    public void recursive() {
+        Foo foo1 = Foo.builder()
+            .bar(new Bar("456", "0xFAD"))
+            .name("John")
+            .name("Jenny")
+            .name("Adam")
+            .point(17, new Point(21,6,4))
+            .point(18, new Point(21,6,4))
+            .point(88, new Point(21,6,4))
+            .build();
+
+        Foo foo2 = Foo.builder()
+            .bar(new Bar("55", "0xEE"))
+            .name("Tom")
+            .point(33, new Point(3,3,3))
+            .build();
+
+        Foo foo3 = Foo.builder()
+            .bar(new Bar("66", "0x11"))
+            .name("Ann")
+            .point(77, new Point(7,7,7))
+            .build();
+
+        List<Foo> list = Arrays.asList(foo1, foo2, foo1, foo3);
+
+        ObjectScanner.walk(list, ctx -> {
+            System.out.printf(" - [%-12s] %8s %-40s | %s = %s%n",
+                ctx.getClass().getSimpleName(),
+                ctx.isVisited(),
+                ctx.typePath(),
+                ctx.namePath(),
+                ctx.value()
+            );
+
+            if(ctx.value() instanceof String) {
+                return false;
+            }
+            if(ctx.isVisited()) {
+                return false;
+            }
+            return true;
+        });
+    }
+
+    @Builder
+    public static class Foo {
+        private final Bar bar;
+        @Singular
+        private final List<String> names;
+        @Singular
+        private final Map<Integer, Point> points;
+    }
+
+    @Data
+    public static class Bar {
+        private final String uid;
+        private final String hex;
+    }
+
+    @Data
+    public static class Point {
+        private final int x;
+        private final int y;
+        private final int z;
+    }
+
+
+}