Переглянути джерело

change: BeanModel is permisive
new: BeanModel#verify returns list of possible violations of conventions

Ranides Atterwim 2 роки тому
батько
коміт
3ca8a211ac

+ 25 - 0
assira.core/src/main/java/net/ranides/assira/reflection/BeanException.java

@@ -0,0 +1,25 @@
+package net.ranides.assira.reflection;
+
+public class BeanException extends IReflectiveException {
+
+    private final IClass<?> type;
+
+    public BeanException(IClass<?> type, String message) {
+        super(message);
+        this.type = type;
+    }
+
+    public BeanException(IClass<?> type, String message, Throwable cause) {
+        super(message, cause);
+        this.type = type;
+    }
+
+    public BeanException(IClass<?> type, Throwable cause) {
+        super(cause);
+        this.type = type;
+    }
+
+    public IClass<?> getBeanType() {
+        return type;
+    }
+}

+ 21 - 0
assira.core/src/main/java/net/ranides/assira/reflection/BeanModel.java

@@ -9,6 +9,7 @@ package net.ranides.assira.reflection;
 
 import net.ranides.assira.reflection.impl.bean.FBeanModel;
 
+import java.util.Collection;
 import java.util.Map;
 import java.util.Optional;
 import java.util.Set;
@@ -49,6 +50,26 @@ import java.util.Set;
  */
 public interface BeanModel {
 
+    /**
+     * Verifies if specified class meets bean requirements
+     *
+     * @param type type
+     * @return error report
+     */
+    static Collection<BeanException> verify(Class<?> type) {
+        return FBeanModel.verify(IClass.typeinfo(type));
+    }
+
+    /**
+     * Verifies if specified class meets bean requirements
+     *
+     * @param type type
+     * @return error report
+     */
+    static Collection<BeanException> verify(IClass<?> type) {
+        return FBeanModel.verify(type);
+    }
+
     /**
      * Creates model for specified type
      *

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

@@ -17,9 +17,9 @@ import static net.ranides.assira.reflection.IAttribute.*;
  */
 public class IAttributes {
 
-    private static final int M_BRIDGE   = 0x00000040;
+    private static final int M_BRIDGE   = 0x0000_0040;
 
-    private static final int M_VARARGS  = 0x00000080;
+    private static final int M_VARARGS  = 0x0000_0080;
 
     private static final int ACCESS_ANY = Modifier.PUBLIC | Modifier.PROTECTED | Modifier.PRIVATE;
 

+ 28 - 8
assira.core/src/main/java/net/ranides/assira/reflection/impl/bean/FBeanModel.java

@@ -8,11 +8,12 @@
 package net.ranides.assira.reflection.impl.bean;
 
 import lombok.experimental.UtilityClass;
-
-import java.util.Collection;
 import net.ranides.assira.collection.maps.CacheMap;
 import net.ranides.assira.reflection.*;
 
+import java.util.ArrayList;
+import java.util.Collection;
+
 /**
  * Factory class for BeanModel.
  *
@@ -27,6 +28,26 @@ public final class FBeanModel {
     
     private static final CacheMap<IClass<?>,BeanModel> CACHE = CacheMap.getInstance(RBeanModel::new);
 
+    /**
+     * Verifies if specified class meets bean requirements. Skips caching.
+     *
+     * Internal method used by BeanModel#verify
+     *
+     * @param type type
+     * @return BeanModel
+     */
+    public static Collection<BeanException> verify(IClass<?> type) {
+        Collection<BeanException> errors = new ArrayList<>();
+        try {
+            new RBeanModel(type, errors);
+        } catch (BeanException e) {
+            errors.add(e);
+        } catch (Exception e) {
+            errors.add(new BeanException(type, e));
+        }
+        return errors;
+    }
+
     /**
      * Returns BeanModel for specified class. Caches results.
      *
@@ -49,11 +70,11 @@ public final class FBeanModel {
      * @param methods methods
      * @return BeanMethod
      */
-    public static BeanMethod resolve(Collection<IMethod> methods) {
+    public static BeanMethod resolve(Collection<BeanException> errors, Collection<IMethod> methods) {
         if(methods.size() > 1) {
-            return new RBeanMethods(methods);
+            return new RBeanMethods(errors, methods);
         } else {
-            return new RBeanMethod(methods.iterator().next());
+            return new RBeanMethod(errors, methods.iterator().next());
         }
     }
 
@@ -67,9 +88,8 @@ public final class FBeanModel {
      * @param info info
      * @return BeanProperty
      */
-    public static BeanProperty resolve(RBeanPropertyBuilder info) {
-        info.validate();
-        if(info.type() == null) {
+    public static BeanProperty resolve(Collection<BeanException> errors, RBeanPropertyBuilder info) {
+        if(!info.validate(errors)) {
             return null;
         }
         if( info.type().attributes().has(IAttribute.ARRAY) ) {

+ 3 - 2
assira.core/src/main/java/net/ranides/assira/reflection/impl/bean/RBeanMethod.java

@@ -12,6 +12,7 @@ import net.ranides.assira.functional.special.AnyFunction;
 import net.ranides.assira.reflection.*;
 
 import java.io.Serializable;
+import java.util.Collection;
 
 /**
  * Default implementation of BeanMethod.
@@ -23,7 +24,7 @@ public class RBeanMethod implements BeanMethod, Serializable {
     
     private final IMethod method;
     
-    RBeanMethod(IMethod method) {
+    RBeanMethod(Collection<BeanException> errors, IMethod method) {
         this.method = method;
 
         if(!method.parent().attributes().has(IAttribute.PUBLIC)) {
@@ -54,7 +55,7 @@ public class RBeanMethod implements BeanMethod, Serializable {
     @Override
     public BeanMethodBinding bind(Object that) {
         if(null == that) {
-            throw new IReflectiveException("non-static method requires non-null 'this'");
+            throw new BeanException(method.parent(), "non-static method requires non-null 'this'");
         }
         return new RBinding(that);
     }

+ 7 - 6
assira.core/src/main/java/net/ranides/assira/reflection/impl/bean/RBeanMethods.java

@@ -27,7 +27,7 @@ public class RBeanMethods implements BeanMethod, Serializable {
 
     private final IMethod[] methods;
     
-    RBeanMethods(Collection<IMethod> methods) {
+    RBeanMethods(Collection<BeanException> errors, Collection<IMethod> methods) {
         this.methods = methods.toArray(new IMethod[0]);
     }
 
@@ -45,6 +45,7 @@ public class RBeanMethods implements BeanMethod, Serializable {
     //
     // maybe we should implement something tricky like... composite IClass for returns?
     // something like composite arguments for arguments?
+    // or we should put error into validation
 
     @Override
     public IClass<?> returns() {
@@ -59,7 +60,7 @@ public class RBeanMethods implements BeanMethod, Serializable {
     @Override
     public BeanMethodBinding bind(Object that) {
         if(null == that) {
-            throw new IReflectiveException("non-static method requires non-null 'this'");
+            throw new BeanException(parent(), "non-static method requires non-null 'this'");
         }
         return new RBinding(that);
     }
@@ -71,7 +72,7 @@ public class RBeanMethods implements BeanMethod, Serializable {
                 return m.invoke(that);
             }
         }
-        throw new IReflectiveException("method " + name() + " can't be invoked with no arguments.");
+        throw new BeanException(parent(), "method " + name() + " can't be invoked with no arguments.");
     }
 
     @Override
@@ -81,7 +82,7 @@ public class RBeanMethods implements BeanMethod, Serializable {
                 return m.invoke(that, arguments);
             }
         }
-        throw new IReflectiveException("method " + name() + " can't be invoked with passed arguments.");
+        throw new BeanException(parent(), "method " + name() + " can't be invoked with passed arguments.");
     }
 
     @Override
@@ -91,7 +92,7 @@ public class RBeanMethods implements BeanMethod, Serializable {
                 return m.call(arguments);
             }
         }
-        throw new IReflectiveException("method " + name() + " can't be invoked with passed arguments.");
+        throw new BeanException(parent(), "method " + name() + " can't be invoked with passed arguments.");
     }
 
     @Override
@@ -101,7 +102,7 @@ public class RBeanMethods implements BeanMethod, Serializable {
                 return m.apply(that, arguments);
             }
         }
-        throw new IReflectiveException("method " + name() + " can't be invoked with passed arguments.");
+        throw new BeanException(parent(), "method " + name() + " can't be invoked with passed arguments.");
     }
     
     

+ 18 - 9
assira.core/src/main/java/net/ranides/assira/reflection/impl/bean/RBeanModel.java

@@ -42,14 +42,22 @@ public class RBeanModel implements BeanModel, Serializable {
     private final Set<String> keys;
 
     RBeanModel(IClass<?> type) {
+        this(type, null);
+    }
+
+    RBeanModel(IClass<?> type, Collection<BeanException> errors) {
         this.type = type;
         
-        Map<String, RBeanPropertyBuilder> pinfo = new LazyMap<>(new OpenMap<>(), RBeanPropertyBuilder::new);
+        Map<String, RBeanPropertyBuilder> pinfo = new LazyMap<>(new OpenMap<>(), name -> new RBeanPropertyBuilder(type, name));
         MultiMap<String, IMethod> minfo = new OpenMultiMap<>();
 
         fluentGet = new OpenMap<>();
         fluentSet = new OpenMap<>();
 
+        if(errors!=null && !type.isPublic()) {
+            errors.add(new BeanException(type, "Unsafe reflective non-public scan: " + type));
+        }
+
         if(RuntimeConfiguration.FEATURE_BEAN_MODEL_FOR_PRIVATE() || type.isPublic()) {
             for (IMethod m : type.methods()) {
                 if (m.parent().equals(IClass.OBJECT) && !OBJECT_METHODS.contains(m.name())) {
@@ -67,30 +75,31 @@ public class RBeanModel implements BeanModel, Serializable {
                 } else {
                     minfo.put(m.name(), m);
                     if (m.arguments().count() == 0 && !m.parent().equals(IClass.OBJECT)) {
-                        fluentGet.put(m.name(), new RBeanMethod(m));
+                        fluentGet.put(m.name(), new RBeanMethod(errors, m));
                     }
 
-                    if (m.arguments().count() == 1 && !m.parent().equals(IClass.OBJECT) && (m.returns().isVoid() || m
-                        .returns()
-                        .isSuper(m.parent()))) {
-                        fluentSet.put(m.name(), new RBeanMethod(m));
+                    if (m.arguments().count() == 1 && !m.parent().equals(IClass.OBJECT) && (m.returns().isVoid() || m.returns().isSuper(m.parent()))) {
+                        fluentSet.put(m.name(), new RBeanMethod(errors, m));
                     }
                 }
             }
         }
         methods = new OpenMap<>();
         for(String m : minfo.uniqueKeySet()) {
-            methods.put(m, FBeanModel.resolve(minfo.getAll(m)));
+            methods.put(m, FBeanModel.resolve(errors, minfo.getAll(m)));
         }
         properties = new OpenMap<>();
         for(Entry<String, RBeanPropertyBuilder> item : pinfo.entrySet()) {
-            BeanProperty resolved = FBeanModel.resolve(item.getValue());
+            BeanProperty resolved = FBeanModel.resolve(errors, item.getValue());
             if(resolved != null) {
                 properties.put(item.getKey(), resolved);
                 fluentGet.remove(item.getKey());
             }
         }
-        this.keys = new FlatSet<String>().join(properties.keySet()).join(fluentGet.keySet()).join(fluentSet.keySet());
+        this.keys = new FlatSet<String>()
+            .join(properties.keySet())
+            .join(fluentGet.keySet())
+            .join(fluentSet.keySet());
     }
     
     @Override

+ 48 - 22
assira.core/src/main/java/net/ranides/assira/reflection/impl/bean/RBeanPropertyBuilder.java

@@ -8,9 +8,7 @@
 package net.ranides.assira.reflection.impl.bean;
 
 import java.util.*;
-import java.util.stream.Collectors;
 
-import net.ranides.assira.collection.query.CQuery;
 import net.ranides.assira.generic.CompareUtils;
 import net.ranides.assira.reflection.*;
 import net.ranides.assira.reflection.util.ClassUtils;
@@ -21,12 +19,21 @@ import net.ranides.assira.reflection.util.ClassUtils;
  *
  * It is created by RBeanModel and used by FBeanModel to create instance of specific BeanProperty implementation.
  *
+ * Please note:
+ * We try to not throw exception on unexpected ill-formed methods which look like accessor, but they aren't.
+ * We simply ignore such methods. If you want to understand why some property is invisible in BeamModel, you should use
+ * #verify method.
+ *
+ * It is obvious and reasonable: we shouldn't throw errors for proper classes over which we do not have control just
+ * because they do not match some conventions (especially because some of them have really awkward edge cases)
+ *
  * @author Ranides Atterwim {@literal <ranides@gmail.com>}
  */
 public class RBeanPropertyBuilder {
     
+    private final IClass<?> parent;
     private final String name;
-    
+
     private final List<IMethod> vsetters = new ArrayList<>(2);
     private final List<IMethod> isetters = new ArrayList<>(2);
 
@@ -39,7 +46,8 @@ public class RBeanPropertyBuilder {
     private IClass<?> type;
     private IClass<?> item;
 
-    RBeanPropertyBuilder(String name) {
+    RBeanPropertyBuilder(IClass<?> parent, String name) {
+        this.parent = parent;
         this.name = name;
     }
 
@@ -122,14 +130,15 @@ public class RBeanPropertyBuilder {
         isetters.add(m);
     }
     
-    void validate() {
-        initVSetter();
-        initItem();
-        initISetter();
-        initType();
+    boolean validate(Collection<BeanException> errors) {
+        initVSetter(errors);
+        initItem(errors);
+        initISetter(errors);
+        initType(errors);
+        return type != null;
     }
 
-    private void initVSetter() throws IllegalArgumentException {
+    private void initVSetter(Collection<BeanException> errors) throws IllegalArgumentException {
         if(vgetter != null) {
             type = vgetter.returns();
             vsetter = vsetters.stream().filter(m -> m.matches(type)).findFirst().orElse(null);
@@ -143,9 +152,11 @@ public class RBeanPropertyBuilder {
                 if(type != null) {
                     vsetter = vsetters.stream().filter(m -> m.matches(type)).findFirst().orElse(null);
                 }
-                // just ignore ambiguous setters
+                if(errors != null) {
+                    errors.add(new BeanException(parent, "WARN: Ambiguous setter: " + name));
+                }
             }
-            if( vsetters.size() == 1) {
+            else if( vsetters.size() == 1) {
                 vsetter = vsetters.get(0);
                 type = vsetter.arguments().types().first().get();
             }
@@ -157,25 +168,33 @@ public class RBeanPropertyBuilder {
         }
     }
     
-    private void initItem() throws IllegalArgumentException {
+    private void initItem(Collection<BeanException> errors) throws IllegalArgumentException {
         if(type !=null && type.attributes().has(IAttribute.ARRAY)) {
             item = type.component();
         }
         if(igetter != null) {
             IClass<?> iitem = igetter.returns();
             if( item!=null && !iitem.equals(item) ) {
-                throw new IReflectiveException("Invalid array getter for: " + name);
+                if(errors != null) {
+                    errors.add(new BeanException(parent, "WARN: Invalid array getter for: " + name));
+                }
+                item = null;
+            } else {
+                item = iitem;
             }
-            item = iitem;
         }
     }
     
-    private void initISetter() throws IllegalArgumentException {
+    private void initISetter(Collection<BeanException> errors) throws IllegalArgumentException {
         if(item != null) {
             isetter = isetters.stream().filter(m -> m.matches(IClass.INT, item)).findFirst().orElse(null);
         } else {
             if( isetters.size() > 1) {
-                throw new IReflectiveException("Ambiguous property setters for: " + name);
+                if(errors != null) {
+                    errors.add(new BeanException(parent, "WARN: Ambiguous property indexed setters for: " + name));
+                }
+                isetter = null;
+                item = null;
             }
             if( isetters.size() == 1) {
                 isetter = isetters.get(0);
@@ -184,7 +203,7 @@ public class RBeanPropertyBuilder {
         }
     }
     
-    private void initType() throws IllegalArgumentException {
+    private void initType(Collection<BeanException> errors) throws IllegalArgumentException {
         if( null != item && null!=type) {
             IClass<?> ctype = null;
             if(type.attributes().has(IAttribute.ARRAY)) {
@@ -197,15 +216,22 @@ public class RBeanPropertyBuilder {
                 // if ctype is not array or collection, then it is false positive:
                 // property is not indexed at all and we ignore it safely
                 item = null;
+                if(errors != null) {
+                    errors.add(new BeanException(parent, "HINT: False indexed property: " + name));
+                }
             }
             if (!CompareUtils.equals(item, ctype)) {
                 item = null;
-                throw new IReflectiveException("Invalid type of indexed property: " + name);
+                if(errors != null) {
+                    errors.add(new BeanException(parent, "WARN: Invalid type of indexed property: " + name));
+                }
+            }
+        }
+        if(null == type && null==item) {
+            if(errors != null) {
+                errors.add(new BeanException(parent, "WARN: Unknown type of indexed property: " + name));
             }
         }
-//        if(null == type && null==item) {
-//            throw new IReflectiveException("Unknown type of indexed property: " + name);
-//        }
         if(type == null && item != null) {
             type = IClass.generic(List.class, ClassUtils.box(item));
         }