瀏覽代碼

Resolver: array.length Resolver: custom context Resolver: evaluate context patterns ObjectWalker: custom tags ObjectWalker: remove 'text', #text method overrides 'value'

Signed-off-by: Ranides Atterwim <ranides@gmail.com>
Mariusz Czarnowski 5 年之前
父節點
當前提交
90c9b6ff72

+ 2 - 2
assira.junit/src/main/java/net/ranides/assira/junit/NewAssert.java

@@ -276,8 +276,8 @@ public final class NewAssert extends org.junit.Assert {
 
     public static void assertLineEquals(File file, Charset charset, String content) {
         try {
-            List<String> expected = Files.lines(file.toPath(), charset).collect(Collectors.toList());
-            List<String> actual = Arrays.asList(content.split("(\r\n|\n)"));
+            String expected = Files.lines(file.toPath(), charset).collect(Collectors.joining("\n"));
+            String actual = String.join("\n", content.split("(\r\n|\n)"));
             assertEquals(expected, actual);
         } catch (IOException cause) {
             throw new UncheckedIOException(cause);

+ 28 - 0
assira/src/main/java/net/ranides/assira/reflection/ResolveContext.java

@@ -0,0 +1,28 @@
+package net.ranides.assira.reflection;
+
+import java.util.Map;
+import java.util.Optional;
+
+public interface ResolveContext {
+
+    ResolveContext getResolveParent();
+
+    Map<String, Object> getResolveMap();
+
+    default Optional<Object> getResolveProperty(String name) {
+        Map<String, Object> map = getResolveMap();
+        if(map.containsKey(name)) {
+            return Optional.of(map.get(name));
+        }
+        ResolveContext parent = getResolveParent();
+        if(parent != null) {
+            return parent.getResolveProperty(name);
+        }
+        return Optional.empty();
+    }
+
+    default boolean hasResolveProperty(String name) {
+        return getResolveProperty(name).isPresent();
+    }
+
+}

+ 73 - 2
assira/src/main/java/net/ranides/assira/reflection/ResolveModel.java

@@ -3,6 +3,7 @@ package net.ranides.assira.reflection;
 import net.ranides.assira.collection.arrays.NativeArray;
 import net.ranides.assira.generic.SerializableCode;
 import net.ranides.assira.reflection.impl.RCompareUtils;
+import net.ranides.assira.text.ResolveFormat;
 import net.ranides.assira.text.StringTraits;
 
 import java.util.Iterator;
@@ -15,6 +16,8 @@ public abstract class ResolveModel extends SerializableCode {
 
     private static final long serialVersionUID = 1L;
 
+    private static final String ARRAY_LENGTH = "length";
+
     public abstract boolean matchesType(IClass<?> that, String name);
 
     public abstract boolean matches(Object that, String name);
@@ -30,31 +33,43 @@ public abstract class ResolveModel extends SerializableCode {
     public static ResolveModel ARRAY = new ResolveModel() {
         @Override
         public boolean matches(Object that, String name) {
-            return that != null && that.getClass().isArray() && StringTraits.isNumber(name);
+            return that != null && that.getClass().isArray() && (StringTraits.isNumber(name) || ARRAY_LENGTH.equals(name));
         }
 
         @Override
         public boolean matchesType(IClass<?> that, String name) {
-            return that.attributes().has(IAttribute.ARRAY) && StringTraits.isNumber(name);
+            return that.attributes().has(IAttribute.ARRAY) && (StringTraits.isNumber(name) || ARRAY_LENGTH.equals(name));
         }
 
         @Override
         public Object get(ResolveStatus _s, Object that, String name) {
+            if(ARRAY_LENGTH.equals(name)) {
+                return NativeArray.wrap(that).size();
+            }
             return NativeArray.wrap(that).get(Integer.parseInt(name));
         }
 
         @Override
         public void set(ResolveStatus _s, Object that, String name, Object value) {
+            if(ARRAY_LENGTH.equals(name)) {
+                throw new UnsupportedOperationException("Array.length is immutable");
+            }
             NativeArray.wrap(that).set(Integer.parseInt(name), value);
         }
 
         @Override
         public Object replace(ResolveStatus _s, Object that, String name, Object value) {
+            if(ARRAY_LENGTH.equals(name)) {
+                throw new UnsupportedOperationException("Array.length is immutable");
+            }
             return NativeArray.wrap(that).replace(Integer.parseInt(name), value);
         }
 
         @Override
         public IClass<?> type(ResolveStatus _s, IClass<?> that, String name) {
+            if(ARRAY_LENGTH.equals(name)) {
+                return IClass.INT;
+            }
             return that.component();
         }
     };
@@ -277,6 +292,62 @@ public abstract class ResolveModel extends SerializableCode {
         }
     };
 
+    public static ResolveModel CONTEXT = new ResolveModel() {
+        @Override
+        public boolean matches(Object that, String name) {
+            return (that instanceof ResolveContext) &&
+                ((ResolveContext) that).hasResolveProperty(name);
+        }
+
+        @Override
+        public boolean matchesType(IClass<?> that, String _name) {
+            return that!=IClass.NULL && IClass.typeinfo(ResolveContext.class).isSuper(that);
+        }
+
+        @Override
+        public Object get(ResolveStatus status, Object that, String name) {
+            Optional<Object> out = ((ResolveContext) that).getResolveProperty(name);
+            if(out.isPresent()) {
+                Object result = out.get();
+                if(result instanceof ResolvePattern) {
+                    ResolvePattern pattern = (ResolvePattern) result;
+                    return pattern.get(that);
+                }
+                if(result instanceof ResolveFormat) {
+                    ResolveFormat format = (ResolveFormat) result;
+                    return format.format(that);
+                }
+                return result;
+            } else {
+                status.error = true;
+                return null;
+            }
+        }
+
+        @Override
+        public void set(ResolveStatus _s, Object that, String name, Object value) {
+            throw new UnsupportedOperationException("ResolveContext is immutable.");
+        }
+
+        @Override
+        public Object replace(ResolveStatus _s, Object that, String name, Object value) {
+            throw new UnsupportedOperationException("ResolveContext is immutable.");
+        }
+
+        @Override
+        public IClass<?> type(ResolveStatus status, IClass<?> that, String name) {
+            Object result = get(status, that, name);
+            if(result instanceof ResolvePattern) {
+                ResolvePattern pattern = (ResolvePattern) result;
+                return pattern.type(that);
+            }
+            if(result instanceof ResolveFormat) {
+                return IClass.STRING;
+            }
+            return status.error ? null : IClass.typefor(result);
+        }
+    };
+
     protected static final class ResolveStatus {
 
         public boolean error;

+ 1 - 0
assira/src/main/java/net/ranides/assira/reflection/ResolveModels.java

@@ -10,6 +10,7 @@ public class ResolveModels implements Serializable {
     private static final long serialVersionUID = 1L;
 
     public static final  ResolveModels DEFAULT = new ResolveModels(new ResolveModel[]{
+            ResolveModel.CONTEXT,
             ResolveModel.ARRAY,
             ResolveModel.LIST,
             ResolveModel.MAP,

+ 12 - 0
assira/src/main/java/net/ranides/assira/reflection/walker/ObjectVisitor.java

@@ -7,6 +7,8 @@ import java.util.function.Predicate;
 
 public interface ObjectVisitor {
 
+    boolean visitRoot(ObjectContext<?> ctx);
+
     boolean visitObject(ObjectContext<?> ctx);
 
     boolean visitArray(WalkerContexts.ArrayContext<?> ctx);
@@ -31,6 +33,11 @@ public interface ObjectVisitor {
         @Override
         public abstract boolean visitObject(ObjectContext<?> ctx);
 
+        @Override
+        public boolean visitRoot(ObjectContext<?> ctx) {
+            return true;
+        }
+
         @Override
         public boolean visitArray(WalkerContexts.ArrayContext<?> ctx) {
             return true;
@@ -67,6 +74,11 @@ public interface ObjectVisitor {
         @Override
         public abstract boolean visitObject(ObjectContext<?> context);
 
+        @Override
+        public boolean visitRoot(ObjectContext<?> ctx) {
+            return true;
+        }
+
         @Override
         public boolean visitArray(WalkerContexts.ArrayContext<?> ctx) {
             return visitObject(ctx);

+ 21 - 7
assira/src/main/java/net/ranides/assira/reflection/walker/ObjectWalker.java

@@ -13,6 +13,7 @@ import net.ranides.assira.reflection.walker.WalkerContexts.ObjectContext;
 
 import java.util.*;
 import java.util.function.Predicate;
+import java.util.function.Supplier;
 
 public class ObjectWalker {
 
@@ -46,8 +47,11 @@ public class ObjectWalker {
     }
 
     private void start(String rootname) {
-        CObject<Object> ctx = new CObject<>(null, null, IClass.OBJECT, null);
-        ctx.context(rootname, IClass.typefor(root), root).accept(visitor);
+        CObject<Object> bootstrap = new CObject<>(null, null, IClass.OBJECT, null);
+        ObjectContext<Object> root = bootstrap.context(rootname, IClass.typefor(this.root), this.root);
+        if(visitor.visitRoot(root)) {
+            root.accept(visitor);
+        }
     }
 
     private void mark(ObjectContext<?> ctx) {
@@ -71,7 +75,7 @@ public class ObjectWalker {
         private final LazyFunction<String> namePath = LazyReference.concurrent();
         private final LazyFunction<String> idPath = LazyReference.concurrent();
 
-        private String text;
+        private final Supplier<Map<String, Object>> custom = LazyReference.concurrent(HashMap::new);
 
         public CAbstract(ObjectContext<?> parent, IField field, Object value) {
             this.field = Optional.of(field);
@@ -184,13 +188,23 @@ public class ObjectWalker {
         }
 
         @Override
-        public void text(String value) {
-            this.text = value;
+        public ResolveContext getResolveParent() {
+            return parent;
+        }
+
+        @Override
+        public Map<String, Object> getResolveMap() {
+            return custom.get();
+        }
+
+        @Override
+        public void custom(Map<String, ?> values) {
+            custom.get().putAll(values);
         }
 
         @Override
-        public String text() {
-            return ValueUtils.or(text, String.valueOf(value));
+        public void custom(String key, Object value) {
+            custom.get().put(key, value);
         }
 
         @Override

+ 5 - 5
assira/src/main/java/net/ranides/assira/reflection/walker/WalkerContexts.java

@@ -11,7 +11,7 @@ import java.util.Set;
 
 public class WalkerContexts {
 
-    public interface ObjectContext<T> {
+    public interface ObjectContext<T> extends ResolveContext {
 
         IClass<ObjectContext<?>> KIND = new TypeToken<ObjectContext<?>>() {};
 
@@ -29,10 +29,6 @@ public class WalkerContexts {
 
         T value();
 
-        void text(String value);
-
-        String text();
-
         long id();
 
         long ref();
@@ -69,6 +65,10 @@ public class WalkerContexts {
 
         <S> ObjectContext<S> context(String name, IClass<S> declared, Object value);
 
+        void custom(Map<String, ?> values);
+
+        void custom(String key, Object value);
+
     }
 
     public interface ArrayContext<T> extends ObjectContext<T> {

+ 29 - 13
assira/src/main/java/net/ranides/assira/reflection/walker/WalkerRules.java

@@ -18,6 +18,7 @@ import java.io.Writer;
 import java.time.temporal.Temporal;
 import java.util.ArrayList;
 import java.util.Collection;
+import java.util.HashMap;
 import java.util.List;
 import java.util.Map;
 import java.util.function.BiConsumer;
@@ -27,34 +28,35 @@ import java.util.function.Function;
 public class WalkerRules {
 
     public static final WalkerRules DEFAULT = new WalkerRules()
+        .tag("PATH", "{id,text,4}.{namePath,text,-50}")
         .rule()
             .match(ObjectContext::isVisited)
-            .term("{id,text,4}.{namePath,text,-50} <ref> = #{ref}")
+            .term("{PATH} <ref> = #{ref}")
         .rule()
             .match(ctx -> ctx.depth() > 32)
-            .term("{id,text,4}.{namePath,text,-50} ...")
+            .term("{PATH} ...")
         .rule()
             .match(ObjectContext::isNull)
-            .term("{id,text,4}.{namePath,text,-50} {typeName} = <null>")
+            .term("{PATH} {typeName} = <null>")
         .rule()
             .match(String.class)
-            .term("{id,text,4}.{namePath,text,-50} {actual.shortname} = '{value}'")
+            .term("{PATH} {actual.shortname} = '{value}'")
         .rule()
             .match(byte[].class)
             .text(ctx -> FormatHex.format(ctx.value()))
-            .term("{id,text,4}.{namePath,text,-50} {actual.shortname} = '{text}'")
+            .term("{PATH} {actual.shortname} = '{value}'")
         .rule()
             .match(char[].class)
             .text(ctx -> String.valueOf(ctx.value()))
-            .term("{id,text,4}.{namePath,text,-50} {actual.shortname} = '{text}'")
+            .term("{PATH} {actual.shortname} = '{value}'")
         .rule()
             .match(ArrayContext.class::isInstance)
-            .hint("{id,text,4}.{namePath,text,-50} {actual.shortname}.size = {size}")
+            .hint("{PATH} {actual.shortname}.size = {size}")
         .rule()
             .match(List.class)
             .match(Collection.class)
             .match(Map.class)
-            .hint("{id,text,4}.{namePath,text,-50} {typeName}.size = {size}")
+            .hint("{PATH} {typeName}.size = {size}")
         .rule()
             .match(java.util.Date.class)
             .match(java.sql.Date.class)
@@ -62,12 +64,14 @@ public class WalkerRules {
             .match(Temporal.class)
             .match(Number.class)
             .match(ObjectContext::isPrimitive)
-            .term("{id,text,4}.{namePath,text,-50} {actual.shortname} = {value}")
+            .term("{PATH} {actual.shortname} = {value}")
         .rule()
             .match(Object.class)
-            .hint("{id,text,4}.{namePath,text,-50} {actual.shortname} = {value}")
+            .hint("{PATH} {actual.shortname} = {value}")
         ;
 
+    private final Map<String, ResolveFormat> tags = new HashMap<>();
+
     private final List<RuleEntry> rules = new ArrayList<>();
 
     public PrintVisitor visitor(OutputStream target) {
@@ -76,6 +80,12 @@ public class WalkerRules {
 
     public PrintVisitor visitor(Writer target) {
         return new PrintVisitor(new PrintWriter(target)){
+            @Override
+            public boolean visitRoot(ObjectContext<?> context) {
+                context.custom(tags);
+                return true;
+            }
+
             @Override
             public boolean visitObject(ObjectContext<?> context) {
                 for (RuleEntry rule : rules) {
@@ -89,10 +99,16 @@ public class WalkerRules {
     }
 
     public WalkerRules inherit(WalkerRules parent) {
+        this.tags.putAll(parent.tags);
         this.rules.addAll(parent.rules);
         return this;
     }
 
+    public WalkerRules tag(String name, String value) {
+        this.tags.put(name, ResolveFormat.compile(value));
+        return this;
+    }
+
     public RuleBuilderRoot rule() {
         return new RuleBuilderRoot();
     }
@@ -155,7 +171,7 @@ public class WalkerRules {
         }
 
         public RuleBuilderRoot text(Function<ObjectContext<Object>, String> formatter) {
-            this.formatter = ctx -> ctx.text(formatter.apply(ctx));
+            this.formatter = ctx -> ctx.custom("value", formatter.apply(ctx));
             return this;
         }
 
@@ -182,7 +198,7 @@ public class WalkerRules {
         }
 
         public RuleBuilderType<F> text(Function<ObjectContext<F>, String> formatter) {
-            this.formatter = ctx -> ctx.text(formatter.apply(ctx));
+            this.formatter = ctx -> ctx.custom("value", formatter.apply(ctx));
             return this;
         }
 
@@ -209,7 +225,7 @@ public class WalkerRules {
         }
 
         public RuleBuilderMore text(Function<ObjectContext<Object>, String> formatter) {
-            this.formatter = ctx -> ctx.text(formatter.apply(ctx));
+            this.formatter = ctx -> ctx.custom("value", formatter.apply(ctx));
             return this;
         }
 

+ 31 - 8
assira/src/test/java/net/ranides/assira/reflection/ResolvePatternTest.java

@@ -19,6 +19,7 @@ import net.ranides.assira.generic.Wrapper;
 import net.ranides.assira.reflection.mockup.ForResolver.*;
 import net.ranides.assira.reflection.util.ResolveUtils;
 
+import net.ranides.assira.text.ResolveFormat;
 import org.junit.Assume;
 import org.junit.Test;
 import static net.ranides.assira.junit.NewAssert.*;
@@ -245,12 +246,8 @@ public class ResolvePatternTest {
 
     @Test
     public void testArray() {
-        try {
-            A context = new A();
-            assertEquals(3, ResolveUtils.get(context, "array.length"));
-        } catch (ResolveException e) {
-            Assume.assumeTrue("Sorry, we can't resolve array.length", false);
-        }
+        A context = new A();
+        assertEquals(3, ResolveUtils.get(context, "array.length"));
     }
     
     @Test
@@ -386,6 +383,32 @@ public class ResolvePatternTest {
             ResolvePattern.compile(strategy, "list[1].unknown.some").replace(context, 0);
         });
     }
-    
-        
+
+    @Test
+    public void testContext() {
+        APlus data = new APlus();
+
+        data.getResolveMap().put("customized", 77);
+        data.getResolveMap().put("customized/big", 99);
+        data.getResolveMap().put("innerlink", new A());
+        data.getResolveMap().put("other.dot", 99);
+        data.getResolveMap().put("alias", ResolvePattern.compile("array.1.name"));
+        data.getResolveMap().put("template", ResolveFormat.compile("({array.1.name}:{content.item.height}:{customized})"));
+
+        assertEquals(16, ResolveUtils.get(data, "content.item.height"));
+        assertEquals("b", ResolveUtils.get(data, "array.1.name"));
+
+        assertEquals(77, ResolveUtils.get(data, "customized"));
+        assertEquals(99, ResolveUtils.get(data, "customized/big"));
+        assertEquals(16, ResolveUtils.get(data, "innerlink.content.item.height"));
+
+        assertThrows(ResolveException.class, () ->{
+            assertEquals(99, ResolveUtils.get(data, "other.dot"));
+        });
+
+        // very advanced functionality: support for nested patterns inside context
+        // every ResolvePattern or ResolveFormat stored inside context is evaluated
+        assertEquals("b", ResolveUtils.get(data, "alias"));
+        assertEquals("(b:16:77)", ResolveUtils.get(data, "template"));
+    }
 }

+ 16 - 0
assira/src/test/java/net/ranides/assira/reflection/mockup/ForResolver.java

@@ -8,6 +8,8 @@
 package net.ranides.assira.reflection.mockup;
 
 import edu.umd.cs.findbugs.annotations.SuppressFBWarnings;
+import net.ranides.assira.reflection.ResolveContext;
+
 import java.util.Arrays;
 import java.util.HashMap;
 import java.util.List;
@@ -43,6 +45,20 @@ public final class ForResolver {
         
         public Map<Integer, C> nullmap;
     }
+
+    public static class APlus extends A implements ResolveContext {
+        private final Map<String, Object> context = new HashMap<>();
+
+        @Override
+        public ResolveContext getResolveParent() {
+            return null;
+        }
+
+        @Override
+        public Map<String, Object> getResolveMap() {
+            return context;
+        }
+    }
     
     public static class B {
         public E item = new E(32,16,1000);