Ranides Atterwim 3 лет назад
Родитель
Сommit
568417782a

+ 179 - 0
assira.core/src/main/java/net/ranides/assira/reflection/ResolveConfiguration.java

@@ -0,0 +1,179 @@
+package net.ranides.assira.reflection;
+
+import java.io.Serializable;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.List;
+
+/**
+ * ResolveConfiguration class is configuration used by ResolvePattern.
+ * It is just collection of ResolveModel instances which will try to process object.
+ *
+ * Order of items inside configuration is used at processing time (first Model has higher priority than last)
+ *
+ * When you compile expression to ResolvePattern, you can pass your own ResolveConfiguration.
+ * If you don't do it, then DEFAULT configuration is used.
+ *
+ * Please note, that this configuration is immutable, you have to use builder pattern to create new one.
+ */
+public class ResolveConfiguration implements Serializable {
+
+    private static final long serialVersionUID = 1L;
+
+    /**
+     * Default configuration. It uses models:
+     *   - {@link ResolveModel#CONTEXT}
+     *   - {@link ResolveModel#ARRAY}
+     *   - {@link ResolveModel#LIST}
+     *   - {@link ResolveModel#MAP}
+     *   - {@link ResolveModel#MATCHER}
+     *   - {@link ResolveModel#CHARS}
+     *   - {@link ResolveModel#BEAN}
+     */
+    public static final ResolveConfiguration DEFAULT = new ResolveConfiguration(new ResolveModel[]{
+            ResolveModel.CONTEXT,
+            ResolveModel.ARRAY,
+            ResolveModel.LIST,
+            ResolveModel.MAP,
+            ResolveModel.MATCHER,
+            ResolveModel.CHARS,
+            ResolveModel.BEAN
+    });
+
+    private final ResolveModel[] models;
+
+    private ResolveConfiguration(ResolveModel[] models) {
+        this.models = models;
+    }
+
+    /**
+     * Factory: creates new builder for configuration
+     *
+     * @return Builder
+     */
+    public static ResolveModelsBuilder builder() {
+        return new ResolveModelsBuilder();
+    }
+
+    /**
+     * Selects first matching model and uses it to read property value.
+     *
+     * If property does not exist, it sets "status.error" flag and returns null.
+     *
+     * @param status output status
+     * @param that that
+     * @param name name
+     * @return Object
+     */
+    public Object get(ResolveModel.ResolveStatus status, Object that, String name) {
+        for(ResolveModel m : models) {
+            if(m.matches(that, name)) {
+                return m.get(status, that, name);
+            }
+        }
+        status.error = true;
+        return null;
+    }
+
+    /**
+     * Selects first matching model and uses it to change property value.
+     *
+     * If property does not exist, it sets "status.error" flag and does nothing.
+     *
+     * @param status output status
+     * @param that that
+     * @param name name
+     * @param value value
+     */
+    public void set(ResolveModel.ResolveStatus status, Object that, String name, Object value) {
+        for(ResolveModel m : models) {
+            if(m.matches(that, name)) {
+                m.set(status, that, name, value);
+                return;
+            }
+        }
+        status.error = true;
+    }
+
+    /**
+     * Selects first matching model and uses it to change property value.
+     * Returns previously assigned value.
+     *
+     * If property does not exist, it sets "status.error" flag and does nothing.
+     *
+     * @param status status
+     * @param that that
+     * @param name name
+     * @param value value
+     * @return Object
+     */
+    public Object replace(ResolveModel.ResolveStatus status, Object that, String name, Object value) {
+        for(ResolveModel m : models) {
+            if(m.matches(that, name)) {
+                return m.replace(status, that, name, value);
+            }
+        }
+        status.error = true;
+        return null;
+    }
+
+    /**
+     * Selects first matching model and uses it to deduce property type.
+     *
+     * If property does not exist, it sets "status.error" flag and returns null.
+     *
+     * @param status output status
+     * @param that that
+     * @param name name
+     * @return IClass
+     */
+    public IClass<?> type(ResolveModel.ResolveStatus status, IClass<?> that, String name) {
+        for(ResolveModel m : models) {
+            if(m.matchesType(that, name)) {
+                return m.type(status, that, name);
+            }
+        }
+        status.error = true;
+        return null;
+    }
+
+    /**
+     * Builder pattern for this configuration object
+     */
+    public static class ResolveModelsBuilder {
+
+        private final List<ResolveModel> models = new ArrayList<>();
+
+        /**
+         * Adds all models from provided configuration
+         *
+         * @param configuration models
+         * @return Builder
+         */
+        public ResolveModelsBuilder add(ResolveConfiguration configuration) {
+            this.models.addAll(Arrays.asList(configuration.models));
+            return this;
+        }
+
+        /**
+         * Adds provided model
+         *
+         * @param model model
+         * @return Builder
+         */
+        public ResolveModelsBuilder add(ResolveModel model) {
+            this.models.add(model);
+            return this;
+        }
+
+        /**
+         * Creates final immutable configuration object, usable by ResolvePattern.
+         *
+         * @return ResolveConfiguration
+         */
+        public ResolveConfiguration build() {
+            return new ResolveConfiguration(models.toArray(new ResolveModel[0]));
+        }
+    }
+
+}

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

