Procházet zdrojové kódy

new: generic.Wrapper - implemented by "CompareWrapper" / "Ref" / etc
change: LexicalCast - TypeToken overloads came back

change: Resolver#set - returns void
new: Resolver#reference - returns Wrapper for value (so you can call set/get/put/type)

Ranides Atterwim před 11 roky
rodič
revize
b57eb33554

+ 1 - 1
pom.xml

@@ -5,7 +5,7 @@
 
     <groupId>net.ranides</groupId>
     <artifactId>assira</artifactId>
-    <version>0.72.1</version>
+    <version>0.72.2</version>
     <packaging>jar</packaging>
 
     <name>assira</name>

+ 21 - 2
src/main/java/net/ranides/assira/generic/CompareWrapper.java

@@ -6,6 +6,9 @@
  */
 package net.ranides.assira.generic;
 
+import java.util.Set;
+import net.ranides.assira.reflection.GenericClass;
+
 /**
  * Abstrakcyjna klasa umożliwiająca opakowanie wartości i przeładowanie  metody
  * porównującej elementy. Przydatna, gdy chcemy umieścić w kontenerze {@link Set}
@@ -15,7 +18,7 @@ package net.ranides.assira.generic;
  * @author ranides
  * 
  */
-public abstract class CompareWrapper<T> {
+public abstract class CompareWrapper<T> implements Wrapper<T> {
     
     private final T value;
 
@@ -31,10 +34,26 @@ public abstract class CompareWrapper<T> {
      * Opakowana wartość.
      * @return
      */
-    public T value() {
+    @Override
+    public T get() {
         return value;
     }
 
+    @Override
+    public GenericClass type() {
+        return new TypeToken<T>() {};
+    }
+
+    @Override
+    public void set(T value) {
+        throw new UnsupportedOperationException("Immutable.");
+    }
+
+    @Override
+    public void put(Object value) {
+        throw new UnsupportedOperationException("Immutable.");
+    }
+
     @Override
     @SuppressWarnings("unchecked")
     public boolean equals(Object object) {

+ 17 - 2
src/main/java/net/ranides/assira/generic/Ref.java

@@ -7,6 +7,8 @@
 package net.ranides.assira.generic;
 
 import java.io.Serializable;
+import net.ranides.assira.reflection.GenericClass;
+import net.ranides.assira.text.LexicalCast;
 
 /**
  * Wrapper na referencję obiektu. Jedno z nielicznych zastosowań (zresztą dość
@@ -19,9 +21,9 @@ import java.io.Serializable;
  * @param <T>
  * @author ranides
  */
-public class Ref<T> implements Serializable {
+public class Ref<T> implements Wrapper<T>, Serializable {
 
-    private static final long serialVersionUID = 1L;
+    private static final long serialVersionUID = 2L;
 
     private T value;
 
@@ -37,6 +39,7 @@ public class Ref<T> implements Serializable {
      * Zwraca wartość wskazywaną przez referencję
      * @return
      */
+    @Override
     public T get() {
         return value;
     }
@@ -45,7 +48,19 @@ public class Ref<T> implements Serializable {
      * Zmienia wartość przechowywaną w referencji
      * @param value
      */
+    @Override
     public void set(T value) {
         this.value = value;
     }
+    
+    @Override
+    public void put(Object value) {
+        this.value = LexicalCast.cast(value, type());
+    }
+
+    @Override
+    public GenericClass type() {
+        return new TypeToken<T>() { };
+    }
+
 }

+ 1 - 3
src/main/java/net/ranides/assira/generic/TypeToken.java

@@ -58,7 +58,7 @@ import net.ranides.assira.reflection.ReflectUtils;
  * @author ranides
  */
 public abstract class TypeToken<T> implements Comparable<TypeToken<T>>, GenericClass {
-
+    
     private final Type type;
 
     /**
@@ -169,6 +169,4 @@ public abstract class TypeToken<T> implements Comparable<TypeToken<T>>, GenericC
         return result;
     }
 
-    public static abstract class TT<Type> extends TypeToken<Type> { }
-
 }

+ 25 - 0
src/main/java/net/ranides/assira/generic/Wrapper.java

@@ -0,0 +1,25 @@
+/*
+ * @author Ranides Atterwim <ranides@gmail.com>
+ * @copyright Ranides Atterwim
+ * @license WTFPL
+ * @url http://ranides.net/projects/assira
+ */
+package net.ranides.assira.generic;
+
+import net.ranides.assira.reflection.GenericClass;
+
+/**
+ *
+ * @author Ranides Atterwim <ranides@gmail.com>
+ */
+public interface Wrapper<T> {
+    
+    GenericClass type();
+    
+    T get();
+    
+    void set(T value);
+    
+    void put(Object value);
+    
+}

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

@@ -272,7 +272,7 @@ public final class ClassInspector {
     }
 
     /**
-     * Sprawdza, czy podany obiekt {@code value} jest instancją klasy {@code ssuper}.
+     * Sprawdza, czy podany obiekt {@code get} jest instancją klasy {@code ssuper}.
      * Uwzględnia autoboxing argumentów.
      * @param ssuper
      * @param value
@@ -359,7 +359,7 @@ public final class ClassInspector {
         Method[] result = new Method[found.size()];
         int i=0;
         for(MethodWrapper item : found) {
-            result[i++] = item.value();
+            result[i++] = item.get();
         }
         return result;
     }

+ 154 - 4
src/main/java/net/ranides/assira/reflection/Resolver.java

@@ -7,13 +7,17 @@
 package net.ranides.assira.reflection;
 
 import java.io.Serializable;
+import java.lang.ref.Reference;
 import java.util.ArrayList;
 import java.util.List;
 import java.util.Map;
 import java.util.regex.Matcher;
 import java.util.regex.Pattern;
 import net.ranides.assira.collection.ArrayUtils;
+import net.ranides.assira.generic.TypeToken;
 import net.ranides.assira.generic.ValueUtils;
+import net.ranides.assira.generic.Wrapper;
+import net.ranides.assira.text.LexicalCast;
 import net.ranides.assira.text.Strings;
 
 /**
@@ -76,8 +80,12 @@ public final class Resolver implements Serializable {
         return Resolver.compile(expression).get(object);
     }
     
-    public static Object set(Object object, String expression, Object value) {
-        return Resolver.compile(expression).set(object, value);
+    public static void set(Object object, String expression, Object value) {
+        Resolver.compile(expression).set(object, value);
+    }
+    
+    public static Wrapper<Object> reference(Object object, String expression) {
+        return Resolver.compile(expression).reference(object);
     }
     
     public Object get(Object object) {
@@ -88,14 +96,57 @@ public final class Resolver implements Serializable {
         return context;
     }
     
-    public Object set(Object object, Object value) {
+    public void set(Object object, Object value) {
         Object context = object;
         int n = tokens.length-1;
         for(int i=0; i<n; i++) {
             context = tokens[i].get(context);
         }
         tokens[n].set(context, value);
-        return context;
+    }
+   
+    public Wrapper<Object> reference(Object object) {
+        TypeContext context = new TypeContext().bind(object);
+        int n = tokens.length-1;
+        for(int i=0; i<n; i++) {
+            context = tokens[i].typeof(context);
+        }
+        GenericClass type = tokens[n].typeof(context).type;
+        return new RRef(context.value, type, tokens[n]);
+    }
+    
+    private static class RRef implements Wrapper<Object> {
+        
+        private final Object context;
+        private final GenericClass type;
+        private final Token token;
+
+        private RRef(Object context, GenericClass type, Token token) {
+            this.context = context;
+            this.type = type;
+            this.token = token;
+        }
+
+        @Override
+        public GenericClass type() {
+            return type;
+        }
+        
+        @Override
+        public Object get() {
+            return token.get(context);
+        }
+        
+        @Override
+        public void set(Object value) {
+            token.set(context, value);
+        }
+        
+        @Override
+        public void put(Object value) {
+            token.set(context, LexicalCast.cast(value, type));
+        }
+
     }
     
     public String expression() {
@@ -169,6 +220,8 @@ public final class Resolver implements Serializable {
         
         void set(Object context, Object value);
         
+        TypeContext typeof(TypeContext context);
+        
     }
     
     private static class AToken implements Token {
@@ -200,6 +253,39 @@ public final class Resolver implements Serializable {
             }
             ArrayUtils.wrap(context).set(index, value);
         }
+
+        @Override
+        public TypeContext typeof(TypeContext context) {
+            if(context.is(Matcher.class)) {
+                TypeContext ret = new TypeContext(String.class);
+                if(null != context.value) {
+                    ret.bind( ValueUtils.or(((Matcher)context.value).group(index), "") );
+                }
+                return ret;
+            }
+            if(context.is(CharSequence.class)) {
+                TypeContext ret = new TypeContext(char.class);
+                if(null != context.value) {
+                    ret.bind(((CharSequence)context.value).charAt(index));
+                }
+                return ret;
+            }
+            if(context.type.rawType().isArray()) {
+                TypeContext ret = new TypeContext(context.type.rawType().getComponentType());
+                if(null != context.value) {
+                    ret.bind( ArrayUtils.wrap(context.value).get(index) );
+                }
+                return ret;
+            }
+            if(context.value instanceof List) {
+                TypeContext ret = new TypeContext(context.type.params()[0]);
+                if(null != context.value) {
+                    ret.bind( ArrayUtils.wrap(context.value).get(index) );
+                }
+                return ret;
+            }
+            throw new IllegalArgumentException("Invalid argument, type is not an array: " + context);
+        }
         
     }
     
@@ -220,6 +306,17 @@ public final class Resolver implements Serializable {
         public void set(Object context, Object value) {
             BeanModel.fromObject(context).put(context, index, value);
         }
+
+        @Override
+        public TypeContext typeof(TypeContext context) {
+            if( null == context.value ) {
+                BeanModel model = BeanModel.fromClass(context.type.rawType());
+                return new TypeContext(model.getGenericType(index));
+            } else {
+                BeanModel model = BeanModel.fromObject(context.value);
+                return new TypeContext(model.getGenericType(index)).bind(model.get(context.value, index));
+            }
+        }
                 
     }
     
@@ -240,6 +337,59 @@ public final class Resolver implements Serializable {
         public void set(Object context, Object value) {
             ((Map<String,Object>)context).put(key, value);
         }
+
+        @Override
+        public TypeContext typeof(TypeContext context) {
+            if( null == context.value) {
+                return new TypeContext( context.type.params()[1]);
+            } else {
+                return new TypeContext( context.type.params()[1])
+                    .bind(((Map<?,?>)context.value).get(key));
+            }
+        }
+        
+    }
+    
+    private static class TypeContext {
+        
+        private GenericClass type;
+        private Object value;
+        
+        public TypeContext() {
+            // do nothing
+        }
+        
+        public TypeContext(Class<?> type) {
+            this(GenericFactory.construct(type));
+        }
+
+        public TypeContext(GenericClass type) {
+            this.type = type;
+            this.value = null;
+        }
+        
+        public boolean is(Class<?> type) {
+            if(value != null && type.isInstance(value)) {
+                return true;
+            }
+            return type.isAssignableFrom(this.type.rawType());
+        }
+        
+        public TypeContext bind(Object value) {
+            this.value = value;
+            if( null == value ) {
+                return this;
+            }
+            if((null == type) || (!type.isParametrised() && !type.rawType().isPrimitive()) ) {
+                this.type = GenericFactory.construct(value.getClass());
+            }
+            return this;
+        }
+
+        @Override
+        public String toString() {
+            return "TypeContext{" + "type=" + type + ", value=" + value + '}';
+        }
         
     }
 }

+ 14 - 3
src/main/java/net/ranides/assira/text/LexicalCast.java

@@ -17,6 +17,7 @@ import net.ranides.assira.collection.ArrayUtils;
 import net.ranides.assira.enums.EnumUtils;
 import net.ranides.assira.generic.Function;
 import net.ranides.assira.generic.TypeToken;
+import net.ranides.assira.reflection.GenericClass;
 import net.ranides.assira.time.DateQuickFormat;
 
 /**
@@ -100,9 +101,14 @@ public final class LexicalCast {
             throw new LexicalCastException(clazz + " cannot be created from JSON", cause);
         }
     }
-
+    
     @SuppressWarnings("unchecked")
     public static <T> T fromJSON(String value, TypeToken<T> token) throws LexicalCastException {
+        return fromJSON(value, (GenericClass)token);
+    }
+
+    @SuppressWarnings("unchecked")
+    public static <T> T fromJSON(String value, GenericClass token) throws LexicalCastException {
         try {
             return (T)MAPPER.readValue(value, TYPE_FACTORY.constructType( token.type() ) );
         } catch(IOException cause) {
@@ -114,14 +120,19 @@ public final class LexicalCast {
     public static <T> T cast(Object value, Class<T> clazz) throws ClassCastException {
         return (T)rawCast(value, null, clazz);
     }
-
+    
     @SuppressWarnings("unchecked")
     public static <T> T cast(Object value, TypeToken<T> token) throws LexicalCastException {
+        return cast(value, (GenericClass)token);
+    }
+
+    @SuppressWarnings("unchecked")
+    public static <T> T cast(Object value, GenericClass token) throws LexicalCastException {
         return (T)rawCast(value, token, TYPE_FACTORY.constructType( token.type() ).getRawClass());
     }
 
     @SuppressWarnings("PMD")
-    private static Object rawCast(Object value, TypeToken<?> token, Class<?> clazz) {
+    private static Object rawCast(Object value, GenericClass token, Class<?> clazz) {
         // brak konwersji null
         if(value==null) {
             return null;

+ 4 - 4
src/test/java/net/ranides/assira/BenchmarkRunner.java

@@ -73,10 +73,10 @@ public final class BenchmarkRunner {
         String[] args = new String[]{
             "--trials", 
             "3",
-            "--warmupMillis", 
-            "100",
-            "--runMillis", 
-            "100",
+//            "--warmupMillis", 
+//            "100",
+//            "--runMillis", 
+//            "100",
             "-Dparam=0",
             clazz.getName()
         };

+ 135 - 6
src/test/java/net/ranides/assira/reflection/ResolverTest.java

@@ -11,10 +11,10 @@ import java.util.Arrays;
 import java.util.HashMap;
 import java.util.List;
 import java.util.Map;
+import java.util.TreeMap;
 import java.util.regex.Matcher;
 import java.util.regex.Pattern;
 import net.ranides.assira.generic.Serializer;
-import net.ranides.assira.text.ResolveFormatter;
 import net.ranides.assira.text.format.AnsiString;
 import org.junit.Test;
 import static org.junit.Assert.*;
@@ -58,7 +58,7 @@ public class ResolverTest {
         
         // get: map, list, array
         assertEquals((Integer)801, Resolver.get(context, "content.item.hello"));
-        assertEquals((Integer)7, Resolver.get(context, "list[1].numbers#seven"));
+        assertEquals((Long)7L, Resolver.get(context, "list[1].numbers#seven"));
         assertNull(Resolver.get(context, "list[1].numbers#eight"));
         assertEquals("B", Resolver.get(context, "list[1].name"));
         assertEquals("b", Resolver.get(context, "array[1].name"));
@@ -149,6 +149,20 @@ public class ResolverTest {
         Matcher hit = Pattern.compile("([a-z]+) ([a-z]+) ([a-z]+)").matcher("one two last");
         assertTrue(hit.find());
         assertEquals("two", Resolver.get(hit, "2"));
+        
+        try {
+            Resolver.set(hit, "2", "text");
+            fail("UnsupportedOperationException expected");
+        } catch(UnsupportedOperationException cause) {
+            assertTrue(cause.getMessage().contains("Matcher"));
+        }
+        
+        try {
+            Resolver.set(hit, "2[1]", 'R');
+            fail("UnsupportedOperationException expected");
+        } catch(UnsupportedOperationException cause) {
+            assertTrue(cause.getMessage().contains("CharSequence"));
+        }
     }
     
     @Test
@@ -157,18 +171,125 @@ public class ResolverTest {
         AnsiString b = new AnsiString("uAdd");
         assertEquals('R', Resolver.get(a, "2"));
         assertEquals('A', Resolver.get(b, "1"));
+        
+        try {
+            Resolver.set(a, "2", 'S');
+            fail("UnsupportedOperationException expected");
+        } catch(UnsupportedOperationException cause) {
+            assertTrue(cause.getMessage().contains("CharSequence"));
+        }
+    }
+    
+    @Test
+    public void testReferenceType() {
+        A context = new A();
+        int numbers[] = new int[]{1,2,3,4,5,6};
+        
+        assertEquals("int", Resolver.reference(numbers, "1").type().toString() );
+        assertEquals("int", Resolver.reference(context, "x").type().toString() );
+        
+        assertEquals("B", Resolver.reference(context, "content").type().toString() );
+        assertEquals("C", Resolver.reference(context, "array[2]").type().toString() );
+        
+        assertEquals("List<C>", Resolver.reference(context, "list").type().toString() );
+        assertEquals("C", Resolver.reference(context, "list[0]").type().toString() );
+        assertEquals("CN", Resolver.reference(context, "list[1]").type().toString() );
+        
+        assertEquals("Map<String, Number>", Resolver.reference(context, "list[1].numbers").type().toString() );
+        assertEquals("Integer", Resolver.reference(context, "list[1].numbers#two").type().toString() );
+        assertEquals("Long", Resolver.reference(context, "list[1].numbers#seven").type().toString() );
+        assertEquals("Number", Resolver.reference(context, "list[1].numbers#unknown").type().toString() );
+        
+        assertEquals("Map<Integer, C>", Resolver.reference(context, "empty").type().toString() );
+        assertEquals("C", Resolver.reference(context, "empty#22").type().toString() );
+        assertEquals("String", Resolver.reference(context, "empty#22.name").type().toString() );
+        
+        assertEquals("Map<Integer, C>", Resolver.reference(context, "nullmap").type().toString() );
+        assertEquals("C", Resolver.reference(context, "nullmap#22").type().toString() );
+        assertEquals("String", Resolver.reference(context, "nullmap#22.name").type().toString() );
+        
+        assertEquals("char", Resolver.reference(context, "nullmap#22.name[88]").type().toString() );
+        assertEquals("char", Resolver.reference(context, "list[1].name[0]").type().toString() );
+        
+        Matcher hit = Pattern.compile("([a-z]+) ([a-z]+) ([a-z]+)").matcher("one two last");
+        assertTrue(hit.find());
+        
+        assertEquals("String", Resolver.reference(hit, "[1]").type().toString());
+        assertEquals("char", Resolver.reference(hit, "[1][1]").type().toString());
+        
+        try {
+            Resolver.reference(context, "list[1].numbers[77]").type();
+            fail("IllegalArgumentException expected");
+        } catch(IllegalArgumentException cause) {
+            assertTrue( cause.getMessage().contains("Map<String, Number>") );
+        }
+    }
+    
+    @Test
+    public void testReferencePut() {
+        A context = new A();
+        int numbers[] = new int[]{1,2,3,4,5,6};
+        
+        Resolver.reference(numbers, "[1]").put("44");
+        assertEquals(44, numbers[1]);
+        
+        Resolver.reference(context, "x").put("55");
+        assertEquals(55, context.x);
+        
+        Resolver.reference(context, "list[1].name").put("hello");
+        assertEquals("hello", context.list.get(1).name);
+        
+        Resolver.reference(context, "list[1].name").put(99);
+        assertEquals("99", context.list.get(1).name);
+        
+        Resolver.reference(context, "list[1].numbers#key").put(14);
+        assertEquals(14, context.list.get(1).numbers.get("key"));
+        
+        
+        assertEquals("Map<String, Number>", Resolver.reference(context, "list[1].numbers").type().toString());
+        assertEquals("Number", Resolver.reference(context, "list[1].numbers#ikey").type().toString());
+
+        Resolver.reference(context, "list[1].numbers#ikey").put(15L);
+        assertEquals("Integer", Resolver.reference(context, "list[1].numbers#ikey").type().toString());
+        assertEquals(15, context.list.get(1).numbers.get("ikey"));
+        
+        Resolver.reference(context, "list[1].numbers#long").put(0x8FFFF0000L);
+        assertEquals("Long", Resolver.reference(context, "list[1].numbers#long").type().toString());
+        assertEquals(0x8FFFF0000L, context.list.get(1).numbers.get("long"));
+    }
+    
+    @Test
+    public void testReferenceSetGet() {
+        // we tested direct get & set methods so we know that internals are OK
+        // here we just want to be sure, that "reference" delegates correctly
+        int numbers[] = new int[]{1,2,3,4,5,6};
+        
+        Resolver.reference(numbers, "[1]").set(44);
+        assertEquals(44, numbers[1]);
+        assertEquals(44, Resolver.reference(numbers, "[1]").get());
+    }
+    
+    @Test
+    public void testExpression() {
+        assertEquals("list[99].name", Resolver.compile("list[99].name").expression());
+        
     }
     
+    
     private static class A {
         public int x = 5;
         public int y = 8;
         public B content = new B();
         
-        public C[] array = new C[]{new C("a"), new C("b"), new C("c")};
+        public C[] array = new C[]{new C("a"), new CN("b"), new C("c")};
         
-        public List<C> list = Arrays.asList(new C("A"), new C("B"), new C("C"));
+        public List<C> list = Arrays.asList(new C("A"), new CN("B"), new C("C"));
         
         public D home = new D();
+        
+        public Map<Integer, C> empty = new TreeMap<>();
+        
+        public Map<Integer, C> nullmap;
     }
     
     private static class B {
@@ -177,13 +298,21 @@ public class ResolverTest {
     
     private static class C {
         public String name;
-        public Map<String, Integer> numbers = new HashMap<>();
+        public Map<String, Number> numbers = new HashMap<>();
         
         public C(String name) {
             this.name = name;
             this.numbers.put("zero", 0);
             this.numbers.put("two", 2);
-            this.numbers.put("seven", 7);
+            this.numbers.put("seven", 7L);
+        }
+        
+    }
+    
+    private static class CN extends C {
+
+        public CN(String name) {
+            super(name);
         }
         
     }

+ 7 - 0
src/test/java/net/ranides/assira/text/LexicalCastTest.java

@@ -15,6 +15,8 @@ import net.ranides.assira.generic.ValueUtils;
 import net.ranides.assira.io.FileFilter.AcceptMode;
 import net.ranides.assira.math.HashHelper;
 import net.ranides.assira.reflection.ClassInspector;
+import net.ranides.assira.reflection.GenericClass;
+import net.ranides.assira.reflection.GenericFactory;
 import org.junit.Test;
 import static org.junit.Assert.*;
 
@@ -210,6 +212,11 @@ public class LexicalCastTest {
 
         assertEquals(ArrayList.class, target.getClass());
         assertEquals(Arrays.asList(1L, 2L, 3L), target);
+
+        GenericClass type = GenericFactory.construct(List.class, GenericFactory.construct(Long.class));
+        List<Long> target2 = LexicalCast.cast(source, type);
+        assertEquals(ArrayList.class, target2.getClass());
+        assertEquals(Arrays.asList(1L, 2L, 3L), target2);
     }
     
     @Test