@@ -2,10 +2,51 @@ package net.ranides.assira.reflection;
 
 import net.ranides.assira.collection.prototype.PrototypeMap;
 
+import java.util.Collections;
+import java.util.Map;
+
+/**
+ * This interface allows you instruct ResolvePattern how to work with your custom object.
+ *
+ * By default ResolvePattern uses {@link BeanModel#fluent(Object)}
+ * to read properties from object.
+ *
+ * If your class implements this interface, then ResolvePattern
+ * will use map returned by {@link #getResolveMap} instead.
+ *
+ * ResolveContext represents immutable objects.
+ * You can't implement custom "write" behaviour.
+ * In such cases, you have 3 options:
+ *  - just follow JavaBean convention
+ *  - just implement Map interface.
+ *  - implement your own ResolveModel and register it inside ResolveConfiguration
+ *
+ * This interface is useful if you DON'T want to implement Map interface,
+ * because it can trigger special behaviour of other framework. And you don't
+ * want to write custom ResolveModel
+ *
+ *
+ *
+ * @author Ranides Atterwim {@literal <ranides@gmail.com>}
+ */
 public interface ResolveContext {
 
-    ResolveContext EMPTY = () -> PrototypeMap.EMPTY;
+    /**
+     * Empty immutable ResolveContext
+     */
+    ResolveContext EMPTY = () -> Collections.emptyMap();
 
-    PrototypeMap getResolveMap();
+    /**
+     * Returns Map which is read-only view of this object.
+     * ResolvePattern will try to read properties from this map instead of
+     * invoking methods or getters.
+     *
+     * Please note that this method should be cheap, because it can be invoked
+     * a lot of times by resolver. It should be rather "view" than "copy" of object.
+     * Some form of virtual map is preferred.
+     *
+     * @return Map
+     */
+    Map<Object, Object> getResolveMap();
 
 }

+ 12 - 1
assira.core/src/main/java/net/ranides/assira/reflection/ResolveException.java

@@ -7,15 +7,26 @@
 package net.ranides.assira.reflection;
 
 /**
+ * This exception is thrown by ResolvePattern methods
  *
  * @author Ranides Atterwim {@literal <ranides@gmail.com>}
  */
 public class ResolveException extends IllegalArgumentException {
-    
+
+    /**
+     * Constructs new ResolveException
+     *
+     * @param message message
+     */
     public ResolveException(String message) {
         super(message);
     }
 
+    /**
+     * Constructs new ResolveException
+     *
+     * @param cause cause
+     */
     public ResolveException(Throwable cause) {
         super(cause);
     }

+ 205 - 21
assira.core/src/main/java/net/ranides/assira/reflection/ResolveModel.java

@@ -12,24 +12,115 @@ import java.util.Map;
 import java.util.Optional;
 import java.util.regex.Matcher;
 
+/**
+ * ResolvePattern can handle object of different types in different way.
+ * It delegates work to different, specialized ResolveModel instances.
+ *
+ * For example, we can write ResolveModel for array inspection, for map inspection,
+ * or for your own custom object.
+ *
+ * It is responsibility of ResolveModel itself to tell if some object can be
+ * handled by it. ResolvePattern makes tests against object using all provided
+ * ResolveModels from ResolveConfiguration and selects first one which told,
+ * that it can process the object.
+ */
 public abstract class ResolveModel extends SerializableCode {
 
     private static final long serialVersionUID = 1L;
 
     private static final String ARRAY_LENGTH = "length";
 
+    /**
+     * Returns true, if model is able to deduce type of field declared by "that" type
+     *
+     * Please remember that type deduction works without instance objects.
+     *
+     * You must return "true" in exactly the same situations as "matches",
+     * because obviously you are the one who knows type of value returned by "get"
+     *
+     * @param that that
+     * @param name name
+     * @return boolean
+     */
     public abstract boolean matchesType(IClass<?> that, String name);
 
+    /**
+     * Returns true, if model is able to read field form "that" object
+     *
+     * You must return "true" in exactly the same situations as "matchesType",
+     * because obviously you are the one who knows type of value returned by "get"
+     *
+     * @param that that
+     * @param name name
+     * @return boolean
+     */
     public abstract boolean matches(Object that, String name);
 
+    /**
+     * Reads value of property from "that" object.
+     *
+     * If property does not exist, you shouldn't throw exception, but
+     * you should change "status.error" flag and return null.
+     *
+     * @param status (output) status
+     * @param that that
+     * @param name name
+     * @return Object
+     */
     public abstract Object get(ResolveStatus status, Object that, String name);
 
+    /**
+     * Changes value of property from "that" object.
+     *
+     * If property does not exist, you shouldn't throw exception, but
+     * you should change "status.error" flag and do nothing.
+     *
+     * @param status (output) status
+     * @param that that
+     * @param name name
+     * @param value value
+     */
     public abstract void set(ResolveStatus status, Object that, String name, Object value);
 
+    /**
+     * Changes value of property from "that" object.
+     * Returns previous value of property.
+     *
+     * If property does not exist, you shouldn't throw exception, but
+     * you should change "status.error" flag and return null.
+     *
+     * @param status (output) status
+     * @param that that
+     * @param name name
+     * @param value value
+     * @return Object
+     */
     public abstract Object replace(ResolveStatus status, Object that, String name, Object value);
 
+    /**
+     * Returns deduced type of property from "that" type.
+     *
+     * If property does not exist, you shouldn't throw exception, but
+     * you should change "status.error" flag and return null.
+     *
+     * Please remember that type deduction works without instance objects.
+     * For example, for array type, you just return "component" type.
+     *
+     * @param status status
+     * @param that that
+     * @param name name
+     * @return IClass
+     */
     public abstract IClass<?> type(ResolveStatus status, IClass<?> that, String name);
 
+    /**
+     * Model for array types
+     *
+     * It will match if the object is array and field name is
+     *  - numerical value
+     *  - "length"
+     *
+     */
     public static final ResolveModel ARRAY = new ResolveModel() {
         @Override
         public boolean matches(Object that, String name) {
@@ -74,6 +165,15 @@ public abstract class ResolveModel extends SerializableCode {
         }
     };
 
+    /**
+     * Model for List types.
+     *
+     * It will match if the object is List and field name is numerical value.
+     *
+     * Please note that it won't match if you try to call some method declared
+     * by list (for example "size"). Such cases are handled by other models.
+     *
+     */
     public static final ResolveModel LIST = new ResolveModel() {
         @Override
         public boolean matches(Object that, String name) {
@@ -112,6 +212,14 @@ public abstract class ResolveModel extends SerializableCode {
         }
     };
 
+    /**
+     * Model for Map types.
+     *
+     * It will match if the object implements Map
+     *
+     * Please note that it is impossible to call any method declared by Map.
+     * Every field will be access using "get" and "put" operations.
+     */
     public static final ResolveModel MAP = new ResolveModel() {
         @Override
         public boolean matches(Object that, String _name) {
@@ -150,6 +258,15 @@ public abstract class ResolveModel extends SerializableCode {
         }
     };
 
+    /**
+     * Model for Matcher types.
+     *
+     * It will match if the object is {@link Matcher}
+     *
+     * Please note that it is impossible to call any method declared by Matcher.
+     * Every field will be accessed using "group" method.
+     * You can access matched groups by index or by name.
+     */
     public static final ResolveModel MATCHER = new ResolveModel() {
         @Override
         public boolean matches(Object that, String _name) {
@@ -163,6 +280,16 @@ public abstract class ResolveModel extends SerializableCode {
 
         @Override
         public Object get(ResolveStatus _s, Object that, String name) {
+            // @todo fix resolver: we can't read named group, if the name is "numerical"
+            // we should try to get group by name even if it is a number
+            // catch exception and then try to get group by index
+            //
+            // more correct, but much much slower
+            // altough resolve is not optimized for performance anyway
+            //
+            // maybe we should try to use just different syntax for
+            // index and name, for example "$index", ":name"
+
             if (StringTraits.isNumber(name)) {
                 return ((Matcher) that).group(Integer.parseInt(name));
             } else {
@@ -186,6 +313,16 @@ public abstract class ResolveModel extends SerializableCode {
         }
     };
 
+    /**
+     * Model for CharSequence types
+     *
+     * It will match if the object is CharSequence and field name is numerical value.
+     *
+     * Please note that it won't match if you try to call some method or getter
+     * (for example "length"). Such cases are handled by other models.
+     *
+     * @todo fix resolver: fix behaviour to match descritpion, do it like for LIST
+     */
     public static final ResolveModel CHARS = new ResolveModel() {
         @Override
         public boolean matches(Object that, String _name) {
@@ -218,6 +355,20 @@ public abstract class ResolveModel extends SerializableCode {
         }
     };
 
+    /**
+     * Model which works for any object
+     *
+     * It will match for every non-null value, so it should be inserted into
+     * ResolveConfiguration at the very end.
+     *
+     * It uses {@link BeanModel#fluent(Object)} to call getter method.
+     * If there is no getter, then it tries to read field value using reflection.
+     *
+     * Please note, it read even private fields (it shouldn't)
+     *
+     * @todo fix resolver: read only public fields
+     * @todo fix resolver: optimize this shit
+     */
     public static final ResolveModel BEAN = new ResolveModel() {
         @Override
         public boolean matches(Object that, String _name) {
@@ -292,22 +443,33 @@ public abstract class ResolveModel extends SerializableCode {
         }
     };
 
+    /**
+     * Model for ResolveContext
+     *
+     * It will match if the object implements ResolveContext
+     *
+     * Please note that it is impossible to call any method declared by ResolveContext.
+     * Every field will be accessed through context map.
+     *
+     * ResolveContext properties are handled in specific way
+     * if their type is ResolveFormat or ResolvePattern.
+     * Those objects will be read and then evaluated against ResolveContext.
+     */
     public static final ResolveModel CONTEXT = new ResolveModel() {
         @Override
         public boolean matches(Object that, String name) {
-            return (that instanceof ResolveContext) && ((ResolveContext) that).getResolveMap().containsKey(name);
+            return (that instanceof ResolveContext);
         }
 
         @Override
         public boolean matchesType(IClass<?> that, String _name) {
-            return that!=IClass.NULL && IClass.typeinfo(ResolveContext.class).isSuper(that);
+            return that.isSubclass(ResolveContext.class);
         }
 
         @Override
         public Object get(ResolveStatus status, Object that, String name) {
-            Optional<Object> out = ((ResolveContext) that).getResolveMap().getOptional(name);
-            if(out.isPresent()) {
-                Object result = out.get();
+            Object result = ((ResolveContext) that).getResolveMap().get(name);
+            if(result != null) {
                 if(result instanceof ResolvePattern) {
                     ResolvePattern pattern = (ResolvePattern) result;
                     return pattern.get(that);
@@ -335,33 +497,51 @@ public abstract class ResolveModel extends SerializableCode {
 
         @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);
+            return IClass.OBJECT;
         }
     };
 
+    /**
+     * This class is simple wrapper which is used as "output parameter".
+     *
+     * Methods inside ResolveModel can set error flag to inform that property does not exist.
+     */
     protected static final class ResolveStatus {
 
         public boolean error;
 
     }
 
-    public static abstract class AbstractGetter<T> extends ResolveModel {
+    /**
+     * This base abstract class can be used to write trivial ResolveModel which
+     * supports only read-only view and does not support type deduction.
+     *
+     * You have to implement only "get" method which returns property value as Optional.
+     *
+     * Please note that this model will consume objects of type T in every case, even if it can't find field.
+     *
+     * Please note that this model is unable to make type deducation and it will always return OBJECT.
+     *
+     * @param <T> T
+     */
+    public static abstract class AbstractView<T> extends ResolveModel {
 
         private final IClass<T> type;
 
-        public AbstractGetter(Class<T> type) {
+        protected AbstractView(Class<T> type) {
             this.type = IClass.typeinfo(type);
         }
 
-        public abstract Optional<Object> get(T that, String name);
+        /**
+         * Method should be implemented by subclass.
+         *
+         * If property with provided name does not exist, should return Optional.empty()
+         *
+         * @param that that
+         * @param name name
+         * @return Optional
+         */
+        protected abstract Optional<Object> get(T that, String name);
 
         @Override
         public boolean matchesType(IClass<?> that, String name) {
@@ -370,20 +550,24 @@ public abstract class ResolveModel extends SerializableCode {
 
         @Override
         public boolean matches(Object that, String name) {
-            return get(null, that, name) != null;
+            return type.isInstance(that);
         }
 
         @Override
-        public Object get(ResolveStatus _s, Object that, String name) {
+        public Object get(ResolveStatus status, Object that, String name) {
             if(type.isInstance(that)) {
-                return get(type.cast(that), name).orElse(null);
+                Optional<Object> out = get(type.cast(that), name);
+                if(out.isPresent()) {
+                    return out.get();
+                }
             }
+            status.error = true;
             return null;
         }
 
         @Override
         public IClass<?> type(ResolveStatus status, IClass<?> that, String name) {
-            return IClass.typefor(get(status, that, name));
+            return IClass.OBJECT;
         }
 
         @Override

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

@@ -1,91 +0,0 @@
-package net.ranides.assira.reflection;
-
-import java.io.Serializable;
-import java.util.ArrayList;
-import java.util.Arrays;
-import java.util.List;
-
-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,
-            ResolveModel.MATCHER,
-            ResolveModel.CHARS,
-            ResolveModel.BEAN
-    });
-
-    private final ResolveModel[] models;
-
-    private ResolveModels(ResolveModel[] models) {
-        this.models = models;
-    }
-
-    public static ResolveModelsBuilder builder() {
-        return new ResolveModelsBuilder();
-    }
-
-    public Object get(ResolveModel.ResolveStatus status, Object that, String name) {
-        for(ResolveModel m : models) {
-            if(m.matches(that, name)) {
-                return m.get(status, that, name);
-            }
-        }
-        status.error = true;
-        return null;
-    }
-
-    public void set(ResolveModel.ResolveStatus status, Object that, String name, Object value) {
-        for(ResolveModel m : models) {
-            if(m.matches(that, name)) {
-                m.set(status, that, name, value);
-                return;
-            }
-        }
-        status.error = true;
-    }
-
-    public Object replace(ResolveModel.ResolveStatus status, Object that, String name, Object value) {
-        for(ResolveModel m : models) {
-            if(m.matches(that, name)) {
-                return m.replace(status, that, name, value);
-            }
-        }
-        status.error = true;
-        return null;
-    }
-
-    public IClass<?> type(ResolveModel.ResolveStatus status, IClass<?> that, String name) {
-        for(ResolveModel m : models) {
-            if(m.matchesType(that, name)) {
-                return m.type(status, that, name);
-            }
-        }
-        status.error = true;
-        return null;
-    }
-
-    public static class ResolveModelsBuilder {
-
-        private final List<ResolveModel> models = new ArrayList<>();
-
-        public ResolveModelsBuilder add(ResolveModels models) {
-            this.models.addAll(Arrays.asList(models.models));
-            return this;
-        }
-
-        public ResolveModelsBuilder add(ResolveModel model) {
-            this.models.add(model);
-            return this;
-        }
-
-        public ResolveModels build() {
-            return new ResolveModels(models.toArray(new ResolveModel[0]));
-        }
-    }
-
-}

+ 118 - 26
assira.core/src/main/java/net/ranides/assira/reflection/ResolvePattern.java

@@ -16,10 +16,22 @@ import net.ranides.assira.generic.Wrapper;
 import net.ranides.assira.text.StringUtils;
 
 /**
+ * ResolvePattern represents compiled string expression used to read properties from object.
  *
- * Please note that there is special behaviour when you format against ResolveContext:
- * if resolved value is of type ResolveFormat or ResolvePattern then
- * it is evaluated against context again.
+ * You can construct expressions which read nested values, for example:
+ *      "transfer.sender.account.name"
+ * You can use dots to separate fields, or you can use brackets:
+ *      "transfer[sender][account][name]"
+ *      "transfer[sender].account[name]"
+ * You can read elements from array/list/map using both ways:
+ *      "file.lines.7.text"
+ *      "file.lines[7].text
+ *
+ * Please note that default configration contains some special behaviour.
+ * Most noticeable models are:
+ *  - {@link ResolveModel#CONTEXT}
+ *  - {@link ResolveModel#MATCHER}
+ *  - {@link ResolveModel#BEAN} (default handler for unrecognized objects)
  *
  * @author Ranides Atterwim {@literal <ranides@gmail.com>}
  */
@@ -29,7 +41,7 @@ public class ResolvePattern implements Serializable {
     
     private static final Pattern RE_INDEX = Pattern.compile("\\[([^\\]]+)\\]");
 
-    private final ResolveModels models;
+    private final ResolveConfiguration configuration;
     
     private final ResolveStrategy handler;
     
@@ -37,8 +49,8 @@ public class ResolvePattern implements Serializable {
     
     private final String[] tokens;
     
-    private ResolvePattern(ResolveModels models, ResolveStrategy handler, String expression) {
-        this.models = models;
+    private ResolvePattern(ResolveConfiguration configuration, ResolveStrategy handler, String expression) {
+        this.configuration = configuration;
         this.handler = handler;
         String[] array = StringUtils.replace(expression, RE_INDEX, m -> "."+m.group(1)+".").split("\\.");
         this.expression = expression;
@@ -66,89 +78,169 @@ public class ResolvePattern implements Serializable {
     public String toString() {
         return StringUtils.join(tokens, ".");
     }
-    
+
+    /**
+     * Factory: compiles provided expression using default ResolveConfiguration.
+     *
+     * Compiled expression uses {@link ResolveStrategy#THROW} and it will throw ResolveException on error.
+     *
+     * @param expression expression
+     * @return ResolvePattern
+     */
     public static ResolvePattern compile(String expression) {
-        return new ResolvePattern(ResolveModels.DEFAULT, ResolveStrategy.THROW, expression);
+        return new ResolvePattern(ResolveConfiguration.DEFAULT, ResolveStrategy.THROW, expression);
     }
-    
+
+    /**
+     * Factory: compiles provided expression using default ResolveConfiguration and provided ResolveStrategy.
+     *
+     * ResolveStrategy defines error handler of compiled expression. For example,
+     * it will throw if you passed {@link ResolveStrategy#THROW},
+     * but it will return string with quoted expression if you passed {@link ResolveStrategy#FORMAT}
+     *
+     * @param handler handler
+     * @param expression expression
+     * @return ResolvePattern
+     */
     public static ResolvePattern compile(ResolveStrategy handler, String expression) {
-        return new ResolvePattern(ResolveModels.DEFAULT, handler, expression);
+        return new ResolvePattern(ResolveConfiguration.DEFAULT, handler, expression);
     }
 
-    public static ResolvePattern compile(ResolveModels models, ResolveStrategy handler, String expression) {
-        return new ResolvePattern(models, handler, expression);
+    /**
+     * Factory: compiles provided expression using provided ResolveConfiguration and ResolveStrategy.
+     *
+     * ResolveConfiguration defines rules for handling different objects in special way.
+     *
+     * ResolveStrategy defines error handler of compiled expression. For example,
+     * it will throw if you passed {@link ResolveStrategy#THROW},
+     * but it will return string with quoted expression if you passed {@link ResolveStrategy#FORMAT}
+     *
+     * @param configuration configuration
+     * @param handler handler
+     * @param expression expression
+     * @return ResolvePattern
+     */
+    public static ResolvePattern compile(ResolveConfiguration configuration, ResolveStrategy handler, String expression) {
+        return new ResolvePattern(configuration, handler, expression);
     }
 
+    /**
+     * Returns expression string used to create this ResolvePattern
+     *
+     * @return String
+     */
     public String expression() {
         return expression;
     }
-    
+
+    /**
+     * Reads value of property from "that" object.
+     *
+     * Behaviour for error handling is defined by {@link ResolveStrategy}.
+     *
+     * @param that that
+     * @return Object
+     */
     public Object get(Object that) {
         ResolveModel.ResolveStatus status = new ResolveModel.ResolveStatus();
         for(int i=0,n=tokens.length-1; i<n; i++) {
-            that = models.get(status, that, tokens[i]);
+            that = configuration.get(status, that, tokens[i]);
             if(status.error) {
                 return handler.get(this, tokens[i]);
             }
         }
-        Object out = models.get(status, that, tokens[tokens.length-1]);
+        Object out = configuration.get(status, that, tokens[tokens.length-1]);
         if(status.error) {
             return handler.get(this);
         }
         return out;
     }
-    
+
+    /**
+     * Changes value of property from "that" object.
+     *
+     * Behaviour for error handling is defined by {@link ResolveStrategy}.
+     *
+     * @param that that
+     * @param value value
+     */
     public void set(Object that, Object value) {
         ResolveModel.ResolveStatus status = new ResolveModel.ResolveStatus();
         for(int i=0,n=tokens.length-1; i<n; i++) {
-            that = models.get(status, that, tokens[i]);
+            that = configuration.get(status, that, tokens[i]);
             if(status.error) {
                 handler.get(this, tokens[i]);
             }
         }
-        models.set(status, that, tokens[tokens.length-1], value);
+        configuration.set(status, that, tokens[tokens.length-1], value);
         if(status.error) {
             handler.set(this);
         }
     }
-    
+
+    /**
+     * Changes value of property from "that" object.
+     * Returns previous value of property.
+     *
+     * Behaviour for error handling is defined by {@link ResolveStrategy}.
+     *
+     * @param that that
+     * @param value value
+     * @return Object
+     */
     public Object replace(Object that, Object value) {
         ResolveModel.ResolveStatus status = new ResolveModel.ResolveStatus();
         for(int i=0,n=tokens.length-1; i<n; i++) {
-            that = models.get(status, that, tokens[i]);
+            that = configuration.get(status, that, tokens[i]);
             if(status.error) {
                 handler.get(this, tokens[i]);
             }
         }
-        Object out = models.replace(status, that, tokens[tokens.length-1], value);
+        Object out = configuration.replace(status, that, tokens[tokens.length-1], value);
         if(status.error) {
             return handler.replace(this);
         }
         return out;
     }
-    
+
+    /**
+     * Returns deduced type of property from "that" type.
+     *
+     * Behaviour for error handling is defined by {@link ResolveStrategy}.
+     *
+     * @param that that
+     * @return IClass
+     */
     public IClass<?> type(Object that) {
         ResolveModel.ResolveStatus status = new ResolveModel.ResolveStatus();
         IClass<?> type = IClass.typefor(that);
         for(int i=0,n=tokens.length-1; i<n; i++) {
-            type = models.type(status, type, tokens[i]);
+            type = configuration.type(status, type, tokens[i]);
             if(status.error) {
                 return handler.type(this, tokens[i]);
             }
         }
-        IClass<?> out = models.type(status, type, tokens[tokens.length-1]);
+        IClass<?> out = configuration.type(status, type, tokens[tokens.length-1]);
         if(status.error) {
             return handler.type(this);
         }
         return out;
     }
-    
+
+    /**
+     * Creates wrapper which uses this pattern to access data from provided object.
+     *
+     * Behaviour of wrapper for error handling is defined by {@link ResolveStrategy}.
+     *
+     * @param that that
+     * @return Wrapper
+     */
     public Wrapper<Object> bind(Object that) {
         return new RWrapper(that);
     }
 
     private Object writeReplace() {
-        return SerializationUtils.proxy(this, models, handler, expression);
+        return SerializationUtils.proxy(this, configuration, handler, expression);
     }
 
     private class RWrapper implements Wrapper<Object> {

+ 92 - 4
assira.core/src/main/java/net/ranides/assira/reflection/ResolveStrategy.java

@@ -8,38 +8,126 @@ package net.ranides.assira.reflection;
 
 import java.io.Serializable;
 
+/**
+ * This class defines error handling used by ResolvePattern *
+ */
 public abstract class ResolveStrategy implements Serializable {
 
     private static final long serialVersionUID = 1L;
-        
+
+    /**
+     * This strategy throws ResolveException if any error occurs
+     */
     public static final ResolveStrategy THROW = new EHThrow();
-    
+
+    /**
+     * This strategy
+     *  - returns quoted expression on read error
+     *  - throws ResolveException on write error
+     *  - returns String.class on type deduction error
+     *
+     * Quoting strategy: "{" + expression + "}"
+     */
     public static final ResolveStrategy FORMAT = new EHFormat();
 
+    /**
+     * This strategy
+     *  - returns quoted expression on read error
+     *  - throws ResolveException on write error
+     *  - returns String.class on type deduction error
+     *
+     * Quoting strategy: "{{" + expression + "}}"
+     */
     public static final ResolveStrategy HANDLEBARS = new EHHandleBars();
 
+    /**
+     * This strategy
+     *  - returns null on read error
+     *  - does nothing on write error
+     *  - returns IObject.NULL on type deduction error
+     */
     public static final ResolveStrategy SILENT = new EHSilent();
-    
+
+    /**
+     * This strategy:
+     *  - returns null on read error
+     *  - throws ResolveException on write error
+     *  - returns IObject.NULL on type deduction error
+     *
+     * But additionally:
+     *  - throws ResolveException if any intermediate field can't be accessed
+     */
     public static final ResolveStrategy NULL = new EHDefault(null);
     
     protected ResolveStrategy() {
         // do nothing
     }
-    
+
+    /**
+     * Factory: creates new strategy which returns default value on error.
+     *
+     * More detailed contract:
+     *  - returns "value" on read error
+     *  - throws ResolveException on write error
+     *  - returns typeof "value" on type deduction error
+     *
+     * But additionally:
+     *  - throws ResolveException if any intermediate field can't be accessed
+     *
+     * @param value value
+     * @return ResolveStrategy
+     */
     public static ResolveStrategy withDefault(Object value) {
         return new EHDefault(value);
     }
 
+    /**
+     * This is handler executed on read error for last component of expression
+     *
+     * @param resolver resolver
+     * @return Object
+     */
     public abstract Object get(ResolvePattern resolver);
 
+    /**
+     * This is handler executed on read error for intermediate component of expression
+     *
+     * @param resolver resolver
+     * @param token token
+     * @return Object
+     */
     public abstract Object get(ResolvePattern resolver, String token);
 
+    /**
+     * This is handler executed on replace error for last component of expression
+     *
+     * @param resolver resolver
+     * @return Object
+     */
     public abstract Object replace(ResolvePattern resolver);
 
+    /**
+     * This is handler executed on write error for last component of expression
+     *
+     * @param resolver resolver
+     */
     public abstract void set(ResolvePattern resolver);
 
+    /**
+     * This is handler executed on type deduction error for last component of expression
+     *
+     * @param resolver resolver
+     * @return IClass
+     */
     public abstract IClass<?> type(ResolvePattern resolver);
 
+    /**
+     * This is handler executed on type deduction error for intermediate component of expression
+     *
+     * @param resolver resolver
+     * @param token token
+     * @return IClass
+     */
     public abstract IClass<?> type(ResolvePattern resolver, String token);
     
     private static class EHSilent extends ResolveStrategy {

+ 1 - 1
assira.core/src/main/java/net/ranides/assira/reflection/walker/ObjectWalker.java

@@ -248,7 +248,7 @@ public class ObjectWalker {
 
 
         @Override
-        public PrototypeMap getResolveMap() {
+        public Map<Object, Object> getResolveMap() {
             return custom.get();
         }
 

+ 1 - 0
assira.core/src/main/java/net/ranides/assira/reflection/walker/WalkerContexts.java

@@ -1,5 +1,6 @@
 package net.ranides.assira.reflection.walker;
 
+import net.ranides.assira.collection.prototype.PrototypeMap;
 import net.ranides.assira.generic.TypeToken;
 import net.ranides.assira.reflection.*;
 

+ 2 - 0
assira.core/src/main/java/net/ranides/assira/text/ResolveFormat.java

@@ -36,6 +36,8 @@ import java.util.stream.Collectors;
  *      [ ] is used as indexing operator (when accessing array elements)
  *
  *      Please note, that in practice . and [ ] can be used interchangably
+ *      To read details about variable resolution, lets look at {@link ResolvePattern}
+ *      which is used internally to parse "ArgumentIndex" fragment.
  * 
  * FORMATS:
  * Please note, that different formats support different "Options